pax_global_header00006660000000000000000000000064151411315740014514gustar00rootroot0000000000000052 comment=fd309b10894e555a1618e42775c61a329b2906b1 karpetrosyan-hishel-fd309b1/000077500000000000000000000000001514113157400161225ustar00rootroot00000000000000karpetrosyan-hishel-fd309b1/.github/000077500000000000000000000000001514113157400174625ustar00rootroot00000000000000karpetrosyan-hishel-fd309b1/.github/OLD_CHANGELOG.md000066400000000000000000000153041514113157400217740ustar00rootroot00000000000000## [0.1.3] - 2025-07-01 ### 🚀 Features - Add `path_prefix` support in `S3Storage` and `AsyncS3Storage` (#342) ### 🐛 Bug Fixes - Avoid race condition when unlinking files in `FileStorage` (#334) ### ⚙️ Miscellaneous Tasks - Remove `types-redis` from dev dependencies (#336) - Bump redis to 6.0.0 and address async `.close()` deprecation warning (#336) ## [0.1.2] - 2025-04-05 ### 🚀 Features - Use `SyncByteStream` instead of `ByteStream` (#298) ### 🐛 Bug Fixes - Add check for fips compliant python (#325) - Fix compatibility with httpx (#291) - Don't raise exceptions if date-containing headers are invalid (#318) - Fix for S3 Storage missing metadata in API request (#320) ## [0.1.1] - 2024-11-02 ### 🐛 Bug Fixes - Fix typing extensions not found (#290) ## [0.1.0] - 2024-11-02 ### 🚀 Features - Add support for Python 3.12 (#286) ### ⚙️ Miscellaneous Tasks - Drop Python 3.8 (#286) - Specify usedforsecurity=False in blake2b (#285) ## [0.0.33] - 2024-10-04 ### 📚 Documentation - Add Logging section to the documentation ## [0.0.32] - 2024-09-27 ### 🐛 Bug Fixes - Don't raise an exception if the `Date` header is not present (#273) ## [0.0.31] - 2024-09-22 ### 🐛 Bug Fixes - 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] - 2024-07-12 ### 🚀 Features - Add `cache_private` property to the controller to support acting as shared cache (#224) - Add `remove` support for storages (#241) ### 🐛 Bug Fixes - 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 (#247) ### ⚙️ Miscellaneous Tasks - Improve efficiency of scanning cached responses in `FileStorage` by reducing number of syscalls (#252) ## [0.0.29] - 2024-06-23 ### 📚 Documentation - Documentation hotfix (#244) ## [0.0.28] - 2024-06-23 ### 🚀 Features - Add `revalidated` response extension (#242) ## [0.0.27] - 2024-05-31 ### 🐛 Bug Fixes - Fix `RedisStorage` when using without ttl (#231) ## [0.0.26] - 2024-04-12 ### 🚀 Features - Prevent cache hits from resetting the ttl (#215) ### ⚙️ Miscellaneous Tasks - Expose `AsyncBaseStorage` and `BaseStorage` (#220) ## [0.0.25] - 2024-03-26 ### 🚀 Features - Add `force_cache` property to the controller, allowing RFC9111 rules to be completely disabled (#204) - Add `.gitignore` to cache directory created by `FileStorage` (#197) ### ⚙️ Miscellaneous Tasks - Remove `stale_*` headers from the `CacheControl` class (#199) ## [0.0.24] - 2024-02-14 ### 🐛 Bug Fixes - Fix `botocore is not installed` exception when using any kind of storage (#186) ## [0.0.23] - 2024-02-14 ### 🚀 Features - Make `S3Storage` to check staleness of all cache files with set interval (#182) - Support caching for `POST` and other HTTP methods (#183) ### 🐛 Bug Fixes - Fix an issue where an empty file in `FileCache` could cause a parsing error (#181) ## [0.0.22] - 2024-01-31 ### 🚀 Features - Make `FileStorage` to check staleness of all cache files with set interval (#169) - Support AWS S3 storages (#164) ### ⚙️ Miscellaneous Tasks - Move `typing_extensions` from requirements.txt to pyproject.toml (#161) ## [0.0.21] - 2023-12-29 ### 🐛 Bug Fixes - Fix inner transport and connection pool instances closing (#147) ### ⚙️ Miscellaneous Tasks - Improve error message when the storage type is incorrect (#138) ## [0.0.20] - 2023-12-12 ### 🚀 Features - Add in-memory storage (#133) - Allow customization of cache key generation (#130) ## [0.0.19] - 2023-11-30 ### 🚀 Features - Add `force_cache` extension to enforce the request to be cached, ignoring the HTTP headers (#117) - Support float numbers for storage ttl (#107) ### 🐛 Bug Fixes - Fix issue where sqlite storage cache get deleted immediately (#119) ## [0.0.18] - 2023-11-23 ### 🚀 Features - Add `cache_disabled` extension to temporarily disable the cache (#109) ### 🐛 Bug Fixes - Fix issue where freshness cannot be calculated to re-send request (#104) ### ⚙️ Miscellaneous Tasks - Update `datetime.datetime.utcnow()` to `datetime.datetime.now(datetime.timezone.utc)` since `datetime.datetime.utcnow()` has been deprecated (#111) ## [0.0.17] - 2023-11-06 ### 🐛 Bug Fixes - Fix `Last-Modified` validation ## [0.0.16] - 2023-10-25 ### 🚀 Features - Add `install_cache` function (#95) - Add sqlite support (#92) ### ⚙️ Miscellaneous Tasks - Move `ttl` argument to `BaseStorage` class (#94) ## [0.0.14] - 2023-10-23 ### 🚀 Features - Add `must-understand` response directive support (#90) ### ⚙️ Miscellaneous Tasks - Replace `AsyncResponseStream` with `AsyncCacheStream` (#86) ## [0.0.13] - 2023-10-05 ### 🚀 Features - Add support for Python 3.12 (#71) ### 🐛 Bug Fixes - Fix connections releasing from the connection pool (#83) ## [0.0.12] - 2023-09-08 ### 🚀 Features - Add metadata into the response extensions (#56) ## [0.0.11] - 2023-08-15 ### 🚀 Features - Add support for request `cache-control` directives (#42) ### ⚙️ Miscellaneous Tasks - Drop httpcore dependency (#40) - Support HTTP methods only if they are defined as cacheable (#37) ## [0.0.10] - 2023-08-07 ### 🚀 Features - Add Response metadata (#33) - Use stale responses only if the client is disconnected (#28) ### 📚 Documentation - Add API Reference documentation (#30) ## [0.0.9] - 2023-08-01 ### 🚀 Features - Expose Controller API (#23) ## [0.0.8] - 2023-07-31 ### ⚙️ Miscellaneous Tasks - 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] - 2023-07-30 ### 🚀 Features - Add support for `Heuristic Freshness` (#11) ### ⚙️ Miscellaneous Tasks - Change `Controller.cache_heuristically` to `Controller.allow_heuristics` (#12) - Handle import errors (#13) ## [0.0.6] - 2023-07-29 ### 🐛 Bug Fixes - Fix `Vary` header validation (#8) ### ⚙️ Miscellaneous Tasks - Dump original requests with the responses (#7) ## [0.0.5] - 2023-07-29 ### 🐛 Bug Fixes - Fix httpx response streaming ## [0.0.4] - 2023-07-29 ### ⚙️ Miscellaneous Tasks - Change `YamlSerializer` name to `YAMLSerializer` ## [0.0.3] - 2023-07-28 ### 🚀 Features - Add `from_cache` response extension ### ⚙️ Miscellaneous Tasks - Add `typing_extensions` into the requirements ## [0.0.2] - 2023-07-25 ### 🚀 Features - Add redis support - Make backends thread and task safe ### ⚙️ Miscellaneous Tasks - Add black as a new linter - Add an expire time for cached responseskarpetrosyan-hishel-fd309b1/.github/dependabot.yml000066400000000000000000000004231514113157400223110ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "pip" directory: "/" schedule: interval: "monthly" groups: python-packages: patterns: - "*" - package-ecosystem: "github-actions" directory: "/" schedule: interval: monthly karpetrosyan-hishel-fd309b1/.github/logo.jpg000066400000000000000000016162751514113157400211460ustar00rootroot00000000000000PNG  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`karpetrosyan-hishel-fd309b1/.github/workflows/000077500000000000000000000000001514113157400215175ustar00rootroot00000000000000karpetrosyan-hishel-fd309b1/.github/workflows/ci.yml000066400000000000000000000040771514113157400226450ustar00rootroot00000000000000name: CI on: push: branches: ["main", "master"] pull_request: branches: ["main", "master"] permissions: contents: read jobs: test: strategy: matrix: os: ["ubuntu-latest"] python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] runs-on: ${{ matrix.os }} steps: - name: Checkout (official GitHub action) uses: actions/checkout@v4 with: fetch-depth: 0 - name: Install uv (official Astral action) uses: astral-sh/setup-uv@v7 with: version: "0.8.9" enable-cache: true python-version: ${{ matrix.python-version }} - name: Set up Python (using uv) run: uv python install - name: Install all dependencies run: uv sync --all-extras - name: Run linting run: scripts/lint - name: Run tests run: scripts/test - name: Upload Coverage if: runner.os != 'macOS' uses: coverallsapp/github-action@v2 with: parallel: true file: coverage.xml upload-dev-docs: name: Upload Dev Docs needs: test runs-on: ubuntu-latest if: github.event_name == 'push' && github.ref == 'refs/heads/master' permissions: contents: write steps: - name: Checkout (official GitHub action) uses: actions/checkout@v4 with: fetch-depth: 0 - name: Install uv (official Astral action) uses: astral-sh/setup-uv@v7 with: version: "0.8.9" enable-cache: true - name: Set up Python (using uv) run: uv python install - name: Install dependencies run: uv sync - name: Deploy dev docs run: | git config user.name "Kar Petrosyan" git config user.email "karpetrosyanpy@gmail.com" uv run mike deploy --push --update-aliases dev coveralls: name: Finish Coveralls needs: test runs-on: ubuntu-latest steps: - name: Finished uses: coverallsapp/github-action@v2 with: parallel-finished: true karpetrosyan-hishel-fd309b1/.github/workflows/publish.yml000066400000000000000000000060011514113157400237050ustar00rootroot00000000000000name: publish on: push: tags: - "[0-9]+.[0-9]+.[0-9]+" - "[0-9]+.[0-9]+.[0-9]+.post[0-9]+" - "[0-9]+.[0-9]+.[0-9]+[a-b][0-9]+" - "[0-9]+.[0-9]+.[0-9]+rc[0-9]+" - "[0-9]+.[0-9]+.[0-9]+.dev[0-9]+" workflow_dispatch: env: PYTHON_VERSION: 3.9 jobs: build: name: Build the source tarball and the wheel runs-on: ubuntu-latest environment: release steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 fetch-tags: 0 - name: Install uv (official Astral action) uses: astral-sh/setup-uv@v7 with: version: "0.8.9" enable-cache: true python-version: ${{ env.PYTHON_VERSION }} - name: UV build run: uv build - name: Archive packages uses: actions/upload-artifact@v5 with: name: dist path: dist publish: name: Publish build artifacts to the PyPI needs: build runs-on: ubuntu-latest environment: release permissions: id-token: write steps: - name: Retrieve packages uses: actions/download-artifact@v4 - name: Upload packages uses: pypa/gh-action-pypi-publish@release/v1 publish-docs: name: Publish documentation to GitHub Pages runs-on: ubuntu-latest needs: build permissions: contents: write steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 fetch-tags: 0 - name: Install uv (official Astral action) uses: astral-sh/setup-uv@v7 with: version: "0.8.9" enable-cache: true python-version: ${{ env.PYTHON_VERSION }} - name: Set up Python (using uv) run: uv python install - name: Publish docs run: | git config user.name "Kar Petrosyan" git config user.email "karpetrosyanpy@gmail.com" uv run mike deploy --push --update-aliases $(scripts/get_doc_version.py) release: name: Create GitHub release runs-on: ubuntu-latest needs: build permissions: contents: write steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 fetch-tags: 0 - name: Install uv (official Astral action) uses: astral-sh/setup-uv@v7 with: version: "0.8.9" enable-cache: true python-version: ${{ env.PYTHON_VERSION }} - name: Set up Python (using uv) run: uv python install - name: Download packages uses: actions/download-artifact@v4 with: name: dist - name: Generate a changelog uses: orhun/git-cliff-action@v4 with: args: "0.1.3.. --latest" env: OUTPUT: release-notes.md GITHUB_REPO: ${{ github.repository }} - name: Create release with assets uses: softprops/action-gh-release@v2 with: files: ./dist/* body_path: release-notes.md karpetrosyan-hishel-fd309b1/.gitignore000066400000000000000000000001161514113157400201100ustar00rootroot00000000000000.venv/ venv/ __pycache__/ .coverage .cache/ .idea/ coverage.xml site .DS_Storekarpetrosyan-hishel-fd309b1/.python-version000066400000000000000000000000051514113157400211220ustar00rootroot000000000000003.10 karpetrosyan-hishel-fd309b1/.vscode/000077500000000000000000000000001514113157400174635ustar00rootroot00000000000000karpetrosyan-hishel-fd309b1/.vscode/settings.json000066400000000000000000000002231514113157400222130ustar00rootroot00000000000000{ "python.testing.pytestArgs": [ "tests" ], "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true }karpetrosyan-hishel-fd309b1/CHANGELOG.md000066400000000000000000000234211514113157400177350ustar00rootroot00000000000000## What's Changed in 1.1.9 ### ⚙️ Miscellaneous Tasks * chore: drop python 3.9 support by @CharString in [#418](https://github.com/karpetrosyan/hishel/pull/418) ### 📦 Dependencies * chore(deps-dev): bump the python-packages group across 1 directory with 5 updates by @dependabot[bot] in [#440](https://github.com/karpetrosyan/hishel/pull/440) ### Contributors * @CharString * @dependabot[bot] **Full Changelog**: https://github.com/karpetrosyan/hishel/compare/1.1.8...1.1.9 ## What's Changed in 1.1.8 ### ⚙️ Miscellaneous Tasks * chore(ci): remove redis action by @karpetrosyan in [#428](https://github.com/karpetrosyan/hishel/pull/428) ### 🐛 Bug Fixes * fix: prevent race conditions by @karpetrosyan in [#436](https://github.com/karpetrosyan/hishel/pull/436) ### Contributors * @karpetrosyan **Full Changelog**: https://github.com/karpetrosyan/hishel/compare/1.1.7...1.1.8 ## What's Changed in 1.1.7 ### ♻️ Refactoring * refactor(storage): create sqlite database path only when creating connections by @jeefberkey in [#426](https://github.com/karpetrosyan/hishel/pull/426) ### ⚙️ Miscellaneous Tasks * chore(deps-dev): bump the python-packages group with 5 updates by @dependabot[bot] in [#424](https://github.com/karpetrosyan/hishel/pull/424) ### 🐛 Bug Fixes * fix(cache): Lambda parameter name clashes the loop variable being closed over by @dump247 in [#427](https://github.com/karpetrosyan/hishel/pull/427) ### 📚 Documentation * add release process guidelines for maintainers by @karpetrosyan ### 🚀 Features * Feature/accept pathlib path in SqliteStorage by @daudef in [#419](https://github.com/karpetrosyan/hishel/pull/419) ### Contributors * @karpetrosyan * @daudef * @dependabot[bot] * @jeefberkey * @dump247 **Full Changelog**: https://github.com/karpetrosyan/hishel/compare/1.1.6...1.1.7 ## What's Changed in 1.1.6 ### 📚 Documentation * remove some stale httpx configs by @karpetrosyan ### 🚀 Features * Add support for request no-cache directive by @karpetrosyan in [#416](https://github.com/karpetrosyan/hishel/pull/416) ### Contributors * @karpetrosyan **Full Changelog**: https://github.com/karpetrosyan/hishel/compare/1.1.5...1.1.6 ## What's Changed in 1.1.5 ### 🐛 Bug Fixes * filter out soft-deleted, expired and incomplete entries in `get_entries` by @karpetrosyan ### Contributors * @karpetrosyan **Full Changelog**: https://github.com/karpetrosyan/hishel/compare/1.1.4...1.1.5 ## What's Changed in 1.1.4 ### 🐛 Bug Fixes * don't raise an error on consumed streams that were read into memory by @karpetrosyan * close sqlite connections properly by @karpetrosyan ### Contributors * @karpetrosyan **Full Changelog**: https://github.com/karpetrosyan/hishel/compare/1.1.3...1.1.4 ## What's Changed in 1.1.3 ### ⚙️ Miscellaneous Tasks * improve git-cliff docs by @karpetrosyan ### 🐛 Bug Fixes * fix: add BaseFilter to __all__ exports by @martinblech in [#408](https://github.com/karpetrosyan/hishel/pull/408) * fix: set `after_revalidation=True` for `NeedsToBeUpdated` -> `FromCache` transition by @jlopex in [#402](https://github.com/karpetrosyan/hishel/pull/402) ### Contributors * @karpetrosyan * @martinblech * @jlopex **Full Changelog**: https://github.com/karpetrosyan/hishel/compare/1.1.2...1.1.3 ## What's Changed in 1.1.2 ### 🐛 Bug Fixes * respect shared option when excluding unstorable headers by @karpetrosyan * remove s-maxage consideration for private caches by @karpetrosyan * ensure 304 responses don't leak by @karpetrosyan ### Contributors * @karpetrosyan * @jlopex **Full Changelog**: https://github.com/karpetrosyan/hishel/compare/1.1.1...1.1.2 ## What's Changed in 1.1.1 ### ⚙️ Miscellaneous Tasks * chore(deps-dev): bump the python-packages group with 10 updates by @dependabot[bot] in [#396](https://github.com/karpetrosyan/hishel/pull/396) ### 📦 Dependencies * chore(deps): bump astral-sh/setup-uv from 5 to 7 by @dependabot[bot] in [#393](https://github.com/karpetrosyan/hishel/pull/393) * chore(deps): bump actions/download-artifact from 4 to 6 by @dependabot[bot] in [#394](https://github.com/karpetrosyan/hishel/pull/394) * chore(deps): bump actions/upload-artifact from 4 to 5 by @dependabot[bot] in [#395](https://github.com/karpetrosyan/hishel/pull/395) ### Contributors * @karpetrosyan * @dependabot[bot] **Full Changelog**: https://github.com/karpetrosyan/hishel/compare/1.1.0...1.1.1 ## What's Changed in 1.1.0 ### ⚙️ Miscellaneous Tasks * add in memory example by @karpetrosyan ### 🐛 Bug Fixes * pass any response with non-expected status code on revalidation to client by @karpetrosyan * pass any response with non-expected status code on revalidation to client by @karpetrosyan ### 🚀 Features * allow setting storage base with via `database_path` for sqlite storage by @karpetrosyan ### Contributors * @karpetrosyan **Full Changelog**: https://github.com/karpetrosyan/hishel/compare/1.0.0...1.1.0 ## What's Changed in 1.0.0 ### ⚙️ Miscellaneous Tasks * add examples, improve docs by @karpetrosyan ### Contributors * @karpetrosyan **Full Changelog**: https://github.com/karpetrosyan/hishel/compare/1.0.0b1...1.0.0 ## What's Changed in 1.0.0b1 ### ♻️ Refactoring * add policies by @karpetrosyan ### ⚙️ Miscellaneous Tasks * add graphql docs by @karpetrosyan * improve sans-io diagram colors by @karpetrosyan ### 🐛 Bug Fixes * filter out `Transfer-Encoding` header for asgi responses by @karpetrosyan * body-sensitive responses caching by @karpetrosyan ### 🚀 Features * add global `use_body_key` setting by @karpetrosyan ### Contributors * @karpetrosyan **Full Changelog**: https://github.com/karpetrosyan/hishel/compare/1.0.0.dev3...1.0.0b1 ## What's Changed in 1.0.0.dev3 ### ♻️ Refactoring * automatically generate httpx sync integration from async by @karpetrosyan * replace pairs with entries, simplify storage API by @karpetrosyan ### ⚙️ Miscellaneous Tasks * more robust compressed response caching by @karpetrosyan * add custom integrations docs by @karpetrosyan * simplify metadata docs by @karpetrosyan ### 🐛 Bug Fixes * add date header for proper age calculation by @karpetrosyan * handle httpx iterable usage instead of iterator correctly by @karpetrosyan * fix compressed data caching for requests by @karpetrosyan * raise on consumed httpx streams, which we can't store as is (it's already decoded) by @karpetrosyan * add missing permissions into `publish.yml` by @karpetrosyan ### 🚀 Features * add logging for asgi by @karpetrosyan * add blacksheep integration examples by @karpetrosyan * add integrations with fastapi and asgi by @karpetrosyan ### Contributors * @karpetrosyan **Full Changelog**: https://github.com/karpetrosyan/hishel/compare/1.0.0.dev2...1.0.0.dev3 ## What's Changed in 1.0.0.dev2 ### ⚙️ Miscellaneous Tasks * fix time travel date, explicitly specify the timezone by @karpetrosyan * add import without extras check in ci by @karpetrosyan * remove redundant utils and tests by @karpetrosyan ### 🐛 Bug Fixes * don't raise an error on 3xx during revalidation by @karpetrosyan * fix check for storing auth requests by @karpetrosyan ### 🚀 Features * add hishel_created_at response metadata by @karpetrosyan ### Contributors * @karpetrosyan **Full Changelog**: https://github.com/karpetrosyan/hishel/compare/1.0.0.dev1...1.0.0.dev2 ## What's Changed in 1.0.0.dev1 ### ⚙️ Miscellaneous Tasks * remove some redundant utils methods by @karpetrosyan ### 📦 Dependencies * improve git-cliff by @karpetrosyan * install async extra with httpx by @karpetrosyan * make `anysqlite` optional dependency by @karpetrosyan * make httpx and async libs optional dependencies by @karpetrosyan ### Contributors * @karpetrosyan **Full Changelog**: https://github.com/karpetrosyan/hishel/compare/1.0.0.dev0...1.0.0.dev1 ## What's Changed in 1.0.0.dev0 ### ⚙️ Miscellaneous Tasks * improve docs versioning, deploy dev doc on ci by @karpetrosyan * use mike powered versioning by @karpetrosyan ### Contributors * @karpetrosyan **Full Changelog**: https://github.com/karpetrosyan/hishel/compare/0.1.5...1.0.0.dev0 ## What's Changed in 0.1.5 ### ⚙️ Miscellaneous Tasks * remove some redundant files from repo by @karpetrosyan ### 🐛 Bug Fixes * fix some line breaks by @karpetrosyan ### 🚀 Features * increase requests buffer size to 128KB, disable charset detection by @karpetrosyan * feat: add close method to storages API by @karpetrosyan in [#384](https://github.com/karpetrosyan/hishel/pull/384) * better cache-control parsing by @karpetrosyan * set chunk size to 128KB for httpx to reduce SQLite read/writes by @karpetrosyan ### Contributors * @karpetrosyan **Full Changelog**: https://github.com/karpetrosyan/hishel/compare/0.1.4...0.1.5 ## What's Changed in 0.1.4 ### ⚙️ Miscellaneous Tasks * move some tests to beta by @karpetrosyan * add sqlite tests for new storage by @karpetrosyan * temporary remove python3.14 from CI by @karpetrosyan * chore(internal): remove src folder by @karpetrosyan in [#373](https://github.com/karpetrosyan/hishel/pull/373) * chore: improve CI by @karpetrosyan in [#369](https://github.com/karpetrosyan/hishel/pull/369) ### 🐛 Bug Fixes * fix beta imports by @karpetrosyan * create an sqlite file in a cache folder by @karpetrosyan ### 🚀 Features * better async implemetation for sqlite storage by @karpetrosyan * get rid of some locks from sqlite storage by @karpetrosyan * add sqlite storage for beta storages by @karpetrosyan * feat: allow already consumed streams with `CacheTransport` by @jamesbraza in [#377](https://github.com/karpetrosyan/hishel/pull/377) * feat: add support for a sans-IO API by @karpetrosyan in [#366](https://github.com/karpetrosyan/hishel/pull/366) ### Contributors * @karpetrosyan * @jamesbraza * @GugNersesyan * @dependabot[bot] * @mmdbalkhi * @AstraLuma * @deathaxe **Full Changelog**: https://github.com/karpetrosyan/hishel/compare/0.1.3...0.1.4 karpetrosyan-hishel-fd309b1/CODE_OF_CONDUCT.md000066400000000000000000000121531514113157400207230ustar00rootroot00000000000000# 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. karpetrosyan-hishel-fd309b1/LICENSE000066400000000000000000000027251514113157400171350ustar00rootroot00000000000000Copyright © 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. karpetrosyan-hishel-fd309b1/README.md000066400000000000000000000254251514113157400174110ustar00rootroot00000000000000

Hishel Logo Hishel Logo

Hishel

Elegant HTTP Caching for Python

PyPI version Python versions License Coverage Downloads

--- **Hishel** (հիշել, *to remember* in Armenian) is a modern HTTP caching library for Python that implements [RFC 9111](https://www.rfc-editor.org/rfc/rfc9111.html) specifications. It provides seamless caching integration for popular HTTP clients with minimal code changes. ## ✨ Features - 🎯 **RFC 9111 Compliant** - Fully compliant with the latest HTTP caching specification - 🔌 **Easy Integration** - Drop-in support for HTTPX, Requests, ASGI, FastAPI, and BlackSheep - 💾 **Flexible Storage** - SQLite backend with more coming soon - ⚡ **High Performance** - Efficient caching with minimal overhead - 🔄 **Async & Sync** - Full support for both synchronous and asynchronous workflows - 🎨 **Type Safe** - Fully typed with comprehensive type hints - 🧪 **Well Tested** - Extensive test coverage and battle-tested - 🎛️ **Configurable** - Fine-grained control over caching behavior with flexible policies - 💨 **Memory Efficient** - Streaming support prevents loading large payloads into memory - 🌐 **Universal** - Works with any ASGI application (Starlette, Litestar, BlackSheep, etc.) - 🎯 **GraphQL Support** - Cache GraphQL queries with body-sensitive content caching ## 📦 Installation ```bash pip install hishel ``` ### Optional Dependencies Install with specific integration support: ```bash pip install hishel[httpx] # For HTTPX support pip install hishel[requests] # For Requests support pip install hishel[fastapi] # For FastAPI support (includes ASGI) ``` Or install multiple: ```bash pip install hishel[httpx,requests,fastapi] ``` > [!NOTE] > ASGI middleware has no extra dependencies - it's included in the base installation. ## 🚀 Quick Start ### With HTTPX **Synchronous:** ```python from hishel.httpx import SyncCacheClient client = SyncCacheClient() # First request - fetches from origin response = client.get("https://api.example.com/data") print(response.extensions["hishel_from_cache"]) # False # Second request - served from cache response = client.get("https://api.example.com/data") print(response.extensions["hishel_from_cache"]) # True ``` **Asynchronous:** ```python from hishel.httpx import AsyncCacheClient async with AsyncCacheClient() as client: # First request - fetches from origin response = await client.get("https://api.example.com/data") print(response.extensions["hishel_from_cache"]) # False # Second request - served from cache response = await client.get("https://api.example.com/data") print(response.extensions["hishel_from_cache"]) # True ``` ### With Requests ```python import requests from hishel.requests import CacheAdapter session = requests.Session() session.mount("https://", CacheAdapter()) session.mount("http://", CacheAdapter()) # First request - fetches from origin response = session.get("https://api.example.com/data") # Second request - served from cache response = session.get("https://api.example.com/data") print(response.headers.get("X-Hishel-From-Cache")) # "True" ``` ### With ASGI Applications Add caching middleware to any ASGI application: ```python from hishel.asgi import ASGICacheMiddleware # Wrap your ASGI app app = ASGICacheMiddleware(app) # Or configure with options from hishel import AsyncSqliteStorage, CacheOptions, SpecificationPolicy app = ASGICacheMiddleware( app, storage=AsyncSqliteStorage(), policy=SpecificationPolicy( cache_options=CacheOptions(shared=True) ), ) ``` ### With FastAPI Add Cache-Control headers using the `cache()` dependency: ```python from fastapi import FastAPI from hishel.fastapi import cache app = FastAPI() @app.get("/api/data", dependencies=[cache(max_age=300, public=True)]) async def get_data(): # Cache-Control: public, max-age=300 return {"data": "cached for 5 minutes"} # Optionally wrap with ASGI middleware for local caching according to specified rules from hishel.asgi import ASGICacheMiddleware from hishel import AsyncSqliteStorage app = ASGICacheMiddleware(app, storage=AsyncSqliteStorage()) ``` ### With BlackSheep Use BlackSheep's native `cache_control` decorator with Hishel's ASGI middleware: ```python from blacksheep import Application, get from blacksheep.server.headers.cache import cache_control app = Application() @get("/api/data") @cache_control(max_age=300, public=True) async def get_data(): # Cache-Control: public, max-age=300 return {"data": "cached for 5 minutes"} ``` ## 🎛️ Advanced Configuration ### Caching Policies Hishel supports two types of caching policies: **SpecificationPolicy** - RFC 9111 compliant HTTP caching (default): ```python from hishel import CacheOptions, SpecificationPolicy from hishel.httpx import SyncCacheClient client = SyncCacheClient( policy=SpecificationPolicy( cache_options=CacheOptions( shared=False, # Use as private cache (browser-like) supported_methods=["GET", "HEAD", "POST"], # Cache GET, HEAD, and POST allow_stale=True # Allow serving stale responses ) ) ) ``` **FilterPolicy** - Custom filtering logic for fine-grained control: ```python from hishel import FilterPolicy, BaseFilter, Request from hishel.httpx import AsyncCacheClient class CacheOnlyAPIRequests(BaseFilter[Request]): def needs_body(self) -> bool: return False def apply(self, item: Request, body: bytes | None) -> bool: return "/api/" in str(item.url) client = AsyncCacheClient( policy=FilterPolicy( request_filters=[CacheOnlyAPIRequests()] ) ) ``` [Learn more about policies →](https://hishel.com/dev/policies/) ### Custom Storage Backend ```python from hishel import SyncSqliteStorage from hishel.httpx import SyncCacheClient storage = SyncSqliteStorage( database_path="my_cache.db", default_ttl=7200.0, # Cache entries expire after 2 hours refresh_ttl_on_access=True # Reset TTL when accessing cached entries ) client = SyncCacheClient(storage=storage) ``` ### GraphQL and Body-Sensitive Caching Cache GraphQL queries and other POST requests by including the request body in the cache key. **Using per-request header:** ```python from hishel import FilterPolicy from hishel.httpx import SyncCacheClient client = SyncCacheClient( policy=FilterPolicy() ) # Cache GraphQL queries - different queries get different cache entries graphql_query = """ query GetUser($id: ID!) { user(id: $id) { name email } } """ response = client.post( "https://api.example.com/graphql", json={"query": graphql_query, "variables": {"id": "123"}}, headers={"X-Hishel-Body-Key": "true"} # Enable body-based caching ) # Different query will be cached separately response = client.post( "https://api.example.com/graphql", json={"query": graphql_query, "variables": {"id": "456"}}, headers={"X-Hishel-Body-Key": "true"} ) ``` **Using global configuration:** ```python from hishel.httpx import SyncCacheClient from hishel import FilterPolicy # Enable body-based caching for all requests client = SyncCacheClient(policy=FilterPolicy(use_body_key=True)) # All POST requests automatically include body in cache key response = client.post( "https://api.example.com/graphql", json={"query": graphql_query, "variables": {"id": "123"}} ) ``` ## 🏗️ Architecture Hishel uses a **sans-I/O state machine** architecture that separates HTTP caching logic from I/O operations: - ✅ **Correct** - Fully RFC 9111 compliant - ✅ **Testable** - Easy to test without network dependencies - ✅ **Flexible** - Works with any HTTP client or server - ✅ **Type Safe** - Clear state transitions with full type hints ## 🔮 Roadmap We're actively working on: - 🎯 Performance optimizations - 🎯 More integrations - 🎯 Partial responses support ## 📚 Documentation Comprehensive documentation is available at [https://hishel.com/dev](https://hishel.com/dev) - [Getting Started](https://hishel.com) - [HTTPX Integration](https://hishel.com/dev/integrations/httpx) - [Requests Integration](https://hishel.com/dev/integrations/requests) - [ASGI Integration](https://hishel.com/dev/asgi) - [FastAPI Integration](https://hishel.com/dev/fastapi) - [BlackSheep Integration](https://hishel.com/dev/integrations/blacksheep) - [GraphQL Integration](https://hishel.com/dev/integrations/graphql) - [Storage Backends](https://hishel.com/dev/storages) - [Request/Response Metadata](https://hishel.com/dev/metadata) - [RFC 9111 Specification](https://hishel.com/dev/specification) ## 🤝 Contributing Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change. See our [Contributing Guide](https://hishel.com/dev/contributing) for more details. ## 📄 License This project is licensed under the BSD-3-Clause License - see the [LICENSE](LICENSE) file for details. ## 💖 Support If you find Hishel useful, please consider: - ⭐ Starring the repository - 🐛 Reporting bugs and issues - 💡 Suggesting new features - 📖 Improving documentation - ☕ [Buying me a coffee](https://buymeacoffee.com/karpetrosyan) ## 🙏 Acknowledgments Hishel is inspired by and builds upon the excellent work in the Python HTTP ecosystem, particularly: - [HTTPX](https://github.com/encode/httpx) - A next-generation HTTP client for Python - [Requests](https://github.com/psf/requests) - The classic HTTP library for Python - [RFC 9111](https://www.rfc-editor.org/rfc/rfc9111.html) - HTTP Caching specification ---

Made with ❤️ by Kar Petrosyan

karpetrosyan-hishel-fd309b1/cliff.toml000066400000000000000000000060341514113157400201050ustar00rootroot00000000000000[git] # Parse commits according to the conventional commits specification. # See https://www.conventionalcommits.org conventional_commits = true # Exclude commits that do not match the conventional commits specification. filter_unconventional = true # Split commits on newlines, treating each line as an individual commit. split_commits = false # An array of regex based parsers to modify commit messages prior to further processing. commit_preprocessors = [{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" }] # Exclude commits that are not matched by any commit parser. filter_commits = true # Order releases topologically instead of chronologically. topo_order = false # Order of commits in each group/release within the changelog. # Allowed values: newest, oldest sort_commits = "newest" commit_parsers = [ { message = "^feat", group = "🚀 Features" }, { message = "^fix", group = "🐛 Bug Fixes" }, { message = "^doc", group = "📚 Documentation" }, { message = "^perf", group = "⚡ Performance" }, { message = "^refactor", group = "♻️ Refactoring" }, { message = "^test", group = "✅ Testing" }, { message = "^style", group = "🎨 Style" }, { message = "^chore\\(deps\\)", group = "📦 Dependencies" }, { message = "^chore|^ci", group = "⚙️ Miscellaneous Tasks" }, { body = ".*security", group = "🔒 Security" }, ] [changelog] # A Tera template to be rendered for each release in the changelog. # See https://keats.github.io/tera/docs/#introduction body = """ ## What's Changed {%- if version %} in {{ version | trim_start_matches(pat="v") }}{%- endif -%} {% for group, commits in commits | group_by(attribute="group") %} ### {{ group | upper_first }} {% for commit in commits %} {% if commit.remote.pr_title -%} {%- set commit_message = commit.remote.pr_title -%} {%- else -%} {%- set commit_message = commit.message -%} {%- endif -%} * {{ commit_message | split(pat="\n") | first | trim }}\ {% if commit.remote.username %} by @{{ commit.remote.username }}{%- endif -%} {% if commit.remote.pr_number %} in \ [#{{ commit.remote.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.remote.pr_number }}) \ {%- endif %} {%- endfor %} {%- endfor -%} {%- if github -%} {% if github.contributors | length != 0 %} {% raw %}\n{% endraw -%} ### Contributors {%- endif %}\ {% for contributor in github.contributors %} * @{{ contributor.username }} {%- endfor -%} {%- endif -%} {% if version %} {% if previous.version %} **Full Changelog**: {{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }} {% endif %} {% else -%} {% raw %}\n{% endraw %} {% endif %} {%- macro remote_url() -%} https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }} {%- endmacro -%} """ # Remove leading and trailing whitespaces from the changelog's body. trim = true # An array of regex based postprocessors to modify the changelog. # Replace the placeholder `` with a URL. postprocessors = [] [remote.github] owner = "karpetrosyan" repo = "hishel"karpetrosyan-hishel-fd309b1/docs/000077500000000000000000000000001514113157400170525ustar00rootroot00000000000000karpetrosyan-hishel-fd309b1/docs/CNAME000066400000000000000000000000131514113157400176120ustar00rootroot00000000000000hishel.com karpetrosyan-hishel-fd309b1/docs/contributing.md000066400000000000000000000114631514113157400221100ustar00rootroot00000000000000--- icon: material/hand-coin-outline --- # Contributing to Hishel Thank you for being interested in contributing to `Hishel`! We appreciate your efforts and welcome contributions of all kinds. You can contribute by: - Reviewing [pull requests](https://github.com/karpetrosyan/hishel/pulls) - [Opening an issue](https://github.com/karpetrosyan/hishel/issues/new) to report bugs or suggest features - [Adding a new feature](https://github.com/karpetrosyan/hishel/compare) - ⭐ **Starring the repository** on [GitHub](https://github.com/karpetrosyan/hishel) - it helps the project grow! This guide will help you understand the development process and repository structure. ## Getting Started ### Setting Up Your Development Environment 1. **Fork the repository**: Fork [Hishel](https://github.com/karpetrosyan/hishel/) to your GitHub account 2. **Clone and create a branch**: ```bash git clone https://github.com/username/hishel cd hishel git switch -c my-feature-name ``` 3. **Install dependencies**: This project uses `uv` for dependency management. Make sure you have it installed, then install the project dependencies: ```bash uv sync --all-extras --dev ``` ## Repository Structure ### The `scripts/` Folder The `scripts/` directory contains utility scripts to simplify development and maintenance tasks: - **`scripts/fix`** - Automatically fixes code style issues, formats code, and generates synchronous code from async code - **`scripts/lint`** - Validates code quality (linting, formatting, type checking, async/sync consistency) - **`scripts/test`** - Runs the test suite with coverage reporting - **`scripts/unasync`** - Converts async code to sync code (see below for details) ### Usage Example ```bash # Fix code style and generate sync files ./scripts/fix # Check code quality ./scripts/lint # Run tests with coverage ./scripts/test ``` ## Critical: Async/Sync Code Generation **⚠️ IMPORTANT: Do not manually edit auto-generated synchronous files!** Hishel maintains both async and sync APIs without code duplication using an **unasync** strategy similar to [httpcore](https://github.com/encode/httpcore). ### How It Works **Write async code once** - All shared async/sync functionality is written in async files: - `hishel/_core/_storages/_async_*.py` → auto-generates → `hishel/_core/_storages/_sync_*.py` - `tests/_core/_async/*.py` → auto-generates → `tests/_core/_sync/*.py` **Automatic transformation** - The `scripts/unasync` script converts async code to sync: ```python # Async code (you write this) async def store(self, key: str) -> None: async with self.connection as conn: await conn.execute(...) # Sync code (automatically generated) def store(self, key: str) -> None: with self.connection as conn: conn.execute(...) ``` ### Using the Script ```bash # Generate sync files from async files ./scripts/unasync # Check if sync files are up-to-date (CI) ./scripts/unasync --check # Or use helper scripts ./scripts/fix # Auto-generates sync files + formatting ./scripts/lint # Checks sync files are up-to-date ``` ### Development Rules ✅ **DO**: - Write and edit async files only (`_async_*.py`) - Run `./scripts/fix` before committing - Let the script generate all sync files ❌ **DON'T**: - Manually edit sync files (`_sync_*.py`) - Commit async changes without running unasync - Modify the sync test files directly ## Development Workflow ### Before Submitting a PR 1. **Make your changes** in the async versions of files 2. **Run the fix script**: ```bash ./scripts/fix ``` 3. **Run the linter**: ```bash ./scripts/lint ``` 4. **Run tests**: ```bash ./scripts/test ``` ## Releasing (Maintainers Only) This section is for maintainers who have permissions to publish new releases. ### Release Process 1. **Update the version** in `pyproject.toml`: ```toml [project] version = "1.1.6" # Update to new version ``` 2. **Generate the changelog** using `git cliff`: ```bash git cliff --output CHANGELOG.md 0.1.3.. --tag 1.1.6 ``` - Start from `0.1.3` (versions before this didn't use conventional commits) - Specify the new release tag with `--tag` 3. **Commit the changes** with an unconventional commit message: ```bash git add pyproject.toml CHANGELOG.md git commit -m "Version 1.1.6" ``` 4. **Create a git tag** for the release: ```bash git tag 1.1.6 ``` 5. **Push to GitHub** (both commits and tags): ```bash git push git push --tags ``` 6. **Ensure CI passes** - Wait for all GitHub Actions workflows to complete successfully 7. **Done!** - The release is published once CI passes ## Questions? If you have questions about contributing, feel free to: - Open an issue for discussion - Ask in an existing pull request - Check the [documentation](https://hishel.com) Thank you for contributing to Hishel! 🎉 karpetrosyan-hishel-fd309b1/docs/custom.css000066400000000000000000000001261514113157400210750ustar00rootroot00000000000000.gh-dark-mode-only { display: none; } .gh-light-mode-only { display: none; } karpetrosyan-hishel-fd309b1/docs/index.md000066400000000000000000000260201514113157400205030ustar00rootroot00000000000000--- icon: material/rocket ---

Hishel Logo Hishel Logo

Hishel

Elegant HTTP Caching for Python

PyPI version Python versions License Coverage Downloads

--- **Hishel** (հիշել, *to remember* in Armenian) is a modern HTTP caching library for Python that implements [RFC 9111](https://www.rfc-editor.org/rfc/rfc9111.html) specifications. It provides seamless caching integration for popular HTTP clients with minimal code changes. ## ✨ Features - 🎯 **RFC 9111 Compliant** - Fully compliant with the latest HTTP caching specification - 🔌 **Easy Integration** - Drop-in support for HTTPX, Requests, ASGI, FastAPI, and BlackSheep - 💾 **Flexible Storage** - SQLite backend with more coming soon - ⚡ **High Performance** - Efficient caching with minimal overhead - 🔄 **Async & Sync** - Full support for both synchronous and asynchronous workflows - 🎨 **Type Safe** - Fully typed with comprehensive type hints - 🧪 **Well Tested** - Extensive test coverage and battle-tested - 🎛️ **Configurable** - Fine-grained control over caching behavior with flexible policies - 💨 **Memory Efficient** - Streaming support prevents loading large payloads into memory - 🌐 **Universal** - Works with any ASGI application (Starlette, Litestar, BlackSheep, etc.) - 🎯 **GraphQL Support** - Cache GraphQL queries with body-sensitive content caching ## 📦 Installation ```bash pip install hishel ``` ### Optional Dependencies Install with specific integration support: ```bash pip install hishel[httpx] # For HTTPX support pip install hishel[requests] # For Requests support pip install hishel[fastapi] # For FastAPI support (includes ASGI) ``` Or install multiple: ```bash pip install hishel[httpx,requests,fastapi] ``` !!! note ASGI middleware has no extra dependencies - it's included in the base installation. ## 🚀 Quick Start ### With HTTPX **Synchronous:** ```python from hishel.httpx import SyncCacheClient client = SyncCacheClient() # First request - fetches from origin response = client.get("https://api.example.com/data") print(response.extensions["hishel_from_cache"]) # False # Second request - served from cache response = client.get("https://api.example.com/data") print(response.extensions["hishel_from_cache"]) # True ``` **Asynchronous:** ```python from hishel.httpx import AsyncCacheClient async with AsyncCacheClient() as client: # First request - fetches from origin response = await client.get("https://api.example.com/data") print(response.extensions["hishel_from_cache"]) # False # Second request - served from cache response = await client.get("https://api.example.com/data") print(response.extensions["hishel_from_cache"]) # True ``` ### With Requests ```python import requests from hishel.requests import CacheAdapter session = requests.Session() session.mount("https://", CacheAdapter()) session.mount("http://", CacheAdapter()) # First request - fetches from origin response = session.get("https://api.example.com/data") # Second request - served from cache response = session.get("https://api.example.com/data") print(response.headers.get("X-Hishel-From-Cache")) # "True" ``` ### With ASGI Applications Add caching middleware to any ASGI application: ```python from hishel.asgi import ASGICacheMiddleware # Wrap your ASGI app app = ASGICacheMiddleware(app) # Or configure with options from hishel import AsyncSqliteStorage, CacheOptions, SpecificationPolicy app = ASGICacheMiddleware( app, storage=AsyncSqliteStorage(), policy=SpecificationPolicy( cache_options=CacheOptions(shared=True) ) ) ``` ### With FastAPI Add Cache-Control headers using the `cache()` dependency: ```python from fastapi import FastAPI from hishel.fastapi import cache app = FastAPI() @app.get("/api/data", dependencies=[cache(max_age=300, public=True)]) async def get_data(): # Cache-Control: public, max-age=300 return {"data": "cached for 5 minutes"} # Optionally wrap with ASGI middleware for local caching according to specified rules from hishel.asgi import ASGICacheMiddleware from hishel import AsyncSqliteStorage app = ASGICacheMiddleware(app, storage=AsyncSqliteStorage()) ``` ### With BlackSheep Use BlackSheep's native `cache_control` decorator with Hishel's ASGI middleware: ```python from blacksheep import Application, get from blacksheep.server.headers.cache import cache_control app = Application() @get("/api/data") @cache_control(max_age=300, public=True) async def get_data(): # Cache-Control: public, max-age=300 return {"data": "cached for 5 minutes"} ``` ## 🎛️ Advanced Configuration ### Caching Policies Hishel supports two types of caching policies for flexible caching strategies: #### SpecificationPolicy (RFC 9111 Compliant) The default policy that follows HTTP caching standards: ```python from hishel import CacheOptions, SpecificationPolicy from hishel.httpx import SyncCacheClient client = SyncCacheClient( policy=SpecificationPolicy( cache_options=CacheOptions( shared=False, # Use as private cache (browser-like) supported_methods=["GET", "HEAD", "POST"], # Cache GET, HEAD, and POST allow_stale=True # Allow serving stale responses ) ) ) ``` #### FilterPolicy (Custom Filtering) Apply custom logic to determine what gets cached: ```python from hishel import FilterPolicy, BaseFilter, Request from hishel.httpx import AsyncCacheClient class CacheOnlyAPIRequests(BaseFilter[Request]): def needs_body(self) -> bool: return False def apply(self, item: Request, body: bytes | None) -> bool: # Only cache requests to /api/ endpoints return "/api/" in str(item.url) client = AsyncCacheClient( policy=FilterPolicy( request_filters=[CacheOnlyAPIRequests()] ) ) ``` !!! tip "Learn More" See the [Policies Guide](policies.md) for detailed examples including GraphQL caching, body inspection, and combining multiple filters. ### Custom Storage Backend ```python from hishel import SyncSqliteStorage from hishel.httpx import SyncCacheClient storage = SyncSqliteStorage( database_path="my_cache.db", default_ttl=7200.0, # Cache entries expire after 2 hours refresh_ttl_on_access=True # Reset TTL when accessing cached entries ) client = SyncCacheClient(storage=storage) ``` ### GraphQL and Body-Sensitive Caching Cache GraphQL queries and other POST requests by including the request body in the cache key. **Using per-request header:** ```python from hishel.httpx import SyncCacheClient client = SyncCacheClient() # Cache GraphQL queries - different queries get different cache entries graphql_query = """ query GetUser($id: ID!) { user(id: $id) { name email } } """ response = client.post( "https://api.example.com/graphql", json={"query": graphql_query, "variables": {"id": "123"}}, headers={"X-Hishel-Body-Key": "true"} # Enable body-based caching ) # Different query will be cached separately response = client.post( "https://api.example.com/graphql", json={"query": graphql_query, "variables": {"id": "456"}}, headers={"X-Hishel-Body-Key": "true"} ) ``` **Using global configuration:** ```python from hishel.httpx import SyncCacheClient from hishel import FilterPolicy # Enable body-based caching for all requests client = SyncCacheClient(policy=FilterPolicy(use_body_key=True)) # All POST requests automatically include body in cache key response = client.post( "https://api.example.com/graphql", json={"query": graphql_query, "variables": {"id": "123"}} ) ``` ## 🏗️ Architecture Hishel uses a **sans-I/O state machine** architecture that separates HTTP caching logic from I/O operations: - ✅ **Correct** - Fully RFC 9111 compliant - ✅ **Testable** - Easy to test without network dependencies - ✅ **Flexible** - Works with any HTTP client or server - ✅ **Type Safe** - Clear state transitions with full type hints ## 🔮 Roadmap We're actively working on: - 🎯 Performance optimizations - 🎯 More integrations - 🎯 Partial responses support ## 📚 Documentation Comprehensive documentation is available at [https://hishel.com/dev](https://hishel.com/dev) - [Getting Started](https://hishel.com) - [HTTPX Integration](https://hishel.com/dev/integrations/httpx) - [Requests Integration](https://hishel.com/dev/integrations/requests) - [ASGI Integration](https://hishel.com/dev/asgi) - [FastAPI Integration](https://hishel.com/dev/fastapi) - [BlackSheep Integration](https://hishel.com/dev/integrations/blacksheep) - [GraphQL Integration](https://hishel.com/dev/integrations/graphql) - [Storage Backends](https://hishel.com/dev/storages) - [Request/Response Metadata](https://hishel.com/dev/metadata) - [RFC 9111 Specification](https://hishel.com/dev/specification) ## 🤝 Contributing Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change. See our [Contributing Guide](https://hishel.com/dev/contributing) for more details. ## 📄 License This project is licensed under the BSD-3-Clause License - see the [LICENSE](https://raw.githubusercontent.com/karpetrosyan/hishel/master/LICENSE) file for details. ## 💖 Support If you find Hishel useful, please consider: - ⭐ Starring the repository - 🐛 Reporting bugs and issues - 💡 Suggesting new features - 📖 Improving documentation - ☕ [Buying me a coffee](https://buymeacoffee.com/karpetrosyan) ## 🙏 Acknowledgments Hishel is inspired by and builds upon the excellent work in the Python HTTP ecosystem, particularly: - [HTTPX](https://github.com/encode/httpx) - A next-generation HTTP client for Python - [Requests](https://github.com/psf/requests) - The classic HTTP library for Python - [RFC 9111](https://www.rfc-editor.org/rfc/rfc9111.html) - HTTP Caching specification ---

Made with ❤️ by Kar Petrosyan

karpetrosyan-hishel-fd309b1/docs/integrations/000077500000000000000000000000001514113157400215605ustar00rootroot00000000000000karpetrosyan-hishel-fd309b1/docs/integrations/asgi.md000066400000000000000000000046201514113157400230270ustar00rootroot00000000000000--- icon: material/web --- # ASGI Integration Hishel provides ASGI middleware for caching HTTP responses in any ASGI-compatible application (FastAPI, Starlette, Django ASGI, etc.). The middleware intercepts requests and responses, caching them according to HTTP caching specifications (RFC 9111) or custom rules. --- ## Installation ```bash pip install hishel ``` No extra dependencies required - ASGI support is included by default. --- ## Quick Start Wrap your ASGI application with `ASGICacheMiddleware`: ```python from hishel.asgi import ASGICacheMiddleware # Your ASGI application async def app(scope, receive, send): if scope["type"] == "http": await send({ "type": "http.response.start", "status": 200, "headers": [[b"cache-control", b"max-age=3600"]], }) await send({ "type": "http.response.body", "body": b"Hello, World!", }) # Wrap with caching middleware cached_app = ASGICacheMiddleware(app) ``` --- ## Basic Usage # HTTP Caching with Hishel Hishel provides elegant HTTP caching middleware for ASGI applications. ## Installation ```bash pip install hishel fastapi aiohttp litestar blacksheep uvicorn ``` ## Examples === "FastAPI" ```python # fastapi_example.py from fastapi import FastAPI from hishel import ASGICacheMiddleware app = FastAPI() @app.get("/") async def index(): return {"message": "Hello, world!"} app = ASGICacheMiddleware(app) # Run: uvicorn fastapi_example:app --reload ``` === "Litestar" ```python # litestar_example.py from litestar import Litestar, get from hishel import ASGICacheMiddleware @get("/") async def index() -> str: return "Hello, world!" app = ASGICacheMiddleware(Litestar([index])) # Run: uvicorn litestar_example:app --reload ``` === "BlackSheep" ```python # blacksheep_example.py from blacksheep import Application, get from hishel import ASGICacheMiddleware app = Application() @get("/") async def index(): return "Hello, world!" app = ASGICacheMiddleware(app) # Run: uvicorn blacksheep_example:app --reload ``` ## See Also - [Storage Backends](../storages.md) - Configure cache storage - [Request/Response Metadata](../metadata.md) - Control caching behavior - [FastAPI Integration](fastapi.md) - FastAPI-specific helpers karpetrosyan-hishel-fd309b1/docs/integrations/blacksheep.md000066400000000000000000000100721514113157400242030ustar00rootroot00000000000000--- icon: material/sheep --- # BlackSheep Integration Hishel provides seamless integration with [BlackSheep](https://www.neoteroi.dev/blacksheep/) through ASGI middleware. You can use BlackSheep's built-in `cache_control` decorator to set Cache-Control headers, and combine it with Hishel's ASGI middleware for local server-side caching. !!! tip "Two approaches" - Use BlackSheep's `@cache_control()` decorator to send Cache-Control headers to clients (browsers, CDNs, proxies) - Add Hishel's ASGI middleware to also cache responses locally based on those Cache-Control headers --- ## Installation ```bash pip install hishel blacksheep ``` --- ## Quick Start ### Cache-Control Headers Only Use BlackSheep's built-in `cache_control` decorator: ```python from blacksheep import Application, get from blacksheep.server.headers.cache import cache_control app = Application() @get("/api/data") @cache_control(max_age=300, public=True) async def get_data(): # Cache-Control: public, max-age=300 return {"data": "Clients will cache this for 5 minutes"} ``` ### With Local Server Caching Combine BlackSheep's `cache_control` decorator with Hishel's ASGI middleware: ```python from blacksheep import Application, get from blacksheep.server.headers.cache import cache_control from hishel.asgi import ASGICacheMiddleware from hishel import AsyncSqliteStorage app = Application() @get("/api/data") @cache_control(max_age=300, public=True) async def get_data(): # Cached locally AND by clients/CDNs return {"data": "Expensive operation result"} # Wrap with Hishel's ASGI middleware for local caching app = ASGICacheMiddleware( app, storage=AsyncSqliteStorage(), ) ``` --- ## Common Examples ### Static Assets Cache static files for a long time: ```python @get("/static/logo.png") @cache_control(max_age=31536000, public=True, immutable=True) async def get_logo(): return {"file": "logo.png"} ``` ### Public API Data Cache public API responses: ```python @get("/api/articles") @cache_control(max_age=300, public=True) async def get_articles(): return {"articles": ["Article 1", "Article 2"]} ``` ### Private User Data Cache user-specific data (browsers only): ```python @get("/api/user/profile") @cache_control(max_age=300, private=True) async def get_profile(): return {"user": "john_doe"} ``` ### CDN Optimization Different cache times for browsers vs CDNs: ```python @get("/api/data") @cache_control(max_age=60, s_maxage=3600, public=True) async def get_data(): # Browsers: 1 minute, CDN: 1 hour return {"data": "..."} ``` ### No Caching Prevent caching of sensitive data: ```python @get("/api/secrets") @cache_control(no_cache=True, no_store=True) async def get_secrets(): # Cache-Control: no-cache, no-store return {"secret": "This response should not be cached or stored!"} ``` --- ## How It Works 1. **BlackSheep's `@cache_control()` decorator** adds Cache-Control headers to responses 2. **Hishel's ASGI middleware** reads those headers and caches responses locally according to RFC 9111 3. **Subsequent requests** are served from the local cache when valid, or forwarded to your handlers when cache is stale/missing This gives you the best of both worlds: - ✅ Client-side caching (browsers, CDNs) via Cache-Control headers - ✅ Server-side caching to reduce load on your application - ✅ RFC 9111 compliant caching behavior - ✅ Simple, declarative API using BlackSheep's native decorators --- ## Notes !!! tip "Use BlackSheep's native decorator" BlackSheep has excellent built-in cache control support. Use `@cache_control()` from BlackSheep rather than creating custom headers. !!! warning "no_store is strongest" `no_store` prevents all caching regardless of other directives. --- ## See Also - [ASGI Integration](asgi.md) - General ASGI middleware documentation - [FastAPI Integration](fastapi.md) - Similar integration with FastAPI - [Storage Backends](../storages.md) - Configure cache storage - [BlackSheep Documentation](https://www.neoteroi.dev/blacksheep/) - [RFC 9111: HTTP Caching](https://www.rfc-editor.org/rfc/rfc9111.html) karpetrosyan-hishel-fd309b1/docs/integrations/custom.md000066400000000000000000000321401514113157400234140ustar00rootroot00000000000000--- icon: material/power-plug-battery --- # Custom Integrations Hishel is designed to be flexible and easy to integrate with any HTTP client or server. This guide will help you build custom integrations for libraries that aren't yet supported out of the box. ## Converting Request/Response Models The core of any Hishel integration is converting your library's request/response models to Hishel's internal `Request` and `Response` models. This translation layer allows Hishel to cache responses regardless of which HTTP library you're using. In the Hishel codebase, you'll find conversion methods like `httpx_to_internal`, `requests_to_internal`, etc. that handle popular libraries. You can use these as reference implementations when building your own integration. ### Core Conversion Principles Follow these guidelines when converting models: **Response Content** : The response content should be the actual data, possibly compressed, but decoded if it was sent with transfer encoding (like chunked). This content must be reusable—if you store compressed data, also preserve the `Content-Encoding` header so it can be decoded later. **Headers** : Store headers as-is, except for headers that the HTTP specification doesn't allow caching. Important: If a response stream has already been consumed and decoded into memory before reaching the cache layer, you must remove the `Content-Encoding` header since the content is no longer encoded. **Requests** : Converting request models is simpler than responses. Hishel doesn't recreate requests from cache or store request body streams—only headers, method, and URL are needed. The converted request may be modified by Hishel before being sent to the server, but exact preservation of the request body isn't critical. ## Using Cache Proxy Classes Hishel provides `AsyncCacheProxy` and `SyncCacheProxy` helper classes that handle all the caching logic for you. These classes are independent of any specific HTTP library and work only with Hishel's internal models, making them perfect for building new integrations. ### AsyncCacheProxy The `AsyncCacheProxy` class manages the entire HTTP caching state machine. You simply provide it with a function that sends requests, and it handles: - Cache key generation - Storage operations - RFC 9111 compliance - State machine transitions - TTL management **Basic Usage:** ```python from hishel import AsyncCacheProxy, AsyncSqliteStorage, CacheOptions, SpecificationPolicy async def send_request(request: Request) -> Response: # Your code to send the HTTP request # This is where you convert from internal models to your library # and back pass # Create the cache proxy cache_proxy = AsyncCacheProxy( request_sender=send_request, storage=AsyncSqliteStorage(), # Optional, defaults to AsyncSqliteStorage policy=SpecificationPolicy(), # Optional, defaults to SpecificationPolicy() ) # Handle a request with caching response = await cache_proxy.handle_request(request) ``` **Key Features:** - **Automatic cache key generation**: Based on URL and optional request body hashing - **Spec-compliant caching**: Full RFC 9111 state machine handling - **Spec-ignoring mode**: Simple cache lookup without RFC 9111 rules - **TTL refresh**: Automatic TTL updates on cache access if configured - **Vary header support**: Proper handling of content negotiation ### SyncCacheProxy The synchronous version works identically but for blocking I/O: ```python from hishel import SyncCacheProxy, SyncSqliteStorage, SpecificationPolicy def send_request(request: Request) -> Response: # Your synchronous request sending code pass cache_proxy = SyncCacheProxy( request_sender=send_request, storage=SyncSqliteStorage(), policy=SpecificationPolicy(), ) response = cache_proxy.handle_request(request) ``` ### Integration Example: httpx Here's how the httpx integration uses `AsyncCacheProxy`: ```python from hishel import AsyncCacheProxy, Request, Response import httpx class AsyncCacheTransport(httpx.AsyncBaseTransport): def __init__( self, next_transport: httpx.AsyncBaseTransport, storage: AsyncBaseStorage | None = None, cache_options: CacheOptions | None = None, ): self._transport = next_transport # Define how to send a request using the underlying transport async def send_request(internal_request: Request) -> Response: # Convert internal Request to httpx.Request httpx_request = internal_to_httpx(internal_request) # Send using underlying transport httpx_response = await self._transport.handle_async_request(httpx_request) # Convert httpx.Response to internal Response return httpx_to_internal(httpx_response) # Create the cache proxy with our send function self._cache_proxy = AsyncCacheProxy( request_sender=send_request, storage=storage, cache_options=cache_options, ) async def handle_async_request(self, request: httpx.Request) -> httpx.Response: # Convert httpx.Request to internal Request internal_request = httpx_to_internal(request) # Let the cache proxy handle the request internal_response = await self._cache_proxy.handle_request(internal_request) # Convert internal Response back to httpx.Response return internal_to_httpx(internal_response) ``` ### When to Use Cache Proxy Classes **Use `AsyncCacheProxy`/`SyncCacheProxy` when:** - ✅ Building a new integration from scratch - ✅ You want automatic RFC 9111 compliance - ✅ You need both spec-respecting and spec-ignoring modes - ✅ You want to focus on model conversion, not caching logic ### Configuration Options Both proxy classes accept these parameters: | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `request_sender` | `Callable` | **Required** | Function that sends requests using your HTTP library | | `storage` | `AsyncBaseStorage` / `SyncBaseStorage` | `AsyncSqliteStorage()` / `SyncSqliteStorage()` | Where to store cached responses | | `policy` | `CachePolicy` | `SpecificationPolicy()` | Caching policy to use | ## Implementation Example Here's how to translate synchronous httpx Request/Response models to Hishel's internal models: ```python import httpx from typing import Iterator, Union, cast, overload from hishel.models import Request, Response, RequestMetadata, Headers from hishel.utils import filter_mapping, make_sync_iterator # 128 KB CHUNK_SIZE = 131072 @overload def httpx_to_internal(value: httpx.Request) -> Request: ... @overload def httpx_to_internal(value: httpx.Response) -> Response: ... def httpx_to_internal( value: Union[httpx.Request, httpx.Response], ) -> Union[Request, Response]: """ Convert httpx.Request/httpx.Response to internal Request/Response. """ headers = Headers( filter_mapping( Headers({key: value for key, value in value.headers.items()}), ["Transfer-Encoding"], ) ) if isinstance(value, httpx.Request): extension_metadata = RequestMetadata( hishel_refresh_ttl_on_access=value.extensions.get("hishel_refresh_ttl_on_access"), hishel_ttl=value.extensions.get("hishel_ttl"), hishel_body_key=value.extensions.get("hishel_body_key"), ) headers_metadata = extract_metadata_from_headers(value.headers) for key, val in extension_metadata.items(): if key in value.extensions: headers_metadata[key] = val return Request( method=value.method, url=str(value.url), headers=headers, stream=cast(Iterator[bytes], value.stream), metadata=headers_metadata, ) elif isinstance(value, httpx.Response): stream = ( make_sync_iterator([value.content]) if value.is_stream_consumed else value.iter_raw(chunk_size=CHUNK_SIZE) ) return Response( status_code=value.status_code, headers=headers, stream=stream, metadata={}, ) ``` !!! warning "Critical: Stream Must Be Available" If a stream was consumed without being read into memory, there's no way to access the data. Hishel will raise an error in this case to prevent silent data loss. ## Common Pitfalls and Solutions ### Stream Consumption Always ensure consumed streams are stored in memory before conversion. Check your library's documentation for methods like `is_stream_consumed` or `content` that indicate whether data is still available. **Best Practice**: Read the stream into memory before creating the Hishel Response: ```python # Good: Stream is preserved if response.is_stream_consumed: stream = make_iterator([response.content]) else: stream = response.iter_raw() # Bad: Stream was consumed elsewhere without storing # This will fail when Hishel tries to cache ``` ### Header Filtering !!! tip "Which Headers to Remove" Remove these headers when caching responses: **Hop-by-hop headers** (never cached): - `Connection` - `Keep-Alive` - `Proxy-Authenticate` - `Proxy-Authorization` - `TE` - `Trailers` - `Transfer-Encoding` - `Upgrade` **Encoding headers** (remove only if content is decoded): - `Content-Encoding` - Remove when you've decoded the content ## Testing Your Integration When implementing a custom integration, test these scenarios: 1. **Basic caching flow** - Request → Response → Cache → Retrieve 2. **Compressed responses** - gzip, deflate, brotli 3. **Chunked transfer encoding** - Verify proper handling 4. **Stream states** - Both consumed and unconsumed streams 5. **HTTP status codes** - 200, 304, 404, 500, etc. 6. **Content types** - JSON, HTML, binary data, large files 7. **Request metadata** - Custom Hishel extensions and TTL settings ### Example Test Case ```python def test_basic_caching(): # Create a request request = mylib.Request("GET", "https://example.com") # Convert to internal model internal_request = mylib_to_internal(request) # Verify conversion assert internal_request.method == "GET" assert internal_request.url == "https://example.com" assert "Transfer-Encoding" not in internal_request.headers ``` ## Integration Template Here's a template to get started with integrating a new library: ```python from typing import Union, overload from hishel.models import Request, Response, Headers, RequestMetadata @overload def mylib_to_internal(value: MyLibRequest) -> Request: ... @overload def mylib_to_internal(value: MyLibResponse) -> Response: ... def mylib_to_internal( value: Union[MyLibRequest, MyLibResponse], ) -> Union[Request, Response]: """Convert MyLib models to Hishel internal models.""" if isinstance(value, MyLibRequest): # Extract method, URL, and headers method = value.method url = str(value.url) headers = Headers({k: v for k, v in value.headers.items()}) # Create request stream if body exists stream = value.stream if hasattr(value, 'stream') else iter([]) # Extract Hishel metadata from extensions/extras if available metadata = {} if hasattr(value, 'extensions'): metadata = { 'hishel_ttl': value.extensions.get('hishel_ttl'), # Add other metadata as needed } return Request( method=method, url=url, headers=headers, stream=stream, metadata=metadata, ) elif isinstance(value, MyLibResponse): # Extract status code status_code = value.status_code # Filter headers (remove hop-by-hop and encoding headers if needed) headers = Headers({k: v for k, v in value.headers.items()}) # Remove Transfer-Encoding headers = filter_headers(headers, ["Transfer-Encoding"]) # Handle stream consumption state if value.is_consumed: # Stream was consumed, use stored content stream = make_iterator([value.content]) # Remove Content-Encoding if content was decoded headers = filter_headers(headers, ["Content-Encoding"]) else: # Stream still available stream = value.iter_content(chunk_size=131072) return Response( status_code=status_code, headers=headers, stream=stream, metadata={}, ) ``` ## Need Help? If you're building an integration and encounter issues: 1. **Check existing integrations** - Look at httpx, requests, and aiohttp implementations in the Hishel codebase 2. **Open an issue** - Post your use case on [GitHub Issues](https://github.com/karpetrosyan/hishel/issues) 3. **Contribute back** - Consider contributing your integration to help others! ## Related Documentation - [ASGI Integration](asgi.md) - Full ASGI middleware for caching - [HTTPX Integration](httpx.md) - Async HTTP client with caching - [Requests Integration](requests.md) - Synchronous HTTP client with cachingkarpetrosyan-hishel-fd309b1/docs/integrations/fastapi.md000066400000000000000000000072561514113157400235430ustar00rootroot00000000000000--- icon: simple/fastapi --- # FastAPI Integration Hishel provides FastAPI integration in two ways: 1. **Cache-Control Headers Only** - Use the `cache()` dependency to send proper Cache-Control headers to clients (browsers, CDNs, proxies) 2. **Full Caching** - Combine with ASGI middleware to also cache responses locally based on the Cache-Control rules you specify !!! tip "Choose your approach" - Use `cache()` dependency alone to let clients/CDNs cache responses - Add ASGI middleware on top to also cache locally on your server - Both approaches use the same Cache-Control headers for consistency --- ## Installation ```bash pip install hishel[fastapi] ``` Or if you already have FastAPI installed: ```bash pip install hishel ``` --- ## Quick Start ### Cache-Control Headers Only Use the `cache()` dependency to add Cache-Control headers: ```python from fastapi import FastAPI from hishel.fastapi import cache app = FastAPI() @app.get("/api/data", dependencies=[cache(max_age=300, public=True)]) async def get_data(): # Cache-Control: public, max-age=300 return {"data": "Clients will cache this for 5 minutes"} ``` ### With Local Server Caching Combine with ASGI middleware to also cache locally: ```python from fastapi import FastAPI from hishel.fastapi import cache from hishel.asgi import ASGICacheMiddleware from hishel import AsyncSqliteStorage app = FastAPI() @app.get("/api/data", dependencies=[cache(max_age=300, public=True)]) async def get_data(): # Cached locally AND by clients/CDNs return {"data": "Expensive operation result"} # Wrap with caching middleware to enable local caching app = ASGICacheMiddleware( app, storage=AsyncSqliteStorage(), ) ``` --- ## Common Examples ### Static Assets Cache static files for a long time: ```python @app.get("/static/logo.png", dependencies=[cache(max_age=31536000, public=True, immutable=True)]) async def get_logo(): return {"file": "logo.png"} ``` ### Public API Data Cache public API responses: ```python @app.get("/api/articles", dependencies=[cache(max_age=300, public=True)]) async def get_articles(): return {"articles": [...]} ``` ### Private User Data Cache user-specific data (browsers only): ```python @app.get("/api/user/profile", dependencies=[cache(max_age=300, private=True)]) async def get_profile(): return {"user": "john_doe"} ``` ### CDN Optimization Different cache times for browsers vs CDNs: ```python @app.get("/api/data", dependencies=[cache( max_age=60, # Browsers: 1 minute s_maxage=3600, # CDN: 1 hour public=True )]) async def get_data(): return {"data": "..."} ``` ### No Caching Prevent caching of sensitive data: ```python @app.get("/api/secrets", dependencies=[cache(no_store=True)]) async def get_secrets(): return {"secret": "value"} ``` --- ## Notes !!! tip "Combine directives wisely" Some directives conflict (e.g., `public` and `private`). Choose combinations that match your caching strategy. !!! warning "no_store is strongest" `no_store` prevents all caching regardless of other directives. !!! info "Field names for fine-grained control" `private` and `no_cache` accept lists of header names for precise control over which parts of the response require special handling. --- ## See Also - [ASGI Integration](asgi.md) - Full ASGI middleware for caching - [Request/Response Metadata](../metadata.md) - Control caching behavior - [Storage Backends](../storages.md) - Configure cache storage - [RFC 9111: HTTP Caching](https://www.rfc-editor.org/rfc/rfc9111.html) - [RFC 8246: Immutable Responses](https://www.rfc-editor.org/rfc/rfc8246.html) - [RFC 5861: Cache-Control Extensions](https://www.rfc-editor.org/rfc/rfc5861.html) karpetrosyan-hishel-fd309b1/docs/integrations/graphql.md000066400000000000000000000356331514113157400235520ustar00rootroot00000000000000--- icon: material/graphql --- # GraphQL Integration Hishel provides robust support for caching GraphQL queries through body-sensitive content caching. Since GraphQL typically uses POST requests with different query bodies to the same endpoint, standard URL-based caching won't work. Hishel solves this by including the request body in the cache key. ## Why Body-Sensitive Caching? Traditional HTTP caching uses the URL as the cache key. However, GraphQL APIs typically: - Use a single endpoint (e.g., `/graphql`) - Send queries via POST requests with JSON bodies - Have different queries/mutations that need separate cache entries Hishel's body-sensitive caching creates unique cache keys based on the request body, allowing proper caching of GraphQL queries. ## Quick Start ### Per-Request Configuration Enable body-based caching for specific GraphQL requests using the `X-Hishel-Body-Key` header: ```python from hishel.httpx import SyncCacheClient client = SyncCacheClient() query = """ query GetUser($userId: ID!) { user(id: $userId) { id name email avatar } } """ # First request - fetches from server response = client.post( "https://api.example.com/graphql", json={ "query": query, "variables": {"userId": "123"} }, headers={"X-Hishel-Body-Key": "true"} ) # Second request - served from cache response = client.post( "https://api.example.com/graphql", json={ "query": query, "variables": {"userId": "123"} }, headers={"X-Hishel-Body-Key": "true"} ) # Different variables - creates new cache entry response = client.post( "https://api.example.com/graphql", json={ "query": query, "variables": {"userId": "456"} }, headers={"X-Hishel-Body-Key": "true"} ) ``` ### Global Configuration Enable body-based caching for all requests using FilterPolicy: ```python from hishel.httpx import SyncCacheClient from hishel import FilterPolicy # All requests will use body in cache key through FilterPolicy client = SyncCacheClient(policy=FilterPolicy(use_body_key=True)) query = """ query GetPosts($limit: Int!) { posts(limit: $limit) { id title content } } """ # No need to set headers - body caching is automatic response = client.post( "https://api.example.com/graphql", json={ "query": query, "variables": {"limit": 10} } ) ``` ## Async Support Hishel fully supports async GraphQL clients: ```python from hishel.httpx import AsyncCacheClient from hishel import FilterPolicy async def fetch_user_data(): async with AsyncCacheClient(policy=FilterPolicy(use_body_key=True)) as client: query = """ query GetUser($id: ID!) { user(id: $id) { name email posts { title createdAt } } } """ response = await client.post( "https://api.example.com/graphql", json={ "query": query, "variables": {"id": "user-123"} } ) return response.json() ``` ## Working with GraphQL Libraries ### GQL (gql) Library The [gql](https://github.com/graphql-python/gql) library is a GraphQL client for Python that provides advanced features like query validation, automatic retries, and more. Hishel integrates seamlessly with gql through HTTPX transports. #### Why Use Hishel with GQL? - **Automatic Query Caching**: Cache identical GraphQL queries without manual implementation - **Network Efficiency**: Reduce API calls and improve response times - **Cost Savings**: Fewer requests to rate-limited or paid GraphQL APIs - **Offline Support**: Serve cached responses when network is unavailable #### Basic Integration You can integrate Hishel with gql in two ways: **Method 1: Using Cached HTTPX Client (Recommended)** === "Async" ```python import asyncio from gql import gql, Client from gql.transport.httpx import HTTPXAsyncTransport from hishel.httpx import AsyncCacheClient from hishel import FilterPolicy async def main(): # Create a cached HTTPX client httpx_client = AsyncCacheClient(policy=FilterPolicy(use_body_key=True)) # Use it as transport for GQL transport = HTTPXAsyncTransport( url="https://api.example.com/graphql", client=httpx_client ) async with Client( transport=transport, fetch_schema_from_transport=True ) as client: # Execute queries - automatic caching query = gql(""" query GetCountries { countries { code name capital } } """) result = await client.execute(query) print(result) asyncio.run(main()) ``` === "Sync" ```python from gql import gql, Client from gql.transport.httpx import HTTPXTransport from hishel.httpx import SyncCacheClient from hishel import FilterPolicy # Create a cached HTTPX client httpx_client = SyncCacheClient(policy=FilterPolicy(use_body_key=True)) # Use it as transport for GQL transport = HTTPXTransport( url="https://api.example.com/graphql", client=httpx_client ) client = Client(transport=transport, fetch_schema_from_transport=True) # Execute queries - automatic caching query = gql(""" query GetCountries { countries { code name capital } } """) result = client.execute(query) print(result) ``` **Method 2: Using CacheTransport (More Control)** This approach gives you fine-grained control over the transport layer: === "Async" ```python import asyncio from gql import gql, Client from gql.transport.httpx import HTTPXAsyncTransport from httpx import AsyncHTTPTransport from hishel.httpx import AsyncCacheTransport from hishel import FilterPolicy async def main(): # Create a caching transport transport = HTTPXAsyncTransport( url="https://countries.trevorblades.com/graphql", transport=AsyncCacheTransport( next_transport=AsyncHTTPTransport(), policy=FilterPolicy(), # Customize caching policy as needed ), ) # Create GQL client with caching transport async with Client( transport=transport, fetch_schema_from_transport=True, ) as session: # Execute query query = gql(""" query getContinents { continents { code name } } """) # First execution - fetches from server result = await session.execute(query) print("First request:", result) # Second execution - served from cache result = await session.execute(query) print("Second request (cached):", result) asyncio.run(main()) ``` === "Sync" ```python from gql import gql, Client from gql.transport.httpx import HTTPXTransport from httpx import HTTPTransport from hishel.httpx import SyncCacheTransport from hishel import FilterPolicy # Create a caching transport transport = HTTPXTransport( url="https://countries.trevorblades.com/graphql", transport=SyncCacheTransport( next_transport=HTTPTransport(), policy=FilterPolicy(use_body_key=True) # Customize caching policy as needed ), ) # Create GQL client with caching transport with Client( transport=transport, fetch_schema_from_transport=True, ) as session: # Execute query query = gql(""" query getContinents { continents { code name } } """) # First execution - fetches from server result = session.execute(query) print("First request:", result) # Second execution - served from cache result = session.execute(query) print("Second request (cached):", result) ``` #### Real-World Example: GitHub GraphQL API Here's a complete example querying the GitHub GraphQL API with caching: === "Async" ```python import asyncio from gql import gql, Client from gql.transport.httpx import HTTPXAsyncTransport from hishel.httpx import AsyncCacheClient from hishel import AsyncSqliteStorage, FilterPolicy async def fetch_github_repos(username: str, token: str): # Create cached client with persistent storage client = AsyncCacheClient( policy=FilterPolicy(use_body_key=True), storage=AsyncSqliteStorage( database_path="github_cache.db", default_ttl=3600.0 # Cache for 1 hour ) ) transport = HTTPXAsyncTransport( url="https://api.github.com/graphql", headers={"Authorization": f"Bearer {token}"}, client=client ) async with Client( transport=transport, fetch_schema_from_transport=False, # GitHub doesn't support introspection ) as session: query = gql(""" query GetUserRepos($username: String!) { user(login: $username) { repositories(first: 10, orderBy: {field: UPDATED_AT, direction: DESC}) { nodes { name description stargazerCount url } } } } """) result = await session.execute( query, variable_values={"username": username} ) return result # Usage asyncio.run(fetch_github_repos("karpetrosyan", "your_token_here")) ``` === "Sync" ```python from gql import gql, Client from gql.transport.httpx import HTTPXTransport from hishel.httpx import SyncCacheClient from hishel import SyncSqliteStorage, FilterPolicy def fetch_github_repos(username: str, token: str): # Create cached client with persistent storage client = SyncCacheClient( policy=FilterPolicy(use_body_key=True), storage=SyncSqliteStorage( database_path="github_cache.db", default_ttl=3600.0 # Cache for 1 hour ) ) transport = HTTPXTransport( url="https://api.github.com/graphql", headers={"Authorization": f"Bearer {token}"}, client=client ) with Client( transport=transport, fetch_schema_from_transport=False, # GitHub doesn't support introspection ) as session: query = gql(""" query GetUserRepos($username: String!) { user(login: $username) { repositories(first: 10, orderBy: {field: UPDATED_AT, direction: DESC}) { nodes { name description stargazerCount url } } } } """) result = session.execute( query, variable_values={"username": username} ) return result # Usage fetch_github_repos("karpetrosyan", "your_token_here") ``` ## Advanced: Custom GraphQL Filters For fine-grained control over GraphQL caching, you can create custom filters that inspect query bodies and response content. This is useful when you want to: - Cache only queries (not mutations) - Skip caching queries with errors ### Example: Cache Only Successful Queries ```python import json from hishel import FilterPolicy, BaseFilter, Request, Response from hishel.httpx import AsyncCacheClient class GraphQLQueryFilter(BaseFilter[Request]): """Only cache GraphQL queries (not mutations).""" def needs_body(self) -> bool: return True def apply(self, item: Request, body: bytes | None) -> bool: if body is None: return False try: data = json.loads(body) query = data.get("query", "") # Cache only if it's a query, not a mutation return "mutation" not in query.lower() except json.JSONDecodeError: return False class GraphQLSuccessFilter(BaseFilter[Response]): """Only cache successful GraphQL responses (no errors).""" def needs_body(self) -> bool: return True def apply(self, item: Response, body: bytes | None) -> bool: if item.status_code != 200 or body is None: return False try: data = json.loads(body) # Cache only if there are no GraphQL errors return "errors" not in data except json.JSONDecodeError: return False # Create the policy with custom filters policy = FilterPolicy( request_filters=[GraphQLQueryFilter()], response_filters=[GraphQLSuccessFilter()], use_body_key=True, # Enable body-based cache keys ) # Use with HTTPX async with AsyncCacheClient(policy=policy) as client: # This query will be cached (successful query) response = await client.post( "https://api.example.com/graphql", json={ "query": "{ user(id: 1) { name email } }" } ) # This mutation will NOT be cached (contains 'mutation') response = await client.post( "https://api.example.com/graphql", json={ "query": "mutation { updateUser(id: 1, name: \"John\") { id } }" } ) ``` This approach gives you complete control over what gets cached based on both request and response content. !!! tip "Learn More About Filters" For more examples of custom filters and detailed documentation, see the [Policies Guide](../policies.md). ## Best Practices 1. **Use `FilterPolicy(use_body_key=True)`** for GraphQL clients to enable body-based caching 2. **Don't cache mutations** - Use `Cache-Control: no-store` or disable caching for mutations 3. **Set appropriate TTLs** - GraphQL responses may vary in freshness requirements 4. **Monitor cache hit rates** - Check `hishel_from_cache` in response extensions 5. **Consider query complexity** - More complex queries benefit more from caching ## See Also - [Request/Response Metadata](../metadata.md) - [Storage Backends](../storages.md) - [HTTPX Integration](httpx.md) - [ASGI Integration](asgi.md) karpetrosyan-hishel-fd309b1/docs/integrations/httpx.md000066400000000000000000000101161514113157400232500ustar00rootroot00000000000000--- icon: material/butterfly --- # HTTPX Integration Hishel provides seamless integration with [HTTPX](https://www.python-httpx.org/), adding RFC 9111-compliant HTTP caching to your HTTPX applications with minimal code changes. ## Quick Start The easiest way to add caching to your HTTPX application is using the cache-enabled client classes: === "Sync" ```python from hishel.httpx import SyncCacheClient client = SyncCacheClient() # First request - fetches from origin response = client.get("https://api.example.com/data") print(response.extensions["hishel_from_cache"]) # False # Second request - served from cache response = client.get("https://api.example.com/data") print(response.extensions["hishel_from_cache"]) # True ``` === "Async" ```python from hishel.httpx import AsyncCacheClient async with AsyncCacheClient() as client: # First request - fetches from origin response = await client.get("https://api.example.com/data") print(response.extensions["hishel_from_cache"]) # False # Second request - served from cache response = await client.get("https://api.example.com/data") print(response.extensions["hishel_from_cache"]) # True ``` That's it! Hishel automatically caches responses according to RFC 9111 rules. --- ## Cache Clients Hishel provides drop-in replacements for HTTPX's `Client` and `AsyncClient`: - `SyncCacheClient` - Synchronous caching client - `AsyncCacheClient` - Asynchronous caching client These clients inherit from HTTPX clients and accept all the same parameters, plus additional caching configuration. === "Sync" ```python from hishel.httpx import SyncCacheClient # Create client with default settings client = SyncCacheClient() # Make requests as usual response = client.get("https://api.example.com/users") # All HTTPX methods work client.post("https://api.example.com/data", json={"key": "value"}) client.put("https://api.example.com/resource/1", data="content") # Or use context manager with SyncCacheClient() as client: response = client.get("https://api.example.com/data") ``` === "Async" ```python from hishel.httpx import AsyncCacheClient # Create client with default settings client = AsyncCacheClient() # Make requests as usual response = await client.get("https://api.example.com/users") # All HTTPX async methods work await client.post("https://api.example.com/data", json={"key": "value"}) await client.put("https://api.example.com/resource/1", data="content") # Or use context manager (recommended) async with AsyncCacheClient() as client: response = await client.get("https://api.example.com/data") ``` ## Cache Transports For more control or to integrate with existing HTTPX clients, use cache transports directly: === "Sync" ```python import httpx from hishel import SyncSqliteStorage from hishel.httpx import SyncCacheTransport # Create transport with caching transport = SyncCacheTransport( next_transport=httpx.HTTPTransport(), storage=SyncSqliteStorage(), ) # Use with standard HTTPX client client = httpx.Client(transport=transport) response = client.get("https://api.example.com/data") ``` === "Async" ```python import httpx from hishel import AsyncSqliteStorage from hishel.httpx import AsyncCacheTransport # Create transport with caching transport = AsyncCacheTransport( next_transport=httpx.AsyncHTTPTransport(), storage=AsyncSqliteStorage(), ) # Use with standard HTTPX client client = httpx.AsyncClient(transport=transport) response = await client.get("https://api.example.com/data") ``` ## See Also - [Metadata Reference](../metadata.md) - Complete guide to caching metadata - [Storage Documentation](../storages.md) - Storage backend configuration - [Specification](../specification.md) - RFC 9111 state machine - [HTTPX Documentation](https://www.python-httpx.org/) - Official HTTPX docskarpetrosyan-hishel-fd309b1/docs/integrations/requests.md000066400000000000000000000043661514113157400237660ustar00rootroot00000000000000--- icon: simple/python --- # Requests Integration Hishel provides seamless integration with [Requests](https://requests.readthedocs.io/), adding RFC 9111-compliant HTTP caching to your Requests applications with minimal code changes. ## Quick Start Add caching to your Requests application using the `CacheAdapter`: ```python import requests from hishel.requests import CacheAdapter # Create session with cache adapter session = requests.Session() session.mount("https://", CacheAdapter()) session.mount("http://", CacheAdapter()) # First request - fetches from origin response = session.get("https://api.example.com/data") print(response.headers.get("X-Hishel-From-Cache")) # None # Second request - served from cache response = session.get("https://api.example.com/data") print(response.headers.get("X-Hishel-From-Cache")) # True ``` That's it! Hishel automatically caches responses according to RFC 9111 rules. --- ## Cache Adapter Hishel provides `CacheAdapter`, a custom HTTPAdapter that adds caching to Requests sessions. ### Basic Usage ```python import requests from hishel.requests import CacheAdapter # Create a session session = requests.Session() # Mount cache adapter for HTTP and HTTPS adapter = CacheAdapter() session.mount("https://", adapter) session.mount("http://", adapter) # Make requests as usual response = session.get("https://api.example.com/users") # All requests methods work session.post("https://api.example.com/data", json={"key": "value"}) session.put("https://api.example.com/resource/1", data="content") # Close when done session.close() ``` **Using Context Manager:** ```python import requests from hishel.requests import CacheAdapter with requests.Session() as session: session.mount("https://", CacheAdapter()) session.mount("http://", CacheAdapter()) response = session.get("https://api.example.com/data") print(response.json()) ``` ## See Also - [Metadata Reference](../metadata.md) - Complete guide to caching metadata - [Storage Documentation](../storages.md) - Storage backend configuration - [Specification](../specification.md) - RFC 9111 state machine - [HTTPX Integration](httpx.md) - Alternative async HTTP client - [Requests Documentation](https://requests.readthedocs.io/) - Official Requests docs karpetrosyan-hishel-fd309b1/docs/metadata.md000066400000000000000000000222111514113157400211520ustar00rootroot00000000000000--- icon: material/apps --- # Request and Response Metadata Metadata allows you to control caching behavior and inspect cache operations. Hishel supports metadata on both requests (to control caching) and responses (to inspect what happened). All metadata fields are prefixed with `hishel_` to avoid collisions with user data. --- ## Request Metadata Request metadata controls how Hishel caches the request and its response. You can set metadata using: - **httpx**: `extensions` parameter (recommended) or `X-Hishel-*` headers - **requests**: `X-Hishel-*` headers !!! tip "httpx supports both methods" While httpx supports both `extensions` and headers, using `extensions` is recommended as it provides better type safety and doesn't pollute HTTP headers sent to the server. ### hishel_ttl **Type:** `float | None` **Description:** Sets a custom time-to-live (TTL) for the cached response. After the specified number of seconds, the cached response will be considered expired and removed during cleanup. **Use Cases:** - Override default TTL for specific endpoints - Set shorter TTL for frequently changing data - Set longer TTL for static resources **Default:** Storage's `default_ttl` setting **Example:** === "httpx (extensions)" ```python from hishel.httpx import SyncCacheClient client = SyncCacheClient() # Cache this response for 1 hour using extensions (recommended) response = client.get( "https://api.example.com/data", extensions={"hishel_ttl": 3600} ) ``` === "httpx (headers)" ```python from hishel.httpx import SyncCacheClient client = SyncCacheClient() # Cache this response for 1 hour using headers response = client.get( "https://api.example.com/data", headers={"X-Hishel-Ttl": "3600"} ) ``` === "requests" ```python import requests from hishel.requests import CacheAdapter session = requests.Session() session.mount("http://", CacheAdapter()) session.mount("https://", CacheAdapter()) # Cache this response for 1 hour response = session.get( "https://api.example.com/data", headers={"X-Hishel-Ttl": "3600"} ) ``` --- ### hishel_refresh_ttl_on_access **Type:** `bool | None` **Description:** When `True`, accessing a cached entry resets its TTL, keeping frequently accessed entries fresh. When `False`, the TTL countdown starts from the original storage time and is not affected by subsequent accesses. **Use Cases:** - Keep popular content cached longer (sliding expiration) - Ensure rarely accessed content expires on schedule (fixed expiration) **Default:** Storage's `refresh_ttl_on_access` setting (typically `True`) **Example:** === "httpx" ```python from hishel.httpx import SyncCacheClient client = SyncCacheClient() # Enable sliding expiration - each access resets the timer response = client.get( "https://api.example.com/user/profile", extensions={"hishel_refresh_ttl_on_access": True} ) ``` === "requests" ```python import requests from hishel.requests import CacheAdapter session = requests.Session() session.mount("http://", CacheAdapter()) session.mount("https://", CacheAdapter()) # Enable sliding expiration - each access resets the timer response = session.get( "https://api.example.com/user/profile", headers={"X-Hishel-Refresh-Ttl-On-Access": "true"} ) ``` --- ### hishel_body_key **Type:** `bool | None` **Description:** When `True`, includes the request body in cache key generation. This allows caching different responses for the same URL but with different request bodies, which is essential for POST requests and GraphQL queries. **Use Cases:** - Cache POST requests with different payloads - Cache GraphQL queries (different queries to same endpoint) - Cache search requests with different parameters in body **Default:** `False` (body not included in cache key) **Example:** === "httpx" ```python from hishel.httpx import SyncCacheClient client = SyncCacheClient() # Cache POST request based on body content response = client.post( "https://api.example.com/search", json={"query": "python"}, extensions={"hishel_body_key": True} ) ``` === "requests" ```python import requests from hishel.requests import CacheAdapter session = requests.Session() session.mount("http://", CacheAdapter()) session.mount("https://", CacheAdapter()) # Cache POST request based on body content response = session.post( "https://api.example.com/search", json={"query": "python"}, headers={"X-Hishel-Body-Key": "true"} ) ``` --- ## Response Metadata Response metadata provides information about cache operations that occurred. These fields are read-only and set by Hishel. ### hishel_from_cache **Type:** `bool | None` **Description:** Indicates whether the response was served from cache (`True`) or fetched from the origin server (`False`). **Use Cases:** - Monitor cache hit rates - Debug caching behavior - Log cache performance - Conditional logic based on cache status **Example:** === "httpx" ```python from hishel.httpx import SyncCacheClient client = SyncCacheClient() response = client.get("https://api.example.com/data") # Check if response came from cache if response.extensions.get("hishel_from_cache"): print("✓ Cache hit") else: print("✗ Cache miss - fetched from origin") ``` === "requests" ```python import requests from hishel.requests import CacheAdapter session = requests.Session() session.mount("http://", CacheAdapter()) session.mount("https://", CacheAdapter()) response = session.get("https://api.example.com/data") # Check if response came from cache if response.headers.get("X-Hishel-From-Cache") == "true": print("✓ Cache hit") else: print("✗ Cache miss - fetched from origin") ``` --- ### hishel_revalidated **Type:** `bool | None` **Description:** Indicates whether a stale cached response was revalidated with the origin server. When `True`, the response was in cache but required validation (typically resulting in a 304 Not Modified response). **Use Cases:** - Monitor revalidation frequency - Debug cache freshness logic - Track conditional request behavior - Optimize cache TTL settings **Example:** === "httpx" ```python from hishel.httpx import SyncCacheClient client = SyncCacheClient() response = client.get("https://api.example.com/data") # Check if cached response was revalidated if response.extensions.get("hishel_revalidated"): print("Response was revalidated (304 Not Modified)") ``` === "requests" ```python import requests from hishel.requests import CacheAdapter session = requests.Session() session.mount("http://", CacheAdapter()) session.mount("https://", CacheAdapter()) response = session.get("https://api.example.com/data") # Check if cached response was revalidated if response.headers.get("X-Hishel-Revalidated") == "true": print("Response was revalidated (304 Not Modified)") ``` --- ### hishel_stored **Type:** `bool | None` **Description:** Indicates whether the response was successfully stored in cache. When `True`, the response met all caching requirements and was saved. When `False`, the response was not cacheable (e.g., due to `Cache-Control: no-store`). **Use Cases:** - Verify responses are being cached - Debug why responses aren't cached - Monitor cache storage success rate - Validate caching configuration **Example:** === "httpx" ```python from hishel.httpx import SyncCacheClient client = SyncCacheClient() response = client.get("https://api.example.com/data") # Check if response was cached if response.extensions.get("hishel_stored"): print("✓ Response stored in cache") else: print("✗ Response not cached") ``` === "requests" ```python import requests from hishel.requests import CacheAdapter session = requests.Session() session.mount("http://", CacheAdapter()) session.mount("https://", CacheAdapter()) response = session.get("https://api.example.com/data") # Check if response was cached if response.headers.get("X-Hishel-Stored") == "true": print("✓ Response stored in cache") else: print("✗ Response not cached") ``` ### hishel_created_at **Type:** `float | None` **Description:** POSIX timestamp (seconds since the epoch) indicating when the response entry was created in the cache. This value is set by Hishel when the response is stored and can be used with `hishel_ttl` to compute remaining freshness. **Use Cases:** - Determine when an entry was cached for logging or debugging. - Compute remaining TTL: `remaining = hishel_ttl - (now - hishel_created_at)`. **Example (httpx extensions):** ```python created = response.extensions.get("hishel_created_at") if created: print("Cached at:", created) ``` **Example (requests headers):** ```python created = response.headers.get("X-Hishel-Created-At") if created: print("Cached at:", created) ``` ---karpetrosyan-hishel-fd309b1/docs/policies.md000066400000000000000000000321231514113157400212040ustar00rootroot00000000000000--- icon: octicons/lock-16 --- # Caching Policies Hishel provides a flexible policy system that allows you to control caching behavior. Policies determine how requests and responses are cached, giving you fine-grained control over the caching strategy. ## Overview A **policy** is an object that defines the caching strategy for your HTTP client. Hishel supports two main types of policies: 1. **SpecificationPolicy** - Follows RFC 9111 HTTP caching specification 2. **FilterPolicy** - Applies custom user-defined filtering logic All policies inherit from the `CachePolicy` base class. ## SpecificationPolicy The `SpecificationPolicy` implements RFC 9111 compliant HTTP caching. This is the default policy used by `Hishel` when no policy is explicitly provided. ### Configuration ```python from hishel import CacheOptions, SpecificationPolicy policy = SpecificationPolicy( cache_options=CacheOptions( shared=True, # Act as a shared cache (proxy/CDN) allow_stale=False, # Don't serve stale responses supported_methods=["GET", "HEAD"], # Cache these methods ) ) ``` ### CacheOptions The `SpecificationPolicy` accepts a `CacheOptions` object that configures how the cache behaves: #### shared Determines whether the cache operates as a shared cache or private cache. **RFC 9111 Section 3.5**: [Authenticated Responses](https://www.rfc-editor.org/rfc/rfc9111.html#section-3.5) - **Shared cache** (`True`): Acts as a proxy, CDN, or gateway cache serving multiple users. - Must respect `private` directives - Must handle `Authorization` header restrictions - Can use `s-maxage` directive instead of `max-age` - **Private cache** (`False`): Acts as a browser or user-agent cache for a single user. - Can cache private responses - Ignores `s-maxage` directives ```python # Shared cache (proxy/CDN) policy = SpecificationPolicy( cache_options=CacheOptions(shared=True) ) # Private cache (browser) policy = SpecificationPolicy( cache_options=CacheOptions(shared=False) ) ``` #### supported_methods HTTP methods that are allowed to be cached. **RFC 9111 Section 3**: A cache MUST NOT store a response to a request unless the request method is understood by the cache. ```python # Default: cache GET and HEAD only policy = SpecificationPolicy( cache_options=CacheOptions( supported_methods=["GET", "HEAD"] ) ) # Cache POST responses (advanced use case) policy = SpecificationPolicy( cache_options=CacheOptions( supported_methods=["GET", "HEAD", "POST"] ) ) ``` #### allow_stale Controls whether stale responses can be served without revalidation. **RFC 9111 Section 4.2.4**: [Serving Stale Responses](https://www.rfc-editor.org/rfc/rfc9111.html#section-4.2.4) ```python # Conservative: never serve stale policy = SpecificationPolicy( cache_options=CacheOptions(allow_stale=False) ) # Permissive: serve stale when allowed by directives policy = SpecificationPolicy( cache_options=CacheOptions(allow_stale=True) ) ``` ### Usage Examples === "HTTPX (Async)" ```python import httpx from hishel import AsyncCacheClient, SpecificationPolicy, CacheOptions policy = SpecificationPolicy( cache_options=CacheOptions( shared=False, # Private browser cache allow_stale=False, ) ) async with AsyncCacheClient(policy=policy) as client: response = await client.get("https://api.example.com/data") ``` === "HTTPX (Sync)" ```python import httpx from hishel import SyncCacheClient, SpecificationPolicy, CacheOptions from hishel import CacheOptions policy = SpecificationPolicy( cache_options=CacheOptions( shared=True, # Shared proxy cache allow_stale=True, ) ) with SyncCacheClient(policy=policy) as client: response = client.get("https://api.example.com/data") ``` === "Requests" ```python import requests from hishel.requests import CacheAdapter from hishel import SpecificationPolicy, CacheOptions policy = SpecificationPolicy( cache_options=CacheOptions(shared=False) ) session = requests.Session() session.mount("https://", CacheAdapter(policy=policy)) session.mount("http://", CacheAdapter(policy=policy)) response = session.get("https://api.example.com/data") ``` === "ASGI Middleware" ```python from hishel.asgi import ASGICacheMiddleware from hishel import SpecificationPolicy, CacheOptions policy = SpecificationPolicy( cache_options=CacheOptions( shared=True, # Server-side shared cache allow_stale=False, ) ) app = ASGICacheMiddleware( app=your_asgi_app, policy=policy, ) ``` ## FilterPolicy The `FilterPolicy` allows you to implement custom caching logic by applying user-defined filters to requests and responses. This is useful when you need fine-grained control over what gets cached based on criteria beyond HTTP headers. ### Configuration ```python from hishel import FilterPolicy, BaseFilter policy = FilterPolicy( request_filters=[...], # List of request filters response_filters=[...], # List of response filters ) ``` ### Creating Custom Filters Filters must inherit from `BaseFilter[T]` where `T` is either `Request` or `Response`. ```python from hishel import BaseFilter, Request, Response class MyRequestFilter(BaseFilter[Request]): def needs_body(self) -> bool: """Return True if the filter needs access to the request body.""" return False def apply(self, item: Request, body: bytes | None) -> bool: """ Return True to allow caching, False to bypass cache. Args: item: The request to filter body: The request body (only if needs_body() returns True) """ # Your filtering logic here return True class MyResponseFilter(BaseFilter[Response]): def needs_body(self) -> bool: """Return True if the filter needs access to the response body.""" return False def apply(self, item: Response, body: bytes | None) -> bool: """ Return True to cache the response, False to skip caching. Args: item: The response to filter body: The response body (only if needs_body() returns True) """ # Your filtering logic here return True ``` ### Filter Examples #### Filter by URL Pattern ```python import re from hishel import BaseFilter, FilterPolicy, Request class URLPatternFilter(BaseFilter[Request]): def __init__(self, pattern: str): self.pattern = re.compile(pattern) def needs_body(self) -> bool: return False def apply(self, item: Request, body: bytes | None) -> bool: # Only cache requests matching the pattern return bool(self.pattern.search(str(item.url))) # Cache only API endpoints policy = FilterPolicy( request_filters=[ URLPatternFilter(r'/api/.*') ] ) ``` #### Filter by Response Status Code ```python from hishel import BaseFilter, FilterPolicy, Response class StatusCodeFilter(BaseFilter[Response]): def __init__(self, allowed_codes: list[int]): self.allowed_codes = allowed_codes def needs_body(self) -> bool: return False def apply(self, item: Response, body: bytes | None) -> bool: # Only cache successful responses return item.status_code in self.allowed_codes # Cache only 200 and 304 responses policy = FilterPolicy( response_filters=[ StatusCodeFilter([200, 304]) ] ) ``` #### Filter by Content Type ```python from hishel import BaseFilter, FilterPolicy, Response class ContentTypeFilter(BaseFilter[Response]): def __init__(self, allowed_types: list[str]): self.allowed_types = allowed_types def needs_body(self) -> bool: return False def apply(self, item: Response, body: bytes | None) -> bool: content_type = item.headers.get("content-type", "") return any(allowed in content_type for allowed in self.allowed_types) # Cache only JSON and XML responses policy = FilterPolicy( response_filters=[ ContentTypeFilter(["application/json", "application/xml"]) ] ) ``` #### Filter with Body Inspection ```python import json from hishel import BaseFilter, FilterPolicy, Response class JSONResponseFilter(BaseFilter[Response]): def needs_body(self) -> bool: # We need access to the body to inspect it return True def apply(self, item: Response, body: bytes | None) -> bool: if body is None: return False try: data = json.loads(body) # Cache only if response contains 'cacheable' field set to True return data.get("cacheable", False) except json.JSONDecodeError: return False policy = FilterPolicy( response_filters=[JSONResponseFilter()] ) ``` ### Combining Multiple Filters Filters are applied in sequence. All request filters must pass for the request to be checked against the cache. All response filters must pass for the response to be cached. ```python from hishel import FilterPolicy policy = FilterPolicy( request_filters=[ URLPatternFilter(r'/api/.*'), MethodFilter(["GET", "HEAD"]), ], response_filters=[ StatusCodeFilter([200, 203, 204, 300, 301, 304, 404, 405, 410]), ContentTypeFilter(["application/json"]), SizeFilter(max_size=1024 * 1024), # Max 1MB ] ) ``` ### Complete Example: GraphQL Caching ```python import json from hishel import AsyncCacheClient, FilterPolicy, BaseFilter, Request, Response class GraphQLQueryFilter(BaseFilter[Request]): """Only cache GraphQL queries (not mutations).""" def needs_body(self) -> bool: return True def apply(self, item: Request, body: bytes | None) -> bool: if body is None: return False try: data = json.loads(body) query = data.get("query", "") # Cache only if it's a query, not a mutation return "mutation" not in query.lower() except json.JSONDecodeError: return False class GraphQLSuccessFilter(BaseFilter[Response]): """Only cache successful GraphQL responses (no errors).""" def needs_body(self) -> bool: return True def apply(self, item: Response, body: bytes | None) -> bool: if item.status_code != 200 or body is None: return False try: data = json.loads(body) # Cache only if there are no GraphQL errors return "errors" not in data except json.JSONDecodeError: return False # Create the policy policy = FilterPolicy( request_filters=[GraphQLQueryFilter()], response_filters=[GraphQLSuccessFilter()], ) # Use with HTTPX async with AsyncCacheClient(policy=policy) as client: response = await client.post( "https://api.example.com/graphql", json={ "query": "{ user(id: 1) { name email } }" } ) ``` ## Policy Comparison | Feature | SpecificationPolicy | FilterPolicy | |---------|-------------------|--------------| | RFC 9111 Compliance | ✅ Full | ❌ None | | Respects Cache-Control headers | ✅ Yes | ❌ No | | Custom filtering logic | ❌ No | ✅ Yes | | Body inspection | ❌ No | ✅ Yes | | Use Case | Standard HTTP caching | Custom caching logic | | Complexity | Simple | Moderate to Complex | ## Best Practices ### When to Use SpecificationPolicy - **Standard web applications**: When caching public HTTP APIs that follow HTTP caching standards - **CDN/Proxy scenarios**: When implementing shared caches that serve multiple users - **Browser-like caching**: When you want behavior similar to a web browser's cache - **REST APIs**: When working with well-designed REST APIs that use proper cache headers ### When to Use FilterPolicy - **GraphQL APIs**: When you need to inspect query bodies to determine cacheability - **Custom business logic**: When caching decisions depend on application-specific rules - **Legacy APIs**: When working with APIs that don't properly implement HTTP caching headers - **Fine-grained control**: When you need to cache based on response content, not just headers - **POST request caching**: When you want to cache POST requests based on their content ### Performance Considerations 1. **Body Inspection**: Filters that set `needs_body() = True` will read the entire request/response body into memory. Use sparingly for large payloads. 2. **Filter Order**: Place cheaper filters (header-based) before expensive ones (body-based) to short-circuit early. 3. **Caching Strategy**: - Use `SpecificationPolicy` for standard HTTP caching (faster, battle-tested) - Use `FilterPolicy` only when you need custom logic 4. **Memory Usage**: FilterPolicy may consume more memory when inspecting bodies. Consider implementing size limits in your filters. ## See Also - [RFC 9111: HTTP Caching](https://www.rfc-editor.org/rfc/rfc9111.html) - [Specification State Machine](specification.md) - [Storage Backends](storages.md) - [GraphQL Integration](integrations/graphql.md) karpetrosyan-hishel-fd309b1/docs/specification.md000066400000000000000000000407261514113157400222250ustar00rootroot00000000000000--- icon: material/robot --- # HTTP Caching State Machine Hishel provides a sans-I/O implementation of the HTTP caching specification (RFC 9111), allowing you to integrate RFC-compliant caching into any Python application—whether client-side or server-side. The implementation uses an event-driven state machine that tells you exactly what to do next based on HTTP caching rules. You handle all I/O (network requests, storage operations), while the state machine ensures RFC 9111 compliance. ## Quick Start ```python from hishel import ( # States IdleClient, CacheMiss, FromCache, NeedRevalidation, NeedToBeUpdated, StoreAndUse, CouldNotBeStored, InvalidateEntries, # Configuration CacheOptions, ) # Create an idle state (starting point) state = IdleClient(options=CacheOptions()) # Starting point for client caching # The state machine guides you through the caching logic next_state = state.next(request, associated_entries=[]) # Each state has a specific signature for its next() method # Type hints tell you exactly what parameters are needed ``` ## How It Works The state machine exposes RFC 9111 logic as a series of **states** and **transitions**. Each state represents a specific situation in the HTTP caching lifecycle: 1. **You provide**: HTTP requests, responses, and cached data 2. **State machine decides**: What action to take next 3. **You execute**: The I/O operations (network, storage) 4. **State machine validates**: Ensures RFC compliance This design allows you to build HTTP caches that are: - ✅ **Correct**: Fully compliant with RFC 9111 - ✅ **Testable**: Sans-I/O design enables easy testing - ✅ **Flexible**: Works with any I/O library or framework - ✅ **Type-safe**: Fully typed with clear state transitions --- ## State Transitions The state machine follows a clear flow through different states based on HTTP caching rules defined in RFC 9111. Here's the complete transition map: ```mermaid graph TB IdleClient[IdleClient
Starting Point] -->|No cache or
uncacheable| CacheMiss[CacheMiss
Forward to Origin] IdleClient -->|Fresh cache| FromCache[FromCache
Use Cached Response] IdleClient -->|Stale cache| NeedRevalidation[NeedRevalidation
Validate with Origin] CacheMiss -->|Response
cacheable| StoreAndUse[StoreAndUse
Store & Return] CacheMiss -->|Response not
cacheable| CouldNotBeStored[CouldNotBeStored
Return without storing] NeedRevalidation -->|304 Not
Modified| NeedToBeUpdated[NeedToBeUpdated
Freshen Cache] NeedRevalidation -->|2xx/5xx
Response| InvalidateEntries[InvalidateEntries
Delete Old Cache] InvalidateEntries --> CacheMiss NeedToBeUpdated --> FromCache FromCache -.->|Terminal| End1((End)) StoreAndUse -.->|Terminal| End2((End)) CouldNotBeStored -.->|Terminal| End3((End)) style IdleClient fill:#8a94a3,stroke:#6c757d,color:#fff style FromCache fill:#7ba88a,stroke:#5d8a6f,color:#fff style StoreAndUse fill:#7ba88a,stroke:#5d8a6f,color:#fff style CouldNotBeStored fill:#c97a7a,stroke:#a86565,color:#fff style NeedRevalidation fill:#c9a961,stroke:#a88b4f,color:#fff style InvalidateEntries fill:#b89a7f,stroke:#9a7d66,color:#fff ``` **Legend:** - **Blue**: Entry state (IdleClient) - **Green**: Success states (FromCache, StoreAndUse) - **Red**: Failure state (CouldNotBeStored) - **Yellow**: Intermediate states requiring I/O - **Orange**: Action states (InvalidateEntries) --- ## State Flow Examples ### Example 1: Fresh Cache Hit ``` Request → IdleClient → FromCache → End ``` **Scenario:** Client requests `/api/users`, cache has fresh response **Actions:** Return cached response immediately, no origin contact ### Example 2: Cache Miss and Store ``` Request → IdleClient → CacheMiss → StoreAndUse → End ``` **Scenario:** First request for `/api/products` **Actions:** Forward to origin, receive cacheable response, store it, return to client ### Example 3: Cache Miss but Cannot Store ``` Request → IdleClient → CacheMiss → CouldNotBeStored → End ``` **Scenario:** Request `/api/private` with Authorization header, response has no cache directives **Actions:** Forward to origin, receive response with `no-store`, return without caching ### Example 4: Successful Revalidation (304) ``` Request → IdleClient → NeedRevalidation → NeedToBeUpdated → FromCache → End ``` **Scenario:** Cached `/api/data` is stale, origin confirms it's unchanged **Actions:** Send conditional request, receive 304, update cache metadata, return cached content ### Example 5: Failed Revalidation (200) ``` Request → IdleClient → NeedRevalidation → InvalidateEntries → CacheMiss → StoreAndUse → End ``` **Scenario:** Cached `/api/status` is stale, origin returns new content **Actions:** Send conditional request, receive 200 with new content, delete old cache, store new response --- ```python from hishel import IdleClient, CacheOptions state = IdleClient(options=CacheOptions()) # Starting point for client caching # signature will look like: # (method) def next( # request: Request, # associated_entries: list[Entry] # ) -> (CacheMiss | FromCache | NeedRevalidation) next_state = state.next(...) ``` In this example, `next_state` will be one of `CacheMiss`, `FromCache`, or `NeedRevalidation`, each exposing the appropriate signature for its next method. --- ## States The state machine implements RFC 9111 through a series of well-defined states. Each state represents a specific point in the HTTP caching lifecycle and determines the next action to take. ### IdleClient **What it means:** The starting point of the cache state machine. This state represents an idle client that has received a request and needs to determine whether it can be satisfied from cache, needs revalidation, or must be forwarded to the origin server. **When you're in this state:** - A new HTTP request has been received - You need to check if cached responses exist - You need to evaluate if cached responses can be used **Transitions:** - **→ FromCache**: A fresh cached response exists and can be used immediately without contacting the origin server - **→ NeedRevalidation**: A stale cached response exists that requires validation with the origin server before use - **→ CacheMiss**: No suitable cached response exists, or the request cannot be satisfied from cache **RFC Reference:** [Section 4 - Constructing Responses from Caches](https://www.rfc-editor.org/rfc/rfc9111.html#section-4) **Example:** ```python from hishel import IdleClient, CacheOptions # Create idle state idle = IdleClient(options=CacheOptions()) # Transition based on request and cached entries next_state = idle.next(request, associated_entries=[]) # Returns: CacheMiss | FromCache | NeedRevalidation ``` --- ### CacheMiss **What it means:** The request cannot be satisfied from cache and must be forwarded to the origin server. After receiving the origin's response, this state evaluates whether the response can be stored in the cache. **When you're in this state:** - No suitable cached response exists for the request - You've received a response from the origin server - You need to determine if this response should be cached **Transitions:** - **→ StoreAndUse**: The response meets all RFC 9111 storage requirements and should be cached - **→ CouldNotBeStored**: The response fails one or more storage requirements and cannot be cached **Storage Requirements Checked:** 1. Request method is understood by the cache 2. Response status code is final (not 1xx) 3. Cache understands the response status code 4. No `no-store` directive present 5. `private` directive allows storage (for shared caches) 6. `Authorization` header is properly handled 7. Response contains explicit caching information OR is heuristically cacheable **RFC Reference:** [Section 3 - Storing Responses in Caches](https://www.rfc-editor.org/rfc/rfc9111.html#section-3) **Example:** ```python from hishel import CacheMiss # After forwarding request to origin cache_miss = CacheMiss(request=request, options=options) # Evaluate response for storage next_state = cache_miss.next(response) # Returns: StoreAndUse | CouldNotBeStored ``` --- ### NeedRevalidation **What it means:** One or more stale cached responses exist for the request, but they cannot be used without validation. A conditional request must be sent to the origin server to check if the cached content is still valid. **When you're in this state:** - Cached responses exist but are stale (past their freshness lifetime) - The responses have validators (ETag or Last-Modified) - You've sent a conditional request to the origin (If-None-Match or If-Modified-Since) - You're waiting for the validation response **Transitions:** - **→ NeedToBeUpdated**: Origin responds with 304 Not Modified - cached responses are still valid and can be freshened - **→ InvalidateEntries + CacheMiss**: Origin responds with 2xx/5xx - cached responses are outdated and must be replaced - **→ CacheMiss**: No matching responses found during the freshening process **Validation Process:** 1. Client sends conditional request with validators from cached response 2. Origin server checks if content has changed 3. **304 response**: Content unchanged, update cache metadata 4. **2xx/5xx response**: Content changed or error, invalidate old cache and store new response **RFC Reference:** [Section 4.3 - Validation](https://www.rfc-editor.org/rfc/rfc9111.html#section-4.3) **Example:** ```python from hishel import NeedRevalidation # After detecting stale cache need_revalidation = NeedRevalidation( request=conditional_request, original_request=original_request, revalidating_entries=[stale_entry], options=options ) # Handle validation response next_state = need_revalidation.next(validation_response) # Returns: NeedToBeUpdated | InvalidateEntries | CacheMiss ``` --- ### FromCache **What it means:** A suitable cached response was found and can be used immediately to satisfy the request. No communication with the origin server is needed. **When you're in this state:** - A fresh cached response exists for the request - The cached response matches all request requirements (Vary headers, etc.) - The response is within its freshness lifetime OR stale responses are allowed **Transitions:** - **→ None**: This is a terminal state. Use the cached response to satisfy the request. **What to do:** 1. Retrieve the cached response 2. Update the Age header to reflect current age 3. Return the response to the client 4. No further state transitions needed **RFC Reference:** [Section 4.2 - Freshness](https://www.rfc-editor.org/rfc/rfc9111.html#section-4.2) **Example:** ```python from hishel import FromCache # When a fresh response is found from_cache = FromCache(entry=cached_entry, options=options) # This is a terminal state assert from_cache.next() is None # Use cached_entry.response to satisfy the request ``` --- ### NeedToBeUpdated **What it means:** The origin server responded with 304 Not Modified during revalidation. The cached responses are still valid but need their metadata refreshed with information from the 304 response. **When you're in this state:** - You received a 304 Not Modified response - One or more cached responses match the validators - The cached content is still valid but metadata needs updating **Transitions:** - **→ FromCache**: After updating cached responses, use them to satisfy the request **Update Process:** 1. Match cached responses using validators (ETag or Last-Modified) 2. Update matched responses with new headers from 304 response 3. Preserve the cached response body (content hasn't changed) 4. Update freshness information (Date, Cache-Control, Expires) **RFC Reference:** [Section 4.3.4 - Freshening Stored Responses](https://www.rfc-editor.org/rfc/rfc9111.html#section-4.3.4) **Example:** ```python from hishel import NeedToBeUpdated # After 304 Not Modified need_update = NeedToBeUpdated( updating_entries=[cached_entry], original_request=original_request, options=options ) # Transition to FromCache next_state = need_update.next() # Returns: FromCache ``` --- ### StoreAndUse **What it means:** The response from the origin server meets all RFC 9111 storage requirements and should be saved to the cache. This is a terminal state indicating successful caching. **When you're in this state:** - You received a response from the origin server - The response passed all storage validation checks - The response should be cached for future requests **Transitions:** - **→ None**: This is a terminal state. Store the response and use it to satisfy the request. **What to do:** 1. Store the request-response entry in your cache storage 2. Store any stream data (request/response bodies) 3. Return the response to the client 4. The response is now available for future requests **RFC Reference:** [Section 3 - Storing Responses in Caches](https://www.rfc-editor.org/rfc/rfc9111.html#section-3) **Example:** ```python from hishel import StoreAndUse # After determining response is cacheable store_and_use = StoreAndUse( entry_id=entry_id, response=response, options=options ) # This is a terminal state assert store_and_use.next() is None # Store the entry and return the response ``` --- ### CouldNotBeStored **What it means:** The response from the origin server does not meet RFC 9111 storage requirements and cannot be cached. This is a terminal state indicating the response should be used but not stored. **When you're in this state:** - You received a response from the origin server - The response failed one or more storage validation checks - The response should be returned to the client but not cached **Common Reasons:** - Contains `no-store` cache directive - Contains `private` directive (for shared caches) - Method not supported for caching - Status code not cacheable - `Authorization` header without explicit caching permission - No explicit caching directives and not heuristically cacheable **Transitions:** - **→ None**: This is a terminal state. Use the response without storing it. **What to do:** 1. Return the response to the client 2. Do NOT store it in cache 3. Future identical requests will also result in cache miss **RFC Reference:** [Section 3 - Storing Responses in Caches](https://www.rfc-editor.org/rfc/rfc9111.html#section-3) **Example:** ```python from hishel import CouldNotBeStored # After determining response is not cacheable could_not_store = CouldNotBeStored( response=response, entry_id=entry_id, options=options ) # This is a terminal state assert could_not_store.next() is None # Return response without storing ``` --- ### InvalidateEntries **What it means:** One or more cached response entries need to be invalidated (deleted) from the cache before proceeding to the next state. This is a wrapper state that performs cleanup before transitioning. **When you're in this state:** - Outdated cached responses need to be removed - You're proceeding to another state after cleanup - This typically occurs during revalidation when new responses replace old ones **Transitions:** - **→ next_state**: After invalidating entries, transition to the wrapped next state (typically `CacheMiss` or `NeedToBeUpdated`) **Common Scenarios:** 1. **After 2xx response during revalidation**: Old cached responses are outdated, invalidate them before storing new response 2. **After 5xx error during revalidation**: Server error invalidates cached responses 3. **During freshening**: Responses that don't match validators need removal **What to do:** 1. Delete the specified entries from cache storage 2. Delete associated stream data 3. Transition to the next state specified **Example:** ```python from hishel import InvalidateEntries, CacheMiss # During revalidation with new response invalidate = InvalidateEntries( entry_ids=[old_entry_id1, old_entry_id2], next_state=CacheMiss(request=request, options=options), options=options ) # Get next state after invalidation next_state = invalidate.next() # Returns: The wrapped next_state (e.g., CacheMiss) ``` ## Configuration You can pass an options parameter to any state to control how it behaves in certain situations. This was primarily added to allow configuration for cases where the RFC does not explicitly specify the behavior. In some places, the RFC might say that a cache MIGHT do something; the `options` parameter lets you define how to handle such cases. Import the CacheOptions class and pass it to the State, like so: ```python from hishel import IdleClient, CacheOptions state = IdleClient( options=CacheOptions( allow_stale=True ) ) ``` karpetrosyan-hishel-fd309b1/docs/static/000077500000000000000000000000001514113157400203415ustar00rootroot00000000000000karpetrosyan-hishel-fd309b1/docs/static/Shelkopryad_350x250_black.png000066400000000000000000000460641514113157400255100ustar00rootroot00000000000000PNG  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`karpetrosyan-hishel-fd309b1/docs/static/Shelkopryad_350x250_yellow.png000066400000000000000000000466541514113157400257540ustar00rootroot00000000000000PNG  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`karpetrosyan-hishel-fd309b1/docs/static/flask_without_cache.png000066400000000000000000003146031514113157400250640ustar00rootroot00000000000000PNG  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`karpetrosyan-hishel-fd309b1/docs/storages.md000066400000000000000000000453751514113157400212410ustar00rootroot00000000000000--- icon: material/database --- # Storage Backends Hishel provides storage backends for persisting HTTP request-response pairs. These storages are designed to work with the RFC 9111-compliant state machine and support both synchronous and asynchronous operations. ## Overview Storage backends handle: - ✅ **Entry Management**: Store complete request-response pairs (entries) - ✅ **Stream Handling**: Efficiently store and retrieve large response bodies - ✅ **TTL Management**: Automatic expiration and cleanup of old entries - ✅ **Soft Deletion**: Mark entries as deleted without immediate removal - ✅ **Cache Keys**: Group multiple entries under a single cache key ## Available Storages Currently available: - **SQLite Storage** - Persistent storage using SQLite database (async and sync) Coming soon: - Memory Storage - In-memory storage for testing and non-persistent caching - any more...? --- ## SQLite Storage SQLite storage provides persistent, file-based caching with excellent performance characteristics. It uses two tables: one for request-response entries and another for streaming data chunks. ### Initialization === "Async" ```python from hishel import AsyncSqliteStorage # Default configuration (creates hishel_cache.db in cache directory) storage = AsyncSqliteStorage() # Custom database path storage = AsyncSqliteStorage(database_path="my_cache.db") # With TTL configuration storage = AsyncSqliteStorage( default_ttl=3600.0, # 1 hour default TTL refresh_ttl_on_access=True # Reset TTL on access ) # Custom connection (advanced usage) import anysqlite conn = await anysqlite.connect("custom_cache.db") storage = AsyncSqliteStorage(connection=conn) ``` === "Sync" ```python from hishel import SyncSqliteStorage # Default configuration (creates hishel_cache.db in cache directory) storage = SyncSqliteStorage() # Custom database path storage = SyncSqliteStorage(database_path="my_cache.db") # With TTL configuration storage = SyncSqliteStorage( default_ttl=3600.0, # 1 hour default TTL refresh_ttl_on_access=True # Reset TTL on access ) # Custom connection (advanced usage) import sqlite3 conn = sqlite3.connect("custom_cache.db") storage = SyncSqliteStorage(connection=conn) ``` ### Configuration Options | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `connection` | `Connection | None` | `None` | Pre-existing database connection. If `None`, a new connection is created. | | `database_path` | `str` | `"hishel_cache.db"` | Path to the SQLite database file (relative to cache directory). | | `default_ttl` | `float | None` | `None` | Default time-to-live in seconds for cached entries. `None` means no expiration. | | `refresh_ttl_on_access` | `bool` | `True` | Whether to reset the TTL when an entry is accessed. | --- ## Basic Usage ### Creating Entries An "entry" consists of an HTTP request and its corresponding response. With the new API, you create a complete entry in one operation by providing both the request and response together. === "Async" ```python from hishel import AsyncSqliteStorage, Request, Response, Headers from hishel._utils import make_async_iterator storage = AsyncSqliteStorage() # Create a complete entry with request and response entry = await storage.create_entry( request=Request( method="GET", url="https://api.example.com/users", headers=Headers({"User-Agent": "MyApp/1.0"}) ), response=Response( status_code=200, headers=Headers({"Content-Type": "application/json"}), stream=make_async_iterator([b'{"users": []}']) ), key="GET:https://api.example.com/users" # Cache key ) # Consume the response stream to save it async for _ in entry.response.aiter_stream(): pass # entry has: # - id: UUID # - request: Request # - response: Response # - cache_key: bytes # - meta: EntryMeta (created_at timestamp) print(f"Created entry with ID: {entry.id}") print(f"Response status: {entry.response.status_code}") ``` === "Sync" ```python from hishel import SyncSqliteStorage, Request, Response, Headers from hishel._utils import make_iterator storage = SyncSqliteStorage() # Create a complete entry with request and response entry = storage.create_entry( request=Request( method="GET", url="https://api.example.com/users", headers=Headers({"User-Agent": "MyApp/1.0"}) ), response=Response( status_code=200, headers=Headers({"Content-Type": "application/json"}), stream=make_iterator([b'{"users": []}']) ), key="GET:https://api.example.com/users" # Cache key ) # Consume the response stream to save it for _ in entry.response.iter_stream(): pass # entry has: # - id: UUID # - request: Request # - response: Response # - cache_key: bytes # - meta: EntryMeta (created_at timestamp) print(f"Created entry with ID: {entry.id}") print(f"Response status: {entry.response.status_code}") ``` ### Custom Entry IDs You can optionally provide a custom UUID for the entry (useful for testing or specific use cases): === "Async" ```python import uuid entry = await storage.create_entry( request=request, response=response, key="my_cache_key", id_=uuid.UUID(int=0) # Custom UUID ) ``` === "Sync" ```python import uuid entry = storage.create_entry( request=request, response=response, key="my_cache_key", id_=uuid.UUID(int=0) # Custom UUID ) ``` ### Retrieving Cached Entries Retrieve all entries associated with a cache key. === "Async" ```python # Get all entries for a cache key cache_key = "GET:https://api.example.com/users" entries = await storage.get_entries(cache_key) # entries is a list of Entry objects for entry in entries: print(f"Cached response: {entry.response.status_code}") # Access response body through stream async for chunk in entry.response.aiter_stream(): print(f"Response chunk: {chunk}") ``` === "Sync" ```python # Get all entries for a cache key cache_key = "GET:https://api.example.com/users" entries = storage.get_entries(cache_key) # entries is a list of Entry objects for entry in entries: print(f"Cached response: {entry.response.status_code}") # Access response body through stream for chunk in entry.response.iter_stream(): print(f"Response chunk: {chunk}") ``` ### Updating Entries Update an existing entry with new information. === "Async" ```python import time from dataclasses import replace # Option 1: Update with a new entry object updated_entry = replace( entry, meta=replace(entry.meta, created_at=time.time()) ) result = await storage.update_entry(entry.id, updated_entry) # Option 2: Update using a callable def update_cache_key(entry): return replace(entry, cache_key=b"new_key") result = await storage.update_entry(entry.id, update_cache_key) if result is None: print("Entry not found") ``` === "Sync" ```python from dataclasses import replace # Option 1: Update with a new entry object updated_entry = replace( entry, response=replace(entry.response, status_code=304) ) result = storage.update_entry(entry.id, updated_entry) # Option 2: Update using a callable def update_cache_key(entry): return replace(entry, cache_key=b"new_key") result = storage.update_entry(entry.id, update_cache_key) if result is None: print("Entry not found") ``` ### Removing Entries Remove entries from the cache (soft deletion - marked as deleted but not immediately removed). === "Async" ```python # Soft delete an entry await storage.remove_entry(entry_id=entry.id) # The entry is marked as deleted and will be removed during cleanup ``` === "Sync" ```python # Soft delete an entry storage.remove_entry(entry_id=entry.id) # The entry is marked as deleted and will be removed during cleanup ``` --- ## Complete Example Here's a complete example showing the full lifecycle of cache storage: === "Async" ```python import uuid from hishel import AsyncSqliteStorage, Request, Response, Headers from hishel._utils import make_async_iterator # Initialize storage storage = AsyncSqliteStorage( database_path="my_app_cache.db", default_ttl=3600.0 # 1 hour ) # Create cache key cache_key = "GET:https://api.example.com/users" # Step 1: Create a complete entry with request and response entry = await storage.create_entry( request=Request( method="GET", url="https://api.example.com/users", stream=make_async_iterator([b"request body"]), ), response=Response( status_code=200, headers=Headers({"Content-Type": "application/json"}), stream=make_async_iterator([ b'{"users": [', b'{"id": 1, "name": "Alice"},', b'{"id": 2, "name": "Bob"}', b']}', ]), ), key=cache_key, ) # Consume streams to store them async for chunk in entry.request.aiter_stream(): pass # Storage automatically saves chunks async for chunk in entry.response.aiter_stream(): pass # Storage automatically saves chunks # Step 2: Retrieve cached entries cached_entries = await storage.get_entries(cache_key) print(f"Found {len(cached_entries)} cached entry/entries") for entry in cached_entries: print(f"Request: {entry.request.method} {entry.request.url}") print(f"Response: {entry.response.status_code}") # Read response body body_chunks = [] async for chunk in entry.response.aiter_stream(): body_chunks.append(chunk) body = b"".join(body_chunks) print(f"Body: {body.decode()}") # Step 3: Update entry if needed from dataclasses import replace updated_entry = replace( entry, cache_key=b"updated_key" ) await storage.update_entry(entry.id, updated_entry) # Step 4: Remove entry when no longer needed await storage.remove_entry(entry.id) ``` === "Sync" ```python import uuid from hishel import SyncSqliteStorage, Request, Response, Headers from hishel._utils import make_iterator # Initialize storage storage = SyncSqliteStorage( database_path="my_app_cache.db", default_ttl=3600.0 # 1 hour ) # Create cache key cache_key = "GET:https://api.example.com/users" # Step 1: Create a complete entry with request and response entry = storage.create_entry( request=Request( method="GET", url="https://api.example.com/users", stream=make_iterator([b"request body"]), ), response=Response( status_code=200, headers=Headers({"Content-Type": "application/json"}), stream=make_iterator([ b'{"users": [', b'{"id": 1, "name": "Alice"},', b'{"id": 2, "name": "Bob"}', b']}', ]), ), key=cache_key, ) # Consume streams to store them for chunk in entry.request.iter_stream(): pass # Storage automatically saves chunks for chunk in entry.response.iter_stream(): pass # Storage automatically saves chunks # Step 2: Retrieve cached entries cached_entries = storage.get_entries(cache_key) print(f"Found {len(cached_entries)} cached entry/entries") for entry in cached_entries: print(f"Request: {entry.request.method} {entry.request.url}") print(f"Response: {entry.response.status_code}") # Read response body body_chunks = [] for chunk in entry.response.iter_stream(): body_chunks.append(chunk) body = b"".join(body_chunks) print(f"Body: {body.decode()}") # Step 3: Update entry if needed from dataclasses import replace updated_entry = replace( entry, cache_key=b"updated_key" ) storage.update_entry(entry.id, updated_entry) # Step 4: Remove entry when no longer needed storage.remove_entry(entry.id) ``` --- ## Advanced Topics ### Stream Handling Hishel storages efficiently handle large request and response bodies using streams. Streams are automatically chunked and stored as you consume them. **Important**: You must consume streams (iterate through them) for the data to be stored. Simply creating an entry with a stream doesn't store the stream data. === "Async" ```python # Create entry with streaming body entry = await storage.create_entry( request=Request( method="POST", url="https://api.example.com/upload", stream=make_async_iterator([ b"chunk1", b"chunk2", b"chunk3", ]) ), response=Response( status_code=200, headers=Headers({}), stream=make_async_iterator([b"OK"]) ), key=cache_key ) # IMPORTANT: Consume the streams to store them async for chunk in entry.request.aiter_stream(): # Each chunk is stored as you iterate pass async for chunk in entry.response.aiter_stream(): pass # Now the streams are fully stored # You can retrieve them later: entries = await storage.get_entries(cache_key) async for chunk in entries[0].request.aiter_stream(): print(f"Chunk: {chunk}") ``` === "Sync" ```python # Create entry with streaming body entry = storage.create_entry( request=Request( method="POST", url="https://api.example.com/upload", stream=make_iterator([ b"chunk1", b"chunk2", b"chunk3", ]) ), response=Response( status_code=200, headers=Headers({}), stream=make_iterator([b"OK"]) ), key=cache_key ) # IMPORTANT: Consume the streams to store them for chunk in entry.request.iter_stream(): # Each chunk is stored as you iterate pass for chunk in entry.response.iter_stream(): pass # Now the streams are fully stored # You can retrieve them later: entries = storage.get_entries(cache_key) for chunk in entries[0].request.iter_stream(): print(f"Chunk: {chunk}") ``` ### TTL and Expiration Control how long cached entries remain valid: === "Async" ```python # Set default TTL for all entries storage = AsyncSqliteStorage(default_ttl=3600.0) # 1 hour # Override TTL for specific requests using metadata entry = await storage.create_entry( request=Request( method="GET", url="https://api.example.com/data", metadata={"hishel_ttl": 7200.0} # 2 hours for this entry ), response=response, key=cache_key ) # Disable TTL refresh on access storage = AsyncSqliteStorage( default_ttl=3600.0, refresh_ttl_on_access=False # TTL won't reset when accessed ) ``` === "Sync" ```python # Set default TTL for all entries storage = SyncSqliteStorage(default_ttl=3600.0) # 1 hour # Override TTL for specific requests using metadata entry = storage.create_entry( request=Request( method="GET", url="https://api.example.com/data", metadata={"hishel_ttl": 7200.0} # 2 hours for this entry ), response=response, key=cache_key ) # Disable TTL refresh on access storage = SyncSqliteStorage( default_ttl=3600.0, refresh_ttl_on_access=False # TTL won't reset when accessed ) ``` ### Cleanup and Maintenance Storage automatically performs cleanup operations to remove expired and deleted entries. Cleanup runs periodically when storage operations are performed. **Cleanup removes:** - Expired entries (past their TTL) - Entries marked as deleted for more than 7 days - Entries with missing or incomplete streams The cleanup process is automatic and doesn't require manual intervention. ### Custom Entry IDs By default, entry IDs are auto-generated UUIDs. You can provide custom IDs if needed: === "Async" ```python import uuid # Provide custom UUID custom_id = uuid.uuid4() entry = await storage.create_entry( request=Request(method="GET", url="https://api.example.com"), response=response, key=cache_key, id_=custom_id ) assert entry.id == custom_id ``` === "Sync" ```python import uuid # Provide custom UUID custom_id = uuid.uuid4() entry = storage.create_entry( request=Request(method="GET", url="https://api.example.com"), response=response, key=cache_key, id_=custom_id ) assert entry.id == custom_id ``` --- ## Database Schema For reference, here's the SQLite database schema used by the storage: ### `entries` Table Stores request-response entry metadata. | Column | Type | Description | |--------|------|-------------| | `id` | BLOB | Primary key - UUID of the entry | | `cache_key` | BLOB | Cache key for grouping entries | | `data` | BLOB | Serialized entry data (request, response, metadata) | | `created_at` | REAL | Timestamp when the entry was created | | `deleted_at` | REAL | Timestamp when soft deleted (NULL if not deleted) | **Indexes:** - `idx_entries_cache_key` - Fast lookups by cache key - `idx_entries_deleted_at` - Efficient cleanup queries ### `streams` Table Stores request and response body chunks. | Column | Type | Description | |--------|------|-------------| | `entry_id` | BLOB | Foreign key to entries.id | | `kind` | INTEGER | Stream type: 0 = request, 1 = response | | `chunk_number` | INTEGER | Chunk sequence number (0, 1, 2, ... or -1 for completion marker) | | `chunk_data` | BLOB | The actual chunk data | **Primary Key:** `(entry_id, kind, chunk_number)` **Special Values:** - `chunk_number = -1` - Completion marker (empty data, signals end of stream) - `kind = 1` - Response stream --- karpetrosyan-hishel-fd309b1/examples/000077500000000000000000000000001514113157400177405ustar00rootroot00000000000000karpetrosyan-hishel-fd309b1/examples/async_httpx.py000077500000000000000000000021461514113157400226640ustar00rootroot00000000000000#!/usr/bin/env uv run # /// script # requires-python = ">=3.10" # dependencies = [ # "hishel[async, httpx]", # ] # # [tool.uv.sources] # hishel = { path = "../", editable = true } # /// import asyncio from typing import cast import anysqlite from hishel import ResponseMetadata from hishel._core._storages._async_sqlite import AsyncSqliteStorage from hishel.httpx import AsyncCacheClient async def fetch_and_print(client, url: str): print(f"\n➡ Sending request to {url}...") response = await client.get(url) meta = cast(ResponseMetadata, response.extensions) print(f"🚀 Was Stored: {meta['hishel_stored']}") print(f"⏰ Created At: {meta['hishel_created_at']}") print(f"🔄 From Cache: {meta['hishel_from_cache']}") print(f"📍 Revalidated: {meta['hishel_revalidated']}") async def main(): url = "https://hishel.com/" async with AsyncCacheClient(storage=AsyncSqliteStorage(connection=await anysqlite.connect(":memory:"))) as client: await fetch_and_print(client, url) await fetch_and_print(client, url) if __name__ == "__main__": asyncio.run(main()) karpetrosyan-hishel-fd309b1/examples/fastapi_.py000066400000000000000000000022031514113157400220750ustar00rootroot00000000000000# /// script # requires-python = ">=3.10" # dependencies = [ # "hishel[fastapi]", # "httpx", # "yaspin", # ] # # [tool.uv.sources] # hishel = { path = "../", editable = true } # /// import asyncio import time import httpx import yaspin from fastapi import FastAPI from hishel.asgi import ASGICacheMiddleware from hishel.fastapi import cache app = FastAPI() processed_requests = 0 @app.get("/items/", dependencies=[cache(max_age=5)]) async def read_item(): global processed_requests processed_requests += 1 return {"created_at": time.time(), "processed_requests": processed_requests} async def main(): async with httpx.AsyncClient(transport=httpx.ASGITransport(app=ASGICacheMiddleware(app))) as client: while True: response = await client.get("http://testserver/items/") data = response.json() print(f"Response: created_at={data['created_at']:.2f}, processed_requests={data['processed_requests']}, ") with yaspin.yaspin(text="Waiting 2 seconds before next request..."): await asyncio.sleep(2) if __name__ == "__main__": asyncio.run(main()) karpetrosyan-hishel-fd309b1/examples/httpx_in_memory.py000066400000000000000000000007231514113157400235410ustar00rootroot00000000000000#!/usr/bin/env uv run # /// script # requires-python = ">=3.10" # dependencies = [ # "hishel[httpx]", # ] # # [tool.uv.sources] # hishel = { path = "../", editable = true } # /// import sqlite3 from hishel import SyncSqliteStorage from hishel.httpx import SyncCacheClient cl = SyncCacheClient(storage=SyncSqliteStorage(connection=sqlite3.connect(":memory:"))) cl.get("https://hishel.com/") response = cl.get("https://hishel.com/") print(response.extensions) karpetrosyan-hishel-fd309b1/examples/requests_.py000077500000000000000000000020101514113157400223200ustar00rootroot00000000000000#!/usr/bin/env uv run # /// script # requires-python = ">=3.10" # dependencies = [ # "hishel[requests]", # ] # # [tool.uv.sources] # hishel = { path = "../", editable = true } # /// import sqlite3 import requests from hishel._core._storages._sync_sqlite import SyncSqliteStorage from hishel.requests import CacheAdapter session = requests.Session() adapter = CacheAdapter(storage=SyncSqliteStorage(connection=sqlite3.connect(":memory:"))) session.mount("http://", adapter) session.mount("https://", adapter) def fetch_and_print(url: str): print(f"\n➡ Sending request to {url}...") response = session.get(url) print(f"🚀 Was Stored: {response.headers['x-hishel-stored']}") print(f"⏰ Created At: {response.headers['x-hishel-created-at']}") print(f"🔄 From Cache: {response.headers['x-hishel-from-cache']}") print(f"📍 Revalidated: {response.headers['x-hishel-revalidated']}") if __name__ == "__main__": url = "https://hishel.com/" fetch_and_print(url) fetch_and_print(url) karpetrosyan-hishel-fd309b1/hishel/000077500000000000000000000000001514113157400173765ustar00rootroot00000000000000karpetrosyan-hishel-fd309b1/hishel/__init__.py000066400000000000000000000034421514113157400215120ustar00rootroot00000000000000from hishel._core._storages._async_sqlite import AsyncSqliteStorage from hishel._core._storages._async_base import AsyncBaseStorage from hishel._core._storages._sync_sqlite import SyncSqliteStorage from hishel._core._storages._sync_base import SyncBaseStorage from hishel._core._headers import Headers as Headers from hishel._core._spec import ( AnyState as AnyState, CacheMiss as CacheMiss, CacheOptions as CacheOptions, CouldNotBeStored as CouldNotBeStored, FromCache as FromCache, IdleClient as IdleClient, InvalidateEntries, NeedRevalidation as NeedRevalidation, NeedToBeUpdated as NeedToBeUpdated, State as State, StoreAndUse as StoreAndUse, ) from hishel._core.models import ( Entry as Entry, EntryMeta as EntryMeta, Request as Request, Response as Response, ResponseMetadata as ResponseMetadata, RequestMetadata as RequestMetadata, ) from hishel._async_cache import AsyncCacheProxy as AsyncCacheProxy from hishel._sync_cache import SyncCacheProxy as SyncCacheProxy from hishel._policies import SpecificationPolicy, FilterPolicy, CachePolicy, BaseFilter __all__ = ( # New API ## States "AnyState", "IdleClient", "CacheMiss", "FromCache", "NeedRevalidation", "AnyState", "CacheOptions", "NeedToBeUpdated", "State", "StoreAndUse", "CouldNotBeStored", "InvalidateEntries", ## Models "Request", "Response", "Entry", "EntryMeta", "RequestMetadata", "ResponseMetadata", ## Headers "Headers", ## Storages "SyncBaseStorage", "AsyncBaseStorage", "SyncSqliteStorage", "AsyncSqliteStorage", # Proxy "AsyncCacheProxy", "SyncCacheProxy", # Policies "BaseFilter", "CachePolicy", "SpecificationPolicy", "FilterPolicy", ) karpetrosyan-hishel-fd309b1/hishel/_async_cache.py000066400000000000000000000212071514113157400223510ustar00rootroot00000000000000from __future__ import annotations import hashlib import logging import time from dataclasses import replace from typing import AsyncIterable, AsyncIterator, Awaitable, Callable from typing_extensions import assert_never from hishel import ( AnyState, AsyncBaseStorage, AsyncSqliteStorage, CacheMiss, CouldNotBeStored, FromCache, IdleClient, NeedRevalidation, NeedToBeUpdated, Request, Response, StoreAndUse, ) from hishel._core._spec import InvalidateEntries, vary_headers_match from hishel._core.models import Entry, ResponseMetadata from hishel._policies import CachePolicy, FilterPolicy, SpecificationPolicy from hishel._utils import make_async_iterator logger = logging.getLogger("hishel.integrations.clients") class AsyncCacheProxy: """ A proxy for HTTP caching in clients. This class is independent of any specific HTTP library and works only with internal models. It delegates request execution to a user-provided callable, making it compatible with any HTTP client. Caching behavior is determined by the policy object. Args: request_sender: Callable that sends HTTP requests and returns responses. storage: Storage backend for cache entries. Defaults to AsyncSqliteStorage. policy: Caching policy to use. Can be SpecificationPolicy (respects RFC 9111) or FilterPolicy (user-defined filtering). Defaults to SpecificationPolicy(). """ def __init__( self, request_sender: Callable[[Request], Awaitable[Response]], storage: AsyncBaseStorage | None = None, policy: CachePolicy | None = None, ) -> None: self.send_request = request_sender self.storage = storage if storage is not None else AsyncSqliteStorage() self.policy = policy if policy is not None else SpecificationPolicy() async def handle_request(self, request: Request) -> Response: if isinstance(self.policy, FilterPolicy): return await self._handle_request_with_filters(request) return await self._handle_request_respecting_spec(request) async def _get_key_for_request(self, request: Request) -> str: if self.policy.use_body_key or request.metadata.get("hishel_body_key"): assert isinstance(request.stream, (AsyncIterator, AsyncIterable)) collected = b"".join([chunk async for chunk in request.stream]) hash_ = hashlib.sha256(collected).hexdigest() request.stream = make_async_iterator([collected]) return hash_ return hashlib.sha256(str(request.url).encode("utf-8")).hexdigest() async def _maybe_refresh_entry_ttl(self, entry: Entry) -> None: if entry.request.metadata.get("hishel_refresh_ttl_on_access"): await self.storage.update_entry( entry.id, lambda current_entry: replace( current_entry, meta=replace(current_entry.meta, created_at=time.time()), ), ) async def _handle_request_with_filters(self, request: Request) -> Response: assert isinstance(self.policy, FilterPolicy) for request_filter in self.policy.request_filters: if request_filter.needs_body(): body = await request.aread() if not request_filter.apply(request, body): logger.debug("Request filtered out by request filter") return await self.send_request(request) else: if not request_filter.apply(request, None): logger.debug("Request filtered out by request filter") return await self.send_request(request) logger.debug("Trying to get cached response ignoring specification") cache_key = await self._get_key_for_request(request) entries = await self.storage.get_entries(cache_key) logger.debug(f"Found {len(entries)} cached entries for the request") for entry in entries: if ( str(entry.request.url) == str(request.url) and entry.request.method == request.method and vary_headers_match( request, entry, ) ): logger.debug( "Found matching cached response for the request", ) response_meta = ResponseMetadata( hishel_from_cache=True, hishel_created_at=entry.meta.created_at, hishel_revalidated=False, hishel_stored=False, ) entry.response.metadata.update(response_meta) # type: ignore await self._maybe_refresh_entry_ttl(entry) return entry.response response = await self.send_request(request) for response_filter in self.policy.response_filters: if response_filter.needs_body(): body = await response.aread() if not response_filter.apply(response, body): logger.debug("Response filtered out by response filter") return response else: if not response_filter.apply(response, None): logger.debug("Response filtered out by response filter") return response response_meta = ResponseMetadata( hishel_from_cache=False, hishel_created_at=time.time(), hishel_revalidated=False, hishel_stored=True, ) response.metadata.update(response_meta) # type: ignore logger.debug("Storing response in cache ignoring specification") entry = await self.storage.create_entry( request, response, cache_key, ) return entry.response async def _handle_request_respecting_spec(self, request: Request) -> Response: assert isinstance(self.policy, SpecificationPolicy) state: AnyState = IdleClient(options=self.policy.cache_options) while state: logger.debug(f"Handling state: {state.__class__.__name__}") if isinstance(state, IdleClient): state = await self._handle_idle_state(state, request) elif isinstance(state, CacheMiss): state = await self._handle_cache_miss(state) elif isinstance(state, StoreAndUse): return await self._handle_store_and_use(state, request) elif isinstance(state, CouldNotBeStored): return state.response elif isinstance(state, NeedRevalidation): state = await self._handle_revalidation(state) elif isinstance(state, FromCache): await self._maybe_refresh_entry_ttl(state.entry) return state.entry.response elif isinstance(state, NeedToBeUpdated): state = await self._handle_update(state) elif isinstance(state, InvalidateEntries): state = await self._handle_invalidate_entries(state) else: assert_never(state) raise RuntimeError("Unreachable") async def _handle_idle_state(self, state: IdleClient, request: Request) -> AnyState: stored_entries = await self.storage.get_entries(await self._get_key_for_request(request)) return state.next(request, stored_entries) async def _handle_cache_miss(self, state: CacheMiss) -> AnyState: response = await self.send_request(state.request) return state.next(response) async def _handle_store_and_use(self, state: StoreAndUse, request: Request) -> Response: entry = await self.storage.create_entry( request, state.response, await self._get_key_for_request(request), ) return entry.response async def _handle_revalidation(self, state: NeedRevalidation) -> AnyState: revalidation_response = await self.send_request(state.request) return state.next(revalidation_response) async def _handle_update(self, state: NeedToBeUpdated) -> AnyState: for updating_entry in state.updating_entries: await self.storage.update_entry( updating_entry.id, lambda existing_entry: replace( existing_entry, response=replace(existing_entry.response, headers=updating_entry.response.headers), ), ) return state.next() async def _handle_invalidate_entries(self, state: InvalidateEntries) -> AnyState: for entry_id in state.entry_ids: await self.storage.remove_entry(entry_id) return state.next() karpetrosyan-hishel-fd309b1/hishel/_async_httpx.py000066400000000000000000000176771514113157400224750ustar00rootroot00000000000000from __future__ import annotations import ssl import typing as t from typing import ( AsyncIterable, AsyncIterator, Iterator, Union, cast, overload, ) from httpx import RequestNotRead from hishel import AsyncCacheProxy, Headers, Request, Response from hishel._core._storages._async_base import AsyncBaseStorage from hishel._core.models import RequestMetadata, extract_metadata_from_headers from hishel._policies import CachePolicy from hishel._utils import ( filter_mapping, make_async_iterator, ) try: import httpx except ImportError as e: raise ImportError( "httpx is required to use hishel.httpx module. " "Please install hishel with the 'httpx' extra, " "e.g., 'pip install hishel[httpx]'." ) from e SOCKET_OPTION = t.Union[ t.Tuple[int, int, int], t.Tuple[int, int, t.Union[bytes, bytearray]], t.Tuple[int, int, None, int], ] # 128 KB CHUNK_SIZE = 131072 @overload def _internal_to_httpx( value: Request, ) -> httpx.Request: ... @overload def _internal_to_httpx( value: Response, ) -> httpx.Response: ... def _internal_to_httpx( value: Union[Request, Response], ) -> Union[httpx.Request, httpx.Response]: """ Convert internal Request/Response to httpx.Request/httpx.Response. """ if isinstance(value, Request): return httpx.Request( method=value.method, url=value.url, headers=value.headers, stream=_IteratorStream(value._aiter_stream()), extensions=value.metadata, ) elif isinstance(value, Response): return httpx.Response( status_code=value.status_code, headers=value.headers, stream=_IteratorStream(value._aiter_stream()), extensions=value.metadata, ) @overload def _httpx_to_internal( value: httpx.Request, ) -> Request: ... @overload def _httpx_to_internal( value: httpx.Response, ) -> Response: ... def _httpx_to_internal( value: Union[httpx.Request, httpx.Response], ) -> Union[Request, Response]: """ Convert httpx.Request/httpx.Response to internal Request/Response. """ headers = Headers( filter_mapping( Headers({key: value for key, value in value.headers.items()}), ["Transfer-Encoding"], ) ) if isinstance(value, httpx.Request): extension_metadata = RequestMetadata( hishel_refresh_ttl_on_access=value.extensions.get("hishel_refresh_ttl_on_access"), hishel_ttl=value.extensions.get("hishel_ttl"), hishel_spec_ignore=value.extensions.get("hishel_spec_ignore"), hishel_body_key=value.extensions.get("hishel_body_key"), ) headers_metadata = extract_metadata_from_headers(value.headers) for key, val in extension_metadata.items(): if key in value.extensions: headers_metadata[key] = val # type: ignore try: stream = make_async_iterator([value.content]) except RequestNotRead: stream = cast(AsyncIterator[bytes], value.stream) return Request( method=value.method, url=str(value.url), headers=headers, stream=stream, metadata=headers_metadata, ) elif isinstance(value, httpx.Response): stream = ( make_async_iterator([value.content]) if value.is_stream_consumed else value.aiter_raw(chunk_size=CHUNK_SIZE) ) if value.is_stream_consumed and "content-encoding" in value.headers: # If the stream was consumed and we don't know about # the original data and its size, fix the Content-Length # header and remove Content-Encoding so we can recreate it later properly. headers = Headers( { **filter_mapping( headers, ["content-encoding"], ), "content-length": str(len(value.content)), } ) return Response( status_code=value.status_code, headers=headers, stream=stream, metadata={}, ) class _IteratorStream(httpx.SyncByteStream, httpx.AsyncByteStream): def __init__(self, iterator: Iterator[bytes] | AsyncIterator[bytes]) -> None: self.iterator = iterator async def __aiter__(self) -> AsyncIterator[bytes]: assert isinstance(self.iterator, (AsyncIterator, AsyncIterable)) async for chunk in self.iterator: yield chunk class AsyncCacheTransport(httpx.AsyncBaseTransport): def __init__( self, next_transport: httpx.AsyncBaseTransport, storage: AsyncBaseStorage | None = None, policy: CachePolicy | None = None, ) -> None: self.next_transport = next_transport self._cache_proxy: AsyncCacheProxy = AsyncCacheProxy( request_sender=self.request_sender, storage=storage, policy=policy, ) self.storage = self._cache_proxy.storage async def handle_async_request( self, request: httpx.Request, ) -> httpx.Response: internal_request = _httpx_to_internal(request) internal_response = await self._cache_proxy.handle_request(internal_request) response = _internal_to_httpx(internal_response) return response async def aclose(self) -> None: await self.next_transport.aclose() await self.storage.close() await super().aclose() async def request_sender(self, request: Request) -> Response: httpx_request = _internal_to_httpx(request) httpx_response = await self.next_transport.handle_async_request(httpx_request) if httpx_response.status_code == 304: # 304 should not have a body, but we read it to ensure we'll not let the stream unconsumed await httpx_response.aread() return _httpx_to_internal(httpx_response) class AsyncCacheClient(httpx.AsyncClient): def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: self.storage: AsyncBaseStorage | None = kwargs.pop("storage", None) self.policy: CachePolicy | None = kwargs.pop("policy", None) super().__init__(*args, **kwargs) def _init_transport( self, verify: ssl.SSLContext | str | bool = True, cert: t.Union[str, t.Tuple[str, str], t.Tuple[str, str, str], None] = None, trust_env: bool = True, http1: bool = True, http2: bool = False, limits: httpx.Limits = httpx.Limits(max_connections=100, max_keepalive_connections=20), transport: httpx.AsyncBaseTransport | None = None, **kwargs: t.Any, ) -> httpx.AsyncBaseTransport: if transport is not None: return transport return AsyncCacheTransport( next_transport=httpx.AsyncHTTPTransport( verify=verify, cert=cert, trust_env=trust_env, http1=http1, http2=http2, limits=limits, ), storage=self.storage, policy=self.policy, ) def _init_proxy_transport( self, proxy: httpx.Proxy, verify: ssl.SSLContext | str | bool = True, cert: t.Union[str, t.Tuple[str, str], t.Tuple[str, str, str], None] = None, trust_env: bool = True, http1: bool = True, http2: bool = False, limits: httpx.Limits = httpx.Limits(max_connections=100, max_keepalive_connections=20), **kwargs: t.Any, ) -> httpx.AsyncBaseTransport: return AsyncCacheTransport( next_transport=httpx.AsyncHTTPTransport( verify=verify, cert=cert, trust_env=trust_env, http1=http1, http2=http2, limits=limits, proxy=proxy, ), storage=self.storage, policy=self.policy, ) karpetrosyan-hishel-fd309b1/hishel/_core/000077500000000000000000000000001514113157400204655ustar00rootroot00000000000000karpetrosyan-hishel-fd309b1/hishel/_core/_headers.py000066400000000000000000000421521514113157400226150ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass from typing import ( Any, Iterator, List, Literal, Mapping, MutableMapping, Optional, Union, cast, ) """ HTTP token and quoted-string parsing utilities. These functions implement RFC 7230 parsing rules for HTTP/1.1 tokens and quoted strings. """ def is_char(c: str) -> bool: """ Check if character is a valid ASCII character (0-127). Per RFC 7230: CHAR = any US-ASCII character (octets 0 - 127) Args: c: Single character string Returns: True if character is valid ASCII (0-127), False otherwise """ if not c: return False return ord(c) <= 127 def is_ctl(c: str) -> bool: """ Check if character is a control character. Per RFC 7230: CTL = control characters (0-31 and 127) Args: c: Single character string Returns: True if character is a control character, False otherwise """ if not c: return False b = ord(c) return b <= 31 or b == 127 def is_separator(c: str) -> bool: """ Check if character is an HTTP separator. Per RFC 2616 Section 2.2: separators = "(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\" | <"> | "/" | "[" | "]" | "?" | "=" | "{" | "}" | SP | HT Args: c: Single character string Returns: True if character is a separator, False otherwise """ if not c: return False return c in '()<>@,;:\\"/[]?={} \t' def is_token(c: str) -> bool: """ Check if character is valid in an HTTP token. Per RFC 7230 Section 3.2.6: token = 1*tchar tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / "0"-"9" / "A"-"Z" / "^" / "_" / "`" / "a"-"z" / "|" / "~" Implementation: token chars are CHAR but not CTL or separators Args: c: Single character string Returns: True if character is valid in a token, False otherwise Examples: >>> is_token('a') True >>> is_token('Z') True >>> is_token('5') True >>> is_token('-') True >>> is_token('!') True >>> is_token(' ') False >>> is_token(',') False >>> is_token('=') False """ return is_char(c) and not is_ctl(c) and not is_separator(c) def is_qd_text(c: str) -> bool: r""" Check if character is valid in quoted-text. Per RFC 7230 Section 3.2.6: quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE qdtext = HTAB / SP / %x21 / %x23-5B / %x5D-7E / obs-text obs-text = %x80-FF In other words: - HTAB (0x09) - SP (0x20) - 0x21 (!) - 0x23-0x5B (# to [, excluding " which is 0x22) - 0x5D-0x7E (] to ~, excluding \ which is 0x5C) - 0x80-0xFF (obs-text, extended ASCII) Args: c: Single character string Returns: True if character is valid quoted-text, False otherwise """ if not c: return False b = ord(c) return ( b == 0x09 # HTAB or b == 0x20 # SP or b == 0x21 # ! or (0x23 <= b <= 0x5B) # # to [ (skips " which is 0x22) or (0x5D <= b <= 0x7E) # ] to ~ (skips \ which is 0x5C) or b >= 0x80 ) # obs-text def http_unquote_pair(c: str) -> str: """ Unquote a single escaped character from a quoted-pair. Per RFC 7230 Section 3.2.6: quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text ) VCHAR = visible characters (0x21-0x7E) Valid escaped characters: - HTAB (0x09) - SP (0x20) - VCHAR (0x21-0x7E) - obs-text (0x80-0xFF) Invalid characters are replaced with '?' Args: c: Single character string (the character after the backslash) Returns: The unquoted character, or '?' if invalid Examples: >>> http_unquote_pair('"') '"' >>> http_unquote_pair('n') 'n' >>> http_unquote_pair('\\') '\\' """ if not c: return "?" b = ord(c) # Valid characters that can be escaped if b == 0x09 or b == 0x20 or (0x21 <= b <= 0x7E) or b >= 0x80: return c return "?" def http_unquote(raw: str) -> tuple[int, str]: """ Unquote an HTTP quoted-string. Per RFC 7230 Section 3.2.6: quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text ) The raw string must begin with a double quote ("). Only the first quoted string is parsed. The function returns the number of characters consumed and the unquoted result. Args: raw: String that must start with a double quote Returns: Tuple of (eaten, result) where: - eaten: number of characters consumed, or -1 on failure - result: the unquoted string, or empty string on failure Examples: >>> http_unquote('"hello"') (7, 'hello') >>> http_unquote('"hello world"') (13, 'hello world') >>> http_unquote('"hello\\"world"') (14, 'hello"world') >>> http_unquote('"test') (-1, '') >>> http_unquote('not quoted') (-1, '') """ if not raw or raw[0] != '"': return -1, "" buf: list[str] = [] i = 1 # Start after opening quote while i < len(raw): b = raw[i] if b == '"': # Found closing quote - success return i + 1, "".join(buf) elif b == "\\": # Escaped character (quoted-pair) if i + 1 >= len(raw): # Backslash at end of string - invalid return -1, "" # Unquote the next character buf.append(http_unquote_pair(raw[i + 1])) i += 2 # Skip both backslash and escaped char else: # Regular character if is_qd_text(b): buf.append(b) else: # Invalid character in quoted text buf.append("?") i += 1 # Reached end without finding closing quote - invalid return -1, "" class Headers(MutableMapping[str, str]): def __init__(self, headers: Mapping[str, Union[str, List[str]]]) -> None: self._headers = {k.lower(): ([v] if isinstance(v, str) else v[:]) for k, v in headers.items()} def get_list(self, key: str) -> Optional[List[str]]: return self._headers.get(key.lower(), None) def __getitem__(self, key: str) -> str: return ", ".join(self._headers[key.lower()]) def __setitem__(self, key: str, value: str) -> None: self._headers.setdefault(key.lower(), []).append(value) def __delitem__(self, key: str) -> None: del self._headers[key.lower()] def __iter__(self) -> Iterator[str]: return iter(self._headers) def __len__(self) -> int: return len(self._headers) def __repr__(self) -> str: return repr(self._headers) def __str__(self) -> str: return str(self._headers) def __eq__(self, other_headers: Any) -> bool: return isinstance(other_headers, Headers) and self._headers == other_headers._headers # type: ignore class Vary: def __init__(self, values: List[str]) -> None: self.values = values @classmethod def from_value(cls, vary_value: str) -> "Vary": values = [] for field_name in vary_value.split(","): field_name = field_name.strip() values.append(field_name) return Vary(values) @dataclass class Range: unit: Literal["bytes"] range: tuple[int | None, int | None] @classmethod def try_from_str(cls, range_header: str) -> "Range" | None: # Example: "bytes=0-99,200-299,-500,100-" unit, values = range_header.split("=") unit = unit.strip() parts = [p.strip() for p in values.split(",")] parsed: list[tuple[int | None, int | None]] = [] for part in parts: if "-" not in part: raise ValueError(f"Invalid range part: {part}") start_str, end_str = part.split("-", 1) start = int(start_str) if start_str else None end = int(end_str) if end_str else None parsed.append((start, end)) if len(parsed) != 1: # we don't support multiple ranges return None return cls( unit=cast(Literal["bytes"], unit), range=parsed[0], ) class CacheControl: """ Unified Cache-Control directives for both requests and responses. Supports all standard directives from RFC9111 and experimental directives. Uses None for unset values instead of -1. Supported Directives: - immutable [RFC8246] - max-age [RFC9111, Section 5.2.1.1, 5.2.2.1] - max-stale [RFC9111, Section 5.2.1.2] - min-fresh [RFC9111, Section 5.2.1.3] - must-revalidate [RFC9111, Section 5.2.2.2] - must-understand [RFC9111, Section 5.2.2.3] - no-cache [RFC9111, Section 5.2.1.4, 5.2.2.4] - no-store [RFC9111, Section 5.2.1.5, 5.2.2.5] - no-transform [RFC9111, Section 5.2.1.6, 5.2.2.6] - only-if-cached [RFC9111, Section 5.2.1.7] - private [RFC9111, Section 5.2.2.7] - proxy-revalidate [RFC9111, Section 5.2.2.8] - public [RFC9111, Section 5.2.2.9] - s-maxage [RFC9111, Section 5.2.2.10] - stale-if-error [RFC5861, Section 4] - stale-while-revalidate [RFC5861, Section 3] no_cache and private can be: - None: directive not present - True: directive present without field names - List[str]: directive present with specific field names """ def __init__(self) -> None: # Common directives self.max_age: Optional[int] = None self.no_store: bool = False self.no_transform: bool = False # Request-specific self.max_stale: Optional[int] = None self.min_fresh: Optional[int] = None self.only_if_cached: bool = False # Response-specific self.must_revalidate: bool = False self.must_understand: bool = False self.public: bool = False self.proxy_revalidate: bool = False self.s_maxage: Optional[int] = None self.immutable: bool = False # Can be boolean or contain field names self.no_cache: Union[bool, List[str]] = False self.private: Union[bool, List[str]] = False # Experimental self.stale_if_error: Optional[int] = None self.stale_while_revalidate: Optional[int] = None # Extensions (unrecognized directives) self.extensions: List[str] = [] def parse_int_value(value: str) -> Optional[int]: """Parse integer value, return None if invalid.""" try: val = int(value) # Cap at max int32 for compatibility return min(val, 2147483647) if val >= 0 else None except (ValueError, OverflowError): return None def parse_field_names(value: str) -> List[str]: """Parse comma-separated field names and canonicalize them.""" fields = [] for field in value.split(","): field = field.strip() if field: # Convert to canonical header form (Title-Case) canonical = "-".join(word.capitalize() for word in field.split("-")) fields.append(canonical) return fields def has_field_names(token: str) -> bool: """Check if token can have comma-separated field names.""" return token in ("no-cache", "private") def parse(value: str) -> CacheControl: """ Parse a Cache-Control header value character by character. This parser handles quoted values and field names correctly, allowing commas within field name lists. Args: value: The Cache-Control header value string Returns: CacheControl object with parsed directives """ cc = CacheControl() if not value: return cc i = 0 length = len(value) while i < length: # Skip leading whitespace and commas while i < length and (value[i] in (" ", "\t", ",")): i += 1 if i >= length: break # Find end of token j = i while j < length and is_token(value[j]): j += 1 if j == i: # No valid token found, skip this character i += 1 continue token = value[i:j].lower() token_has_fields = has_field_names(token) # Skip whitespace after token while j < length and value[j] in (" ", "\t"): j += 1 # Check if token has a value (token=value) if j < length and value[j] == "=": k = j + 1 # Skip whitespace after equals sign while k < length and value[k] in (" ", "\t"): k += 1 if k >= length: # Directive ends with '=' but no value i = k continue # Check for quoted value if value[k] == '"': eaten, result = http_unquote(value[k:]) if eaten == -1: # Quote mismatch, skip to next directive i = k + 1 continue i = k + eaten handle_directive_with_value(cc, token, result) else: # Unquoted value z = k while z < length: if token_has_fields: # For directives with field names, stop only at whitespace if value[z] in (" ", "\t"): break else: # For other directives, stop at whitespace or comma if value[z] in (" ", "\t", ","): break z += 1 result = value[k:z] # Remove trailing comma if present if result and result[-1] == ",": result = result[:-1] i = z handle_directive_with_value(cc, token, result) else: # Token without value handle_directive_without_value(cc, token) i = j return cc def handle_directive_with_value(cc: CacheControl, token: str, value: str) -> None: """Handle a directive that has a value.""" if token == "max-age": cc.max_age = parse_int_value(value) elif token == "s-maxage": cc.s_maxage = parse_int_value(value) elif token == "max-stale": cc.max_stale = parse_int_value(value) elif token == "min-fresh": cc.min_fresh = parse_int_value(value) elif token == "stale-if-error": cc.stale_if_error = parse_int_value(value) elif token == "stale-while-revalidate": cc.stale_while_revalidate = parse_int_value(value) elif token == "no-cache": # no-cache with field names cc.no_cache = parse_field_names(value) elif token == "private": # private with field names cc.private = parse_field_names(value) else: # Unrecognized directive with value cc.extensions.append(f"{token}={value}") def handle_directive_without_value(cc: CacheControl, token: str) -> None: """Handle a directive that doesn't have a value.""" if token == "max-stale": # max-stale without value means accept any stale response cc.max_stale = 2147483647 # max int32 elif token == "no-cache": cc.no_cache = True elif token == "private": cc.private = True elif token == "no-store": cc.no_store = True elif token == "no-transform": cc.no_transform = True elif token == "only-if-cached": cc.only_if_cached = True elif token == "must-revalidate": cc.must_revalidate = True elif token == "must-understand": cc.must_understand = True elif token == "public": cc.public = True elif token == "proxy-revalidate": cc.proxy_revalidate = True elif token == "immutable": cc.immutable = True else: # Unrecognized directive without value cc.extensions.append(token) def parse_cache_control(value: str | None) -> CacheControl: """ Parse a Cache-Control header from either a request or response. This is the main entry point for parsing. Args: value: The Cache-Control header value Returns: CacheControl object containing all parsed directives Examples: >>> # Response example >>> cc = parse_cache_control("public, max-age=3600, must-revalidate") >>> cc.public True >>> cc.max_age 3600 >>> cc.must_revalidate True >>> # Request example >>> cc = parse_cache_control("max-age=0, no-cache") >>> cc.max_age 0 >>> cc.no_cache True >>> # With field names >>> cc = parse_cache_control('no-cache="Set-Cookie, Authorization"') >>> cc.no_cache ['Set-Cookie', 'Authorization'] >>> # Experimental directives >>> cc = parse_cache_control("immutable, stale-while-revalidate=86400") >>> cc.immutable True >>> cc.stale_while_revalidate 86400 """ if value is None: return CacheControl() return parse(value) karpetrosyan-hishel-fd309b1/hishel/_core/_spec.py000066400000000000000000003150701514113157400221360ustar00rootroot00000000000000from __future__ import annotations import logging import time import uuid from abc import ABC, abstractmethod from dataclasses import dataclass, field, replace from typing import ( TYPE_CHECKING, Any, Dict, Optional, TypeVar, Union, ) from hishel._core._headers import Headers, Range, Vary, parse_cache_control from hishel._core.models import ResponseMetadata from hishel._utils import parse_date, partition if TYPE_CHECKING: from hishel import Entry, Request, Response TState = TypeVar("TState", bound="State") HEURISTICALLY_CACHEABLE_STATUS_CODES = ( 200, 203, 204, 300, 301, 308, 404, 405, 410, 414, 501, ) logger = logging.getLogger("hishel.core.spec") @dataclass class CacheOptions: """ Configuration options for HTTP cache behavior. These options control how the cache interprets and applies RFC 9111 caching rules. All options have sensible defaults that follow the specification. Attributes: ---------- shared : bool Determines whether the cache operates as a shared cache or private cache. RFC 9111 Section 3.5: Authenticated Responses https://www.rfc-editor.org/rfc/rfc9111.html#section-3.5 - Shared cache (True): Acts as a proxy, CDN, or gateway cache serving multiple users. Must respect private directives and Authorization header restrictions. Can use s-maxage directive instead of max-age for shared-specific freshness. - Private cache (False): Acts as a browser or user-agent cache for a single user. Can cache private responses and ignore s-maxage directives. Default: True (shared cache) Examples: -------- >>> # Shared cache (proxy/CDN) >>> options = CacheOptions(shared=True) >>> # Private cache (browser) >>> options = CacheOptions(shared=False) supported_methods : list[str] HTTP methods that are allowed to be cached by this cache implementation. RFC 9111 Section 3, paragraph 2.1: https://www.rfc-editor.org/rfc/rfc9111.html#section-3-2.1.1 "A cache MUST NOT store a response to a request unless: - the request method is understood by the cache" Default: ["GET", "HEAD"] (most commonly cached methods) Examples: -------- >>> # Default: cache GET and HEAD only >>> options = CacheOptions() >>> options.supported_methods ['GET', 'HEAD'] >>> # Cache POST responses (advanced use case) >>> options = CacheOptions(supported_methods=["GET", "HEAD", "POST"]) allow_stale : bool Controls whether stale responses can be served without revalidation. RFC 9111 Section 4.2.4: Serving Stale Responses https://www.rfc-editor.org/rfc/rfc9111.html#section-4.2.4 "A cache MUST NOT generate a stale response unless it is disconnected or doing so is explicitly permitted by the client or origin server (e.g., by the max-stale request directive in Section 5.2.1, extension directives such as those defined in [RFC5861], or configuration in accordance with an out-of-band contract)." Default: False (no stale responses) Examples: -------- >>> # Conservative: never serve stale >>> options = CacheOptions(allow_stale=False) >>> # Permissive: serve stale when allowed >>> options = CacheOptions(allow_stale=True) >>> # Stale-while-revalidate pattern (RFC 5861) >>> # Even with allow_stale=True, directives are respected >>> options = CacheOptions(allow_stale=True) """ shared: bool = True """ When True, the cache operates as a shared cache (proxy/CDN). When False, as a private cache (browser). """ supported_methods: list[str] = field(default_factory=lambda: ["GET", "HEAD"]) """HTTP methods that are allowed to be cached.""" allow_stale: bool = False """When True, stale responses can be served without revalidation.""" @dataclass class State(ABC): options: CacheOptions @abstractmethod def next(self, *args: Any, **kwargs: Any) -> Union["State", None]: raise NotImplementedError("Subclasses must implement this method") def vary_headers_match( original_request: Request, associated_entry: Entry, ) -> bool: """ Determines if request headers match the Vary requirements of a cached response. The Vary header specifies which request headers were used to select the representation. For a cached response to be reusable, all headers listed in Vary must match between the original and new requests. RFC 9111 Section 4.1: Calculating Cache Keys https://www.rfc-editor.org/rfc/rfc9111.html#section-4.1 Parameters: ---------- original_request : Request The new incoming request that we're trying to satisfy associated_entry : Entry A cached request-response entry that might match the new request Returns: ------- bool True if the Vary headers match (or no Vary header exists), False if they don't match or Vary contains "*" RFC 9111 Compliance: ------------------- From RFC 9111 Section 4.1: "When a cache receives a request that can be satisfied by a stored response and that stored response contains a Vary header field, the cache MUST NOT use that stored response without revalidation unless all the presented request header fields nominated by that Vary field value match those fields in the original request (i.e., the request that caused the cached response to be stored)." "The header fields from two requests are defined to match if and only if those in the first request can be transformed to those in the second request by applying any of the following: - adding or removing whitespace - combining multiple header field lines with the same field name - normalizing header field values" "A stored response with a Vary header field value containing a member '*' always fails to match." Examples: -------- >>> # No Vary header - always matches >>> request = Request(headers=Headers({"accept": "application/json"})) >>> response = Response(headers=Headers({})) # No Vary >>> entry = Entry(request=request, response=response) >>> vary_headers_match(request, entry) True >>> # Vary: Accept with matching Accept header >>> request1 = Request(headers=Headers({"accept": "application/json"})) >>> response = Response(headers=Headers({"vary": "Accept"})) >>> entry = Entry(request=request1, response=response) >>> request2 = Request(headers=Headers({"accept": "application/json"})) >>> vary_headers_match(request2, entry) True >>> # Vary: Accept with non-matching Accept header >>> request2 = Request(headers=Headers({"accept": "application/xml"})) >>> vary_headers_match(request2, entry) False >>> # Vary: * always fails >>> response = Response(headers=Headers({"vary": "*"})) >>> entry = Entry(request=request1, response=response) >>> vary_headers_match(request2, entry) False """ # Extract the Vary header from the cached response vary_header = associated_entry.response.headers.get("vary") # If no Vary header exists, any request matches # The response doesn't vary based on request headers if not vary_header: return True # Parse the Vary header value into individual header names vary = Vary.from_value(vary_header) # Check each header name listed in Vary for vary_header in vary.values: # Special case: Vary: * # RFC 9111 Section 4.1: "A stored response with a Vary header field # value containing a member '*' always fails to match." # # Vary: * means the response varies on factors beyond request headers # (e.g., cookies, user agent state, time of day). It can never be matched. if vary_header == "*": return False # Compare the specific header value between original and new request # Both headers must have the same value (or both be absent) if original_request.headers.get(vary_header) != associated_entry.request.headers.get(vary_header): return False # All Vary headers matched return True def get_freshness_lifetime(response: Response, is_cache_shared: bool) -> Optional[int]: """ Calculates the freshness lifetime of a cached response in seconds. The freshness lifetime is the time period during which a cached response can be used without validation. It's determined by explicit directives (max-age, s-maxage, Expires) or heuristically calculated. RFC 9111 Section 4.2.1: Calculating Freshness Lifetime https://www.rfc-editor.org/rfc/rfc9111.html#section-4.2.1 Parameters: ---------- response : Response The cached response to calculate freshness for is_cache_shared : bool True if this is a shared cache (proxy, CDN), False for private cache (browser) Returns: ------- Optional[int] Freshness lifetime in seconds, or None if it cannot be determined RFC 9111 Compliance: ------------------- From RFC 9111 Section 4.2.1: "A cache can calculate the freshness lifetime (denoted as freshness_lifetime) of a response by evaluating the following rules and using the first match: - If the cache is shared and the s-maxage response directive is present, use its value - If the max-age response directive is present, use its value - If the Expires response header field is present, use its value minus the value of the Date response header field - Otherwise, no explicit expiration time is present in the response. A heuristic freshness lifetime might be applicable; see Section 4.2.2" Priority Order: -------------- 1. s-maxage (shared caches only) - highest priority 2. max-age - applies to all caches 3. Expires - Date - legacy but still supported 4. Heuristic freshness - calculated from Last-Modified Examples: -------- >>> # max-age directive >>> response = Response(headers=Headers({"cache-control": "max-age=3600"})) >>> get_freshness_lifetime(response, is_cache_shared=True) 3600 >>> # s-maxage overrides max-age for shared caches >>> response = Response(headers=Headers({ ... "cache-control": "max-age=3600, s-maxage=7200" ... })) >>> get_freshness_lifetime(response, is_cache_shared=True) 7200 >>> get_freshness_lifetime(response, is_cache_shared=False) 3600 """ # Parse the Cache-Control header to extract directives response_cache_control = parse_cache_control(response.headers.get("Cache-Control")) # PRIORITY 1: s-maxage (Shared Cache Only) # RFC 9111 Section 5.2.2.10: s-maxage Response Directive # https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.2.10 # # "The s-maxage response directive indicates that, for a shared cache, # the maximum age specified by this directive overrides the maximum age # specified by either the max-age directive or the Expires header field." # # s-maxage only applies to shared caches (proxies, CDNs) # Private caches (browsers) ignore it and fall through to max-age if is_cache_shared and response_cache_control.s_maxage is not None: return response_cache_control.s_maxage # PRIORITY 2: max-age # RFC 9111 Section 5.2.2.1: max-age Response Directive # https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.2.1 # # "The max-age response directive indicates that the response is to be # considered stale after its age is greater than the specified number # of seconds." # # max-age is the most common caching directive # It applies to both shared and private caches if response_cache_control.max_age is not None: return response_cache_control.max_age # PRIORITY 3: Expires - Date # RFC 9111 Section 5.3: Expires # https://www.rfc-editor.org/rfc/rfc9111.html#section-5.3 # # "The Expires header field gives the date/time after which the response # is considered stale." # # This is an older mechanism (HTTP/1.0) but still supported # Freshness lifetime = Expires - Date if "expires" in response.headers: expires_timestamp = parse_date(response.headers["expires"]) if expires_timestamp is None: raise RuntimeError("Cannot parse Expires header") # pragma: nocover # Get the Date header or use current time as fallback date_timestamp = parse_date(response.headers["date"]) if "date" in response.headers else time.time() if date_timestamp is None: # pragma: nocover # If the Date header is invalid, we use the current time as the date # RFC 9110 Section 6.6.1: Date # "A recipient with a clock that receives a response with an invalid # Date header field value MAY replace that value with the time that # response was received." date_timestamp = time.time() # Calculate freshness lifetime as difference between Expires and Date return int(expires_timestamp - (time.time() if date_timestamp is None else date_timestamp)) # PRIORITY 4: Heuristic Freshness # RFC 9111 Section 4.2.2: Calculating Heuristic Freshness # https://www.rfc-editor.org/rfc/rfc9111.html#section-4.2.2 # # "Since origin servers do not always provide explicit expiration times, # a cache MAY assign a heuristic expiration time when an explicit time # is not specified." # # If no explicit freshness information exists, try to calculate it # heuristically based on the Last-Modified header heuristic_freshness = get_heuristic_freshness(response) if heuristic_freshness is None: return None return get_heuristic_freshness(response) def allowed_stale(response: Response, allow_stale_option: bool) -> bool: """ Determines if a stale response is allowed to be served without revalidation. Stale responses can sometimes be served to improve performance or availability, but only if certain conditions are met and it's explicitly allowed. RFC 9111 Section 4.2.4: Serving Stale Responses https://www.rfc-editor.org/rfc/rfc9111.html#section-4.2.4 Parameters: ---------- response : Response The stale cached response being considered for use allow_stale_option : bool Configuration flag indicating if serving stale is allowed Returns: ------- bool True if the stale response is allowed to be served, False otherwise RFC 9111 Compliance: ------------------- From RFC 9111 Section 4.2.4: "A cache MUST NOT generate a stale response if it is prohibited by an explicit in-protocol directive (e.g., by a no-cache response directive, a must-revalidate response directive, or an applicable s-maxage or proxy-revalidate response directive; see Section 5.2.2)." "A cache MUST NOT generate a stale response unless it is disconnected or doing so is explicitly permitted by the client or origin server (e.g., by the max-stale request directive in Section 5.2.1, extension directives such as those defined in [RFC5861], or configuration in accordance with an out-of-band contract)." Conditions that prohibit serving stale: -------------------------------------- 1. allow_stale_option is False (configuration disallows it) 2. Response has no-cache directive (must always revalidate) 3. Response has must-revalidate directive (must revalidate when stale) 4. Response has proxy-revalidate directive (shared caches must revalidate) 5. Response has s-maxage directive (shared caches must revalidate) Examples: -------- >>> # Stale allowed with permissive configuration >>> response = Response(headers=Headers({"cache-control": "max-age=3600"})) >>> allowed_stale(response, allow_stale_option=True) True >>> # Stale not allowed when configuration disables it >>> allowed_stale(response, allow_stale_option=False) False >>> # must-revalidate prevents serving stale >>> response = Response(headers=Headers({ ... "cache-control": "max-age=3600, must-revalidate" ... })) >>> allowed_stale(response, allow_stale_option=True) False """ # First check: Is serving stale enabled in configuration? # If not, we can't serve stale responses regardless of directives if not allow_stale_option: return False # Parse Cache-Control directives to check for prohibitions response_cache_control = parse_cache_control(response.headers.get("Cache-Control")) # PROHIBITION 1: no-cache directive # RFC 9111 Section 5.2.2.4: no-cache Response Directive # https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.2.4 # # "The no-cache response directive... indicates that the response MUST NOT # be used to satisfy any other request without forwarding it for validation # and receiving a successful response." # # no-cache means the response must ALWAYS be revalidated before use, # even if it's fresh. Stale responses definitely cannot be served. if response_cache_control.no_cache: return False # PROHIBITION 2: must-revalidate directive # RFC 9111 Section 5.2.2.2: must-revalidate Response Directive # https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.2.2 # # "The must-revalidate response directive indicates that once the response # has become stale, a cache MUST NOT reuse that response to satisfy another # request until it has been successfully validated by the origin." # # must-revalidate specifically prohibits serving stale responses # This is used for responses where serving stale content could cause # incorrect operation (e.g., financial transactions) if response_cache_control.must_revalidate: return False # All checks passed - stale response may be served return True def get_heuristic_freshness(response: Response) -> int | None: """ Calculates a heuristic freshness lifetime when no explicit expiration is provided. When a response lacks explicit caching directives (max-age, Expires), caches may assign a heuristic freshness lifetime based on other response characteristics, particularly the Last-Modified header. RFC 9111 Section 4.2.2: Calculating Heuristic Freshness https://www.rfc-editor.org/rfc/rfc9111.html#section-4.2.2 Parameters: ---------- response : Response The response to calculate heuristic freshness for Returns: ------- int | None Heuristic freshness lifetime in seconds, or None if it cannot be calculated RFC 9111 Compliance: ------------------- From RFC 9111 Section 4.2.2: "Since origin servers do not always provide explicit expiration times, a cache MAY assign a heuristic expiration time when an explicit time is not specified, employing algorithms that use other field values (such as the Last-Modified time) to estimate a plausible expiration time. This specification does not provide specific algorithms, but it does impose worst-case constraints on their results." "If the response has a Last-Modified header field, caches are encouraged to use a heuristic expiration value that is no more than some fraction of the interval since that time. A typical setting of this fraction might be 10%." Heuristic Calculation: --------------------- - Freshness = 10% of (now - Last-Modified) - Maximum: 1 week (604,800 seconds) - Minimum: 0 seconds Rationale: --------- If a resource hasn't changed in a long time (old Last-Modified), it's likely stable and can be cached longer. The 10% rule is a conservative heuristic that balances caching benefits with freshness. Examples: -------- >>> # Resource last modified 10 days ago >>> # Heuristic freshness = 10% of 10 days = 1 day >>> last_modified = (datetime.now() - timedelta(days=10)).strftime(...) >>> response = Response(headers=Headers({"last-modified": last_modified})) >>> get_heuristic_freshness(response) 86400 # 1 day in seconds >>> # Resource last modified 100 days ago >>> # Would be 10 days, but capped at 1 week maximum >>> last_modified = (datetime.now() - timedelta(days=100)).strftime(...) >>> response = Response(headers=Headers({"last-modified": last_modified})) >>> get_heuristic_freshness(response) 604800 # 1 week (maximum) >>> # No Last-Modified header >>> response = Response(headers=Headers({})) >>> get_heuristic_freshness(response) None """ # Get the Last-Modified header if present last_modified = response.headers.get("last-modified") if last_modified: # Parse the Last-Modified timestamp last_modified_timestamp = parse_date(last_modified) if last_modified_timestamp is None: # pragma: nocover # Cannot parse the date, cannot calculate heuristic freshness return None # Calculate how long ago the resource was last modified now = time.time() age_since_modification = now - last_modified_timestamp # RFC 9111 recommends 10% of the age since modification # "A typical setting of this fraction might be 10%." heuristic_freshness = int(age_since_modification * 0.1) # Cap at one week maximum # RFC 9111 Section 4.2.2: "Historically, HTTP required the Expires # field value to be no more than a year in the future. While longer # freshness lifetimes are no longer prohibited, extremely large values # have been demonstrated to cause problems." # # We use a conservative 1-week maximum for heuristic freshness ONE_WEEK = 604_800 # 7 days * 24 hours * 60 minutes * 60 seconds return min(ONE_WEEK, heuristic_freshness) # No Last-Modified header, cannot calculate heuristic freshness return None def get_age(response: Response) -> int: """ Calculates the current age of a cached response in seconds. Age represents how old a cached response is - the time since it was generated or last validated by the origin server. This is crucial for determining if a response is still fresh. RFC 9111 Section 4.2.3: Calculating Age https://www.rfc-editor.org/rfc/rfc9111.html#section-4.2.3 Parameters: ---------- response : Response The cached response to calculate age for Returns: ------- int Age of the response in seconds (always >= 0) RFC 9111 Compliance: ------------------- From RFC 9111 Section 4.2.3: "A response's 'age' is the time that has passed since it was generated by, or successfully validated with, the origin server." The full RFC formula accounts for: - apparent_age: Current time minus Date header - age_value: Age header from upstream caches - response_delay: Network latency - resident_time: Time stored in this cache This simplified implementation calculates: age = max(0, now - Date) Where: - now: Current time - Date: Value from the Date response header Fallbacks: --------- - If Date header is missing: age = 0 - If Date header is invalid: age = 0 - If Date is in the future: age = 0 (via max(0, ...)) Note on Accuracy: ---------------- This is a simplified calculation suitable for single-hop caches. A full implementation would consider: - Age header from upstream caches - Request/response timing for latency correction - Clock skew compensation Examples: -------- >>> # Response from 1 hour ago >>> date = (datetime.utcnow() - timedelta(hours=1)).strftime(...) >>> response = Response(headers=Headers({"date": date})) >>> get_age(response) 3600 # 1 hour in seconds >>> # Fresh response (Date = now) >>> date = datetime.utcnow().strftime(...) >>> response = Response(headers=Headers({"date": date})) >>> get_age(response) 0 # or very close to 0 >>> # No Date header >>> response = Response(headers=Headers({})) >>> get_age(response) 0 """ # RFC 9110 Section 6.6.1: Date # https://www.rfc-editor.org/rfc/rfc9110#section-6.6.1 # # "A recipient with a clock that receives a response with an invalid Date # header field value MAY replace that value with the time that response # was received." # # If no Date header exists, we treat the response as having age 0 # This is conservative - it assumes the response is brand new if "date" not in response.headers: return 0 # Parse the Date header date = parse_date(response.headers["date"]) if date is None: # pragma: nocover # Invalid Date header, treat as age 0 return 0 # Calculate apparent age: how long ago was the response generated? now = time.time() apparent_age = max(0, now - date) # Return age as integer seconds # max(0, ...) ensures we never return negative age (e.g., if Date is in future) return int(apparent_age) def make_conditional_request(request: Request, response: Response) -> Request: """ Converts a regular request into a conditional request for validation. Conditional requests use validators (ETag, Last-Modified) to check if a cached response is still valid. If the resource hasn't changed, the server responds with 304 Not Modified, saving bandwidth. RFC 9111 Section 4.3.1: Sending a Validation Request https://www.rfc-editor.org/rfc/rfc9111.html#section-4.3.1 Parameters: ---------- request : Request The original request to make conditional response : Response The cached response containing validators (ETag, Last-Modified) Returns: ------- Request A new request with conditional headers added (If-None-Match, If-Modified-Since) RFC 9111 Compliance: ------------------- From RFC 9111 Section 4.3.1: "When generating a conditional request for validation, a cache... updates that request with one or more precondition header fields. These contain validator metadata sourced from a stored response(s) that has the same URI." "When generating a conditional request for validation, a cache: - MUST send the relevant entity tags (using If-Match, If-None-Match, or If-Range) if the entity tags were provided in the stored response(s) being validated. - SHOULD send the Last-Modified value (using If-Modified-Since) if the request is not for a subrange, a single stored response is being validated, and that response contains a Last-Modified value." Conditional Headers Added: ------------------------- 1. If-None-Match: Added if response has ETag - Asks server: "Send full response only if ETag doesn't match" - 304 response if ETag matches (resource unchanged) - 200 response with content if ETag differs (resource changed) 2. If-Modified-Since: Added if response has Last-Modified - Asks server: "Send full response only if modified after this date" - 304 response if not modified (resource unchanged) - 200 response with content if modified (resource changed) Validator Priority: ------------------ Both validators are sent if available. ETags are more reliable than Last-Modified (1-second granularity), so servers typically check ETag first. Examples: -------- >>> # Request with ETag validator >>> request = Request(method="GET", url="https://example.com/resource") >>> response = Response(headers=Headers({"etag": '"abc123"'})) >>> conditional = make_conditional_request(request, response) >>> conditional.headers["if-none-match"] '"abc123"' >>> # Request with Last-Modified validator >>> response = Response(headers=Headers({ ... "last-modified": "Mon, 01 Jan 2024 00:00:00 GMT" ... })) >>> conditional = make_conditional_request(request, response) >>> conditional.headers["if-modified-since"] 'Mon, 01 Jan 2024 00:00:00 GMT' >>> # Request with both validators >>> response = Response(headers=Headers({ ... "etag": '"abc123"', ... "last-modified": "Mon, 01 Jan 2024 00:00:00 GMT" ... })) >>> conditional = make_conditional_request(request, response) >>> "if-none-match" in conditional.headers True >>> "if-modified-since" in conditional.headers True """ # Extract validators from the cached response # VALIDATOR 1: Last-Modified # RFC 9110 Section 8.8.2: Last-Modified # https://www.rfc-editor.org/rfc/rfc9110#section-8.8.2 # # Last-Modified indicates when the resource was last changed # Used to create If-Modified-Since conditional header if "last-modified" in response.headers: last_modified = response.headers["last-modified"] else: last_modified = None # VALIDATOR 2: ETag (Entity Tag) # RFC 9110 Section 8.8.3: ETag # https://www.rfc-editor.org/rfc/rfc9110#section-8.8.3 # # ETag is an opaque validator that represents a specific version of a resource # More reliable than Last-Modified (no timestamp granularity issues) # Used to create If-None-Match conditional header if "etag" in response.headers: etag = response.headers["etag"] else: etag = None # Build precondition headers dictionary precondition_headers: Dict[str, str] = {} # ADD PRECONDITION 1: If-None-Match (from ETag) # RFC 9110 Section 13.1.2: If-None-Match # https://www.rfc-editor.org/rfc/rfc9110#section-13.1.2 # # "MUST send the relevant entity tags (using If-Match, If-None-Match, or # If-Range) if the entity tags were provided in the stored response(s) # being validated." # # If-None-Match tells the server: "Only send the full response if the # current ETag is different from this one" # # Server responses: # - 304 Not Modified: ETag matches, cached version is still valid # - 200 OK: ETag differs, sends new content if etag is not None: precondition_headers["If-None-Match"] = etag # ADD PRECONDITION 2: If-Modified-Since (from Last-Modified) # RFC 9110 Section 13.1.3: If-Modified-Since # https://www.rfc-editor.org/rfc/rfc9110#section-13.1.3 # # "SHOULD send the Last-Modified value (using If-Modified-Since) if the # request is not for a subrange, a single stored response is being # validated, and that response contains a Last-Modified value." # # If-Modified-Since tells the server: "Only send the full response if the # resource has been modified after this date" # # Server responses: # - 304 Not Modified: Not modified since date, cached version is valid # - 200 OK: Modified since date, sends new content if last_modified: precondition_headers["If-Modified-Since"] = last_modified # Create a new request with the original headers plus precondition headers # The replace() function creates a copy of the request with updated headers return replace( request, headers=Headers( { **request.headers, **precondition_headers, } ), ) def exclude_unstorable_headers(response: Response, is_cache_shared: bool) -> Response: """ Removes headers that must not be stored in the cache. Certain headers are connection-specific or contain sensitive information that should not be cached. This function filters them out before storage. RFC 9111 Section 3.1: Storing Header and Trailer Fields https://www.rfc-editor.org/rfc/rfc9111.html#section-3.1 Parameters: ---------- response : Response The response to filter headers from is_cache_shared : bool True if this is a shared cache (affects private directive handling) Returns: ------- Response A new response with unstorable headers removed RFC 9111 Compliance: ------------------- From RFC 9111 Section 3.1: "Caches MUST include all received response header fields -- including unrecognized ones -- when storing a response; this assures that new HTTP header fields can be successfully deployed. However, the following exceptions are made: - The Connection header field and fields whose names are listed in it are not stored (see Section 7.6.1 of [HTTP]) - Caches MUST NOT store fields defined as being specific to a particular connection or applicable only to a tunnel or gateway, unless the cache was specifically designed to support these fields" Headers Always Excluded: ----------------------- Connection-specific headers (RFC 9110 Section 7.6.1): - Connection - Keep-Alive - Proxy-Connection (non-standard but common) - Transfer-Encoding - Upgrade - TE Hop-by-hop authentication headers: - Proxy-Authenticate - Proxy-Authorization - Proxy-Authentication-Info Headers Conditionally Excluded: ------------------------------- - Fields listed in no-cache directive (always excluded) - Fields listed in private directive (excluded for shared caches only) Examples: -------- >>> # Remove connection-specific headers >>> response = Response(headers=Headers({ ... "cache-control": "max-age=3600", ... "connection": "keep-alive", ... "keep-alive": "timeout=5", ... "content-type": "application/json" ... })) >>> filtered = exclude_unstorable_headers(response, is_cache_shared=True) >>> "connection" in filtered.headers False >>> "content-type" in filtered.headers True >>> # Remove headers listed in no-cache >>> response = Response(headers=Headers({ ... "cache-control": 'no-cache="Set-Cookie"', ... "set-cookie": "session=abc123" ... })) >>> filtered = exclude_unstorable_headers(response, is_cache_shared=True) >>> "set-cookie" in filtered.headers False """ # Initialize set of headers to exclude # These are connection-specific headers that must never be cached # RFC 9110 Section 7.6.1: Connection-Specific Header Fields # https://www.rfc-editor.org/rfc/rfc9110#section-7.6.1 need_to_be_excluded = set( [ "connection", # Connection management "keep-alive", # Connection timeout and max requests "te", # Transfer encoding accepted by client "transfer-encoding", # How the body is encoded for transfer "upgrade", # Protocol upgrade (e.g., WebSocket) "proxy-connection", # Non-standard but widely used "proxy-authenticate", # Proxy authentication challenge "proxy-authentication-info", # Proxy auth additional info "proxy-authorization", # Proxy auth credentials ] ) # Parse Cache-Control to check for no-cache and private directives cache_control = parse_cache_control(response.headers.get("cache-control")) # EXCLUSION RULE 1: no-cache with field names # RFC 9111 Section 5.2.2.4: no-cache Response Directive # https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.2.4 # # "The qualified form of the no-cache response directive, with an argument # that lists one or more field names, indicates that a cache MAY use the # response to satisfy a subsequent request, subject to any other restrictions # on caching, if the listed header fields are excluded from the subsequent # response or the subsequent response has been successfully revalidated with # the origin server." # # Example: Cache-Control: no-cache="Set-Cookie, Set-Cookie2" # Means: Cache the response but exclude Set-Cookie headers from the cache if isinstance(cache_control.no_cache, list): for field in cache_control.no_cache: need_to_be_excluded.add(field.lower()) # EXCLUSION RULE 2: private with field names (shared caches only) # RFC 9111 Section 5.2.2.7: private Response Directive # https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.2.7 # # "If a qualified private response directive is present, with an argument # that lists one or more field names, then only the listed header fields # are limited to a single user: a shared cache MUST NOT store the listed # header fields if they are present in the original response but MAY store # the remainder of the response message without those header fields" # # Example: Cache-Control: private="Authorization" # Means: Shared caches can cache the response but must exclude Authorization if is_cache_shared and isinstance(cache_control.private, list): for field in cache_control.private: need_to_be_excluded.add(field.lower()) # Filter out the excluded headers # Create new Headers dict with only the headers we want to keep new_headers = Headers( {key: value for key, value in response.headers.items() if key.lower() not in need_to_be_excluded} ) # Return a new response with filtered headers return replace( response, headers=new_headers, ) def refresh_response_headers( stored_response: Response, revalidation_response: Response, is_cache_shared: bool, ) -> Response: """ Updates a stored response's headers with fresh metadata from a 304 response. When revalidation succeeds (304 Not Modified), the cached response is still valid but its metadata (Date, Cache-Control, etc.) should be updated with fresh values from the 304 response. RFC 9111 Section 3.2: Updating Stored Header Fields https://www.rfc-editor.org/rfc/rfc9111.html#section-3.2 Parameters: ---------- stored_response : Response The cached response that is being freshened revalidation_response : Response The 304 Not Modified response containing fresh metadata Returns: ------- Response The stored response with updated headers RFC 9111 Compliance: ------------------- From RFC 9111 Section 3.2: "When doing so, the cache MUST add each header field in the provided response to the stored response, replacing field values that are already present, with the following exceptions: - Header fields that provide metadata about the message content and/or the selected representation (e.g., Content-Encoding, Content-Type, Content-Range) MUST NOT be updated unless the response is being stored as a result of successful validation." Update Rules: ------------ 1. Merge headers from 304 response into stored response 2. 304 headers override stored headers (newer metadata) 3. EXCEPT: Content metadata headers are NOT updated - Content-Encoding - Content-Type - Content-Range 4. Remove unstorable headers after merging Rationale for Exceptions: ------------------------ Content-* headers describe the body of the response. A 304 response has no body, so its Content-* headers (if any) don't describe the cached body. We must preserve the original Content-* headers from the cached response. For example: - Cached response: Content-Type: application/json, body is JSON - 304 response: Content-Type: text/plain (this is wrong for the cached body!) - Result: Keep application/json from cached response Examples: -------- >>> # Update Date and Cache-Control, preserve Content-Type >>> stored = Response( ... status_code=200, ... headers=Headers({ ... "date": "Mon, 01 Jan 2024 00:00:00 GMT", ... "cache-control": "max-age=3600", ... "content-type": "application/json" ... }) ... ) >>> revalidation = Response( ... status_code=304, ... headers=Headers({ ... "date": "Mon, 01 Jan 2024 12:00:00 GMT", ... "cache-control": "max-age=7200", ... "content-type": "text/plain" # Should be ignored ... }) ... ) >>> refreshed = refresh_response_headers(stored, revalidation) >>> refreshed.headers["cache-control"] 'max-age=7200' # Updated >>> refreshed.headers["content-type"] 'application/json' # Preserved from stored response """ # Define headers that must NOT be updated from the 304 response # These headers describe the message body/representation # RFC 9111 Section 3.2: "Header fields that provide metadata about the # message content and/or the selected representation... MUST NOT be updated" excluded_headers = set( [ "content-encoding", # How the body is encoded (gzip, br, etc.) "content-type", # MIME type of the body "content-range", # For partial content (206 responses) ] ) # Merge headers: Start with stored response, overlay revalidation response # Headers from revalidation_response override stored_response # EXCEPT for excluded headers (content metadata) new_headers = { **stored_response.headers, # Base: original cached headers **{ key: value for key, value in revalidation_response.headers.items() if key.lower() not in excluded_headers # Skip content metadata }, } # Remove unstorable headers from the final merged headers # This ensures we don't accidentally cache connection-specific headers # that might have been in the 304 response return exclude_unstorable_headers( replace( stored_response, headers=Headers(new_headers), ), is_cache_shared, ) AnyState = Union[ "CacheMiss", "StoreAndUse", "CouldNotBeStored", "FromCache", "NeedToBeUpdated", "NeedRevalidation", "IdleClient", "InvalidateEntries", ] # Defined in https://www.rfc-editor.org/rfc/rfc9110#name-safe-methods SAFE_METHODS = frozenset(["GET", "HEAD", "OPTIONS", "TRACE"]) @dataclass class IdleClient(State): """ Represents the idle state of a client initiating an HTTP request. This is the entry point of the cache state machine. When a client wants to send a request, this state determines whether the request can be satisfied from cache, needs revalidation, or must be forwarded to the origin server (cache miss). State Transitions: ----------------- - CacheMiss: When no suitable cached response exists or the request cannot be cached - FromCache: When a fresh or stale-but-allowed cached response can be used - NeedRevalidation: When a stale cached response exists and must be validated RFC 9111 References: ------------------- - Section 4: Constructing Responses from Caches https://www.rfc-editor.org/rfc/rfc9111.html#section-4 - Section 4.1: Calculating Cache Keys (Vary handling) https://www.rfc-editor.org/rfc/rfc9111.html#section-4.1 - Section 4.2: Freshness https://www.rfc-editor.org/rfc/rfc9111.html#section-4.2 - Section 4.3: Validation https://www.rfc-editor.org/rfc/rfc9111.html#section-4.3 Attributes: ---------- options : CacheOptions Configuration options for cache behavior (inherited from State) """ def next( self, request: Request, associated_entries: list[Entry] ) -> Union["CacheMiss", "FromCache", "NeedRevalidation"]: """ Determines the next state transition based on the request and available cached responses. This method implements the core cache lookup algorithm defined in RFC 9111 Section 4. It evaluates whether a cached response can be reused and transitions to the appropriate next state. Parameters: ---------- request : Request The incoming HTTP request from the client associated_entries : list[Entry] List of request-response entries previously stored in the cache that may match this request. These entries are pre-filtered by cache key (typically URI). Returns: ------- Union[CacheMiss, FromCache, NeedRevalidation] - CacheMiss: No suitable cached response; request must be forwarded to origin - FromCache: A suitable cached response can be returned immediately - NeedRevalidation: A cached response exists but requires validation before use RFC 9111 Compliance: ------------------- This method enforces the requirements from RFC 9111 Section 4, paragraph 1: "When presented with a request, a cache MUST NOT reuse a stored response unless: 1. the presented target URI and that of the stored response match, and 2. the request method associated with the stored response allows it to be used for the presented request, and 3. request header fields nominated by the stored response (if any) match those presented (see Section 4.1), and 4. the stored response does not contain the no-cache directive (Section 5.2.2.4), unless it is successfully validated (Section 4.3), and 5. 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)." Implementation Notes: -------------------- - Range requests always result in a cache miss (simplified behavior) - Unsafe methods (POST, PUT, DELETE, etc.) are written through to origin - Multiple matching responses are sorted by Date header (most recent first) - Age header is updated when serving from cache - Request no-cache directive forces revalidation of cached responses Examples: -------- >>> # Cache miss - no matching responses >>> idle = IdleClient(options=default_options) >>> next_state = idle.next(get_request, []) >>> isinstance(next_state, CacheMiss) True >>> # From cache - fresh response available >>> idle = IdleClient(options=default_options) >>> cached_pair = CompletePair(get_request, fresh_response) >>> next_state = idle.next(get_request, [cached_pair]) >>> isinstance(next_state, FromCache) True >>> # Need revalidation - stale response that cannot be served stale >>> idle = IdleClient(options=default_options) >>> cached_pair = CompletePair(get_request, stale_response) >>> next_state = idle.next(get_request, [cached_pair]) >>> isinstance(next_state, NeedRevalidation) True >>> # Need revalidation - request no-cache forces validation of fresh response >>> idle = IdleClient(options=default_options) >>> no_cache_request = Request( ... method="GET", ... url="https://example.com", ... headers=Headers({"cache-control": "no-cache"}) ... ) >>> cached_pair = CompletePair(no_cache_request, fresh_response) >>> next_state = idle.next(no_cache_request, [cached_pair]) >>> isinstance(next_state, NeedRevalidation) True """ # ============================================================================ # STEP 1: Handle Range Requests # ============================================================================ # RFC 9111 Section 3.3: Storing Incomplete Responses # https://www.rfc-editor.org/rfc/rfc9111.html#section-3.3 # # Range requests are complex and require special handling. For simplicity, # this implementation treats all range requests as cache misses. # A full implementation could store and combine partial responses. request_range = Range.try_from_str(request.headers["range"]) if "range" in request.headers else None if request_range is not None: # Simplified behavior: always forward range requests to origin return CacheMiss(options=self.options, request=request) # ============================================================================ # STEP 2: Handle Unsafe Methods (Write-Through) # ============================================================================ # RFC 9111 Section 4, paragraph 5: # https://www.rfc-editor.org/rfc/rfc9111.html#section-4-5 # # "A cache MUST write through requests with methods that are unsafe # (Section 9.2.1 of [HTTP]) to the origin server; i.e., a cache is not # allowed to generate a reply to such a request before having forwarded # the request and having received a corresponding response." # # Unsafe methods: POST, PUT, DELETE, PATCH, etc. # Safe methods: GET, HEAD, OPTIONS, TRACE if request.method.upper() not in SAFE_METHODS: return CacheMiss(request=request, options=self.options) # pragma: nocover # ============================================================================ # STEP 3: Define Cache Reuse Conditions (RFC 9111 Section 4) # ============================================================================ # The following lambda functions implement the five conditions that must ALL # be satisfied for a cached response to be reusable. # CONDITION 1: URI Matching # RFC 9111 Section 4, paragraph 2.1: # "the presented target URI (Section 7.1 of [HTTP]) and that of the stored # response match" # # The cache key primarily consists of the request URI. Only responses with # matching URIs can be considered for reuse. url_matches = lambda pair: pair.request.url == request.url # noqa: E731 # CONDITION 2: Method Matching # RFC 9111 Section 4, paragraph 2.2: # "the request method associated with the stored response allows it to be # used for the presented request" # # Generally, only GET responses can satisfy GET requests, HEAD responses # for HEAD requests, etc. Some methods (like HEAD) can sometimes be satisfied # by GET responses, but this implementation requires exact matches. method_matches = lambda pair: pair.request.method == request.method # noqa: E731 # CONDITION 3: Vary Header Matching # RFC 9111 Section 4.1: Calculating Cache Keys # https://www.rfc-editor.org/rfc/rfc9111.html#section-4.1 # # "When a cache receives a request that can be satisfied by a stored response # and that stored response contains a Vary header field, the cache MUST NOT # use that stored response without revalidation unless all the presented # request header fields nominated by that Vary field value match those fields # in the original request." # # Example: If response has "Vary: Accept-Encoding", the cached response can # only be used if the new request has the same Accept-Encoding header value. vary_headers_same = lambda pair: vary_headers_match(request, pair) # noqa: E731 # CONDITION 4: No-Cache Directive Handling # RFC 9111 Section 5.2.2.4: no-cache Response Directive # https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.2.4 # # "The no-cache response directive... indicates that the response MUST NOT be # used to satisfy any other request without forwarding it for validation and # receiving a successful response." # # If a cached response has Cache-Control: no-cache, it cannot be reused without # validation, regardless of its freshness. def no_cache_missing(pair: Entry) -> bool: """Check if the cached response lacks the no-cache directive.""" return parse_cache_control(pair.response.headers.get("cache-control")).no_cache is False # CONDITION 5: Freshness or Allowed Stale # RFC 9111 Section 4.2: Freshness # https://www.rfc-editor.org/rfc/rfc9111.html#section-4.2 # # A response can be reused if it is either: # a) Fresh: age < freshness_lifetime # b) Allowed to be served stale: Section 4.2.4 # https://www.rfc-editor.org/rfc/rfc9111.html#section-4.2.4 # # Note: Condition 5.3 (successfully validated) is handled in the # NeedRevalidation state, not here. def fresh_or_allowed_stale(pair: Entry) -> bool: """ Determine if a cached response is fresh or allowed to be served stale. RFC 9111 Section 4.2: "A 'fresh' response is one whose age has not yet exceeded its freshness lifetime. Conversely, a 'stale' response is one where it has." RFC 9111 Section 4.2.4: Serving Stale Responses "A cache MUST NOT generate a stale response unless it is disconnected or doing so is explicitly permitted by the client or origin server." """ freshness_lifetime = get_freshness_lifetime(pair.response, self.options.shared) age = get_age(pair.response) # Check freshness: response_is_fresh = (freshness_lifetime > current_age) is_fresh = False if freshness_lifetime is None else age < freshness_lifetime # Check if stale responses are allowed (e.g., max-stale directive) return is_fresh or allowed_stale(pair.response, allow_stale_option=self.options.allow_stale) # ============================================================================ # STEP 4: Filter Cached Responses by Conditions 1-4 # ============================================================================ # Apply the first four conditions to filter the list of associated pairs. # Condition 5 (freshness) is applied separately to partition responses into # "ready to use" and "needs revalidation" groups. filtered_pairs = [ pair for pair in associated_entries if url_matches(pair) and method_matches(pair) and vary_headers_same(pair) and no_cache_missing(pair) # type: ignore[no-untyped-call] ] # ============================================================================ # STEP 5: Select Most Recent Response # ============================================================================ # RFC 9111 Section 4, paragraph 8: # https://www.rfc-editor.org/rfc/rfc9111.html#section-4-8 # # "When more than one suitable response is stored, a cache MUST use the most # recent one (as determined by the Date header field). It can also forward # the request with 'Cache-Control: max-age=0' or 'Cache-Control: no-cache' # to disambiguate which response to use." # # Sort by Date header in descending order (most recent first). filtered_pairs.sort( key=lambda pair: parse_date( pair.response.headers.get("date", str(int(time.time()))), ) or int(time.time()), reverse=True, ) # ============================================================================ # STEP 6: Partition by Freshness (Condition 5) # ============================================================================ # Separate responses into two groups: # - ready_to_use: Fresh or allowed-stale responses that can be served immediately # - need_revalidation: Stale responses that require validation before serving ready_to_use, need_revalidation = partition(filtered_pairs, fresh_or_allowed_stale) # ============================================================================ # STEP 7: Handle Request no-cache Directive # ============================================================================ # RFC 9111 Section 5.2.1.4: no-cache Request Directive # https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.1.4 # # "The no-cache request directive indicates that a cache MUST NOT use a # stored response to satisfy the request without successful validation on # the origin server." # # When a client sends Cache-Control: no-cache in the request, it's explicitly # requesting that the cache not use any stored response without first validating # it with the origin server. This is different from the response no-cache directive, # which applies to how responses should be cached. request_cache_control = parse_cache_control(request.headers.get("cache-control")) if request_cache_control.no_cache is True: # Move all fresh responses to the revalidation queue # This ensures that even fresh cached responses will be validated # with the origin server via conditional requests (If-None-Match, # If-Modified-Since) before being served to the client. need_revalidation.extend(ready_to_use) ready_to_use = [] # ============================================================================ # STEP 8: Determine Next State Based on Available Responses # ============================================================================ if ready_to_use: # -------------------------------------------------------------------- # Transition to: FromCache # -------------------------------------------------------------------- # We have a fresh (or allowed-stale) response that can be served. # # RFC 9111 Section 4, paragraph 4: # https://www.rfc-editor.org/rfc/rfc9111.html#section-4-4 # # "When a stored response is used to satisfy a request without validation, # a cache MUST generate an Age header field (Section 5.1), replacing any # present in the response with a value equal to the stored response's # current_age; see Section 4.2.3." # # The Age header informs the client how old the cached response is. # Use the most recent response (first in sorted list) selected_pair = ready_to_use[0] # Calculate current age and update the Age header current_age = get_age(selected_pair.response) return FromCache( entry=replace( selected_pair, response=replace( selected_pair.response, headers=Headers( { **selected_pair.response.headers, "age": str(current_age), } ), ), ), options=self.options, ) elif need_revalidation: # -------------------------------------------------------------------- # Transition to: NeedRevalidation # -------------------------------------------------------------------- # We have stale cached response(s) that cannot be served without # validation (e.g., they lack must-revalidate or similar directives). # # RFC 9111 Section 4.3: Validation # https://www.rfc-editor.org/rfc/rfc9111.html#section-4.3 # # "When a cache has one or more stored responses for a requested URI, # but cannot serve any of them (e.g., because they are not fresh, or # one cannot be chosen), it can use the conditional request mechanism # in the forwarded request to give the next inbound server an opportunity # to choose a valid stored response to use, updating the stored metadata # in the process, or to replace the stored response(s) with a new response." # # Convert the request into a conditional request using validators # (ETag, Last-Modified) from the cached response. return NeedRevalidation( request=make_conditional_request(request, need_revalidation[-1].response), revalidating_entries=need_revalidation, options=self.options, original_request=request, ) else: # -------------------------------------------------------------------- # Transition to: CacheMiss # -------------------------------------------------------------------- # No suitable cached responses found. The request must be forwarded # to the origin server. # # This can happen when: # - No responses are cached for this URI # - Cached responses don't match the request (e.g., different Vary headers) # - Cached responses have no-cache directive # - Other conditions prevent cache reuse return CacheMiss( request=request, options=self.options, ) @dataclass class CacheMiss(State): """ Represents a cache miss state where a response must be evaluated for storage. This state is reached when: 1. No suitable cached response exists (from IdleClient) 2. A request must be forwarded to the origin server 3. The origin server's response must be evaluated for cacheability State Transitions: ----------------- - StoreAndUse: Response meets all RFC 9111 storage requirements and will be cached - CouldNotBeStored: Response fails one or more storage requirements and cannot be cached RFC 9111 References: ------------------- - Section 3: Storing Responses in Caches https://www.rfc-editor.org/rfc/rfc9111.html#section-3 - Section 3.5: Authenticated Responses https://www.rfc-editor.org/rfc/rfc9111.html#section-3.5 Attributes: ---------- request : Request The request that caused the cache miss. Note this is a Request object, not an IncompletePair, because this state can be reached from NeedRevalidation where we don't have an incomplete pair. after_revalidation : bool Flag indicating if this cache miss occurred after a revalidation attempt. When True, the response will be marked with hishel_revalidated metadata. options : CacheOptions Configuration options for cache behavior (inherited from State) """ request: Request """ The request that missed the cache. Note that this has a type of Request and not IncompletePair because when moving to this state from `NeedRevalidation` we don't have incomplete pair """ after_revalidation: bool = False """ Indicates whether the cache miss occurred after a revalidation attempt. """ def next(self, response: Response) -> Union["StoreAndUse", "CouldNotBeStored"]: """ Evaluates whether a response can be stored in the cache. This method implements the storage decision algorithm from RFC 9111 Section 3. A response can only be stored if ALL of the following conditions are met: 1. Request method is understood by the cache 2. Response status code is final (not 1xx informational) 3. Cache understands how to handle the response status code 4. No no-store directive is present 5. Private directive allows storage (for shared caches) 6. Authorization is properly handled (for shared caches) 7. Response contains explicit caching information or is heuristically cacheable Parameters: ---------- response : Response The HTTP response received from the origin server pair_id : uuid.UUID Unique identifier for this request-response pair Returns: ------- Union[StoreAndUse, CouldNotBeStored] - StoreAndUse: Response can and will be stored in cache - CouldNotBeStored: Response cannot be stored (fails one or more requirements) RFC 9111 Compliance: ------------------- From RFC 9111 Section 3: "A cache MUST NOT store a response to a request unless: - the request method is understood by the cache; - the response status code is final; - if the response status code is 206 or 304, or the must-understand cache directive is present: the cache understands the response status code; - the no-store cache directive is not present in the response; - if the cache is shared: the private response directive is either not present or allows a shared cache to store a modified response; - if the cache is shared: the Authorization header field is not present in the request or a response directive is present that explicitly allows shared caching; - the response contains at least one of the following: * a public response directive * a private response directive (if cache is not shared) * an Expires header field * a max-age response directive * an s-maxage response directive (if cache is shared) * a status code that is defined as heuristically cacheable" Examples: -------- >>> # Cacheable response >>> cache_miss = CacheMiss(request=get_request, options=default_options) >>> response = Response( ... status_code=200, ... headers=Headers({"cache-control": "max-age=3600"}) ... ) >>> next_state = cache_miss.next(response) >>> isinstance(next_state, StoreAndUse) True >>> # Non-cacheable response (no-store) >>> response = Response( ... status_code=200, ... headers=Headers({"cache-control": "no-store"}) ... ) >>> next_state = cache_miss.next(response) >>> isinstance(next_state, CouldNotBeStored) True """ # ============================================================================ # STEP 2: Parse Cache-Control Directive # ============================================================================ # Extract and parse the Cache-Control header to check caching directives request = self.request response_cache_control = parse_cache_control(response.headers.get("cache-control")) # ============================================================================ # STEP 3: Evaluate Storage Requirements (7 Conditions) # ============================================================================ # All conditions must be True for the response to be storable. # Each condition corresponds to a requirement from RFC 9111 Section 3. # CONDITION 1: Request Method Understanding # RFC 9111 Section 3, paragraph 2.1: # https://www.rfc-editor.org/rfc/rfc9111.html#section-3-2.1.1 # # "the request method is understood by the cache" # # The cache must recognize and support caching for this HTTP method. # Typically, only safe methods (GET, HEAD) are cacheable. # This prevents caching of methods with side effects (POST, PUT, DELETE). method_understood_by_cache = request.method.upper() in self.options.supported_methods # CONDITION 2: Response Status Code is Final # RFC 9111 Section 3, paragraph 2.2: # https://www.rfc-editor.org/rfc/rfc9111.html#section-3-2.2.1 # # "the response status code is final (see Section 15 of [HTTP])" # # 1xx status codes are informational and not final responses. # Only final responses (2xx, 3xx, 4xx, 5xx) can be cached. # Check: status_code % 100 != 1 means not in the 1xx range response_status_code_is_final = response.status_code // 100 != 1 # CONDITION 3: Cache Understands Response Status Code # RFC 9111 Section 3, paragraph 2.3: # https://www.rfc-editor.org/rfc/rfc9111.html#section-3-2.3.1 # # "if the response status code is 206 or 304, or the must-understand cache # directive (see Section 5.2.2.3) is present: the cache understands the # response status code" # # 206 Partial Content: Used for range requests, requires special handling # 304 Not Modified: Used for conditional requests, is not a complete response # # This implementation takes a conservative approach: if the status is 206 or 304, # we mark it as not understood, preventing storage. A full implementation would # handle these specially (304 updates existing cache, 206 stores partial content). if response.status_code in (206, 304): understands_how_to_cache = False else: understands_how_to_cache = True # CONDITION 4: No no-store Directive # RFC 9111 Section 5.2.2.5: no-store Response Directive # https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.2.5 # # "The no-store response directive indicates that a cache MUST NOT store # any part of either the immediate request or the response" # # no-store is the strongest cache prevention directive. When present, # nothing should be stored, regardless of other directives. no_store_is_not_present = not response_cache_control.no_store # CONDITION 5: Private Directive Allows Storing (Shared Cache Only) # RFC 9111 Section 5.2.2.7: private Response Directive # https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.2.7 # # "The unqualified private response directive indicates that a shared cache # MUST NOT store the response" # # For shared caches (proxies, CDNs): # - If private=True, the response is for a single user only # - Shared caches MUST NOT store private responses # # For private caches (browser caches): # - private directive is allowed and encouraged # # Logic: If cache is shared AND response is private, storing is NOT allowed # Therefore: we check (shared cache) AND (private is NOT True) private_directive_allows_storing = not (self.options.shared and response_cache_control.private is True) # CONDITION 6: Authorization Header Handling (Shared Cache Only) # RFC 9111 Section 3.5: Caching Authenticated Responses # https://www.rfc-editor.org/rfc/rfc9111.html#section-3.5 # # "A shared cache MUST NOT use a cached response to a request with an # Authorization header field unless... a response directive is present that # explicitly allows shared caching" # # Requests with Authorization headers often contain user-specific data. # Shared caches must be careful not to serve one user's data to another. has_explicit_directive = ( response_cache_control.public or response_cache_control.s_maxage is not None or response_cache_control.must_revalidate ) can_cache_auth_request = ( not self.options.shared or "authorization" not in request.headers or has_explicit_directive ) # CONDITION 7: Response Contains Required Caching Information # RFC 9111 Section 3, paragraph 2.7: # https://www.rfc-editor.org/rfc/rfc9111.html#section-3-2.7.1 # # "the response contains at least one of the following:..." # # A response must have explicit caching metadata OR be heuristically cacheable. # This ensures we only cache responses that the origin server intended to be cached. contains_required_component = ( # OPTION A: public Directive # RFC 9111 Section 5.2.2.9: # https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.2.9 # "The public response directive indicates that a cache MAY store the response" # Explicitly marks response as cacheable by any cache response_cache_control.public # OPTION B: private Directive (Private Cache Only) # RFC 9111 Section 5.2.2.7: # https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.2.7 # "private... indicates that... a private cache MAY store the response" # For private caches only (not shared caches) or (not self.options.shared and response_cache_control.private) # OPTION C: Expires Header # RFC 9111 Section 5.3: Expires # https://www.rfc-editor.org/rfc/rfc9111.html#section-5.3 # "The Expires header field gives the date/time after which the response # is considered stale" # Explicit expiration time or ("expires" in response.headers) # OPTION D: max-age Directive # RFC 9111 Section 5.2.2.1: max-age Response Directive # https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.2.1 # "The max-age response directive indicates that the response is to be # considered stale after its age is greater than the specified number of seconds" # Most common caching directive or (response_cache_control.max_age is not None) # OPTION E: s-maxage Directive (Shared Cache Only) # RFC 9111 Section 5.2.2.10: s-maxage Response Directive # https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.2.10 # "The s-maxage response directive indicates that, for a shared cache, # the maximum age specified by this directive overrides the maximum age # specified by either the max-age directive or the Expires header field" # Specific to shared caches (proxies, CDNs) or (self.options.shared and response_cache_control.s_maxage is not None) # OPTION F: Heuristically Cacheable Status Code # RFC 9111 Section 4.2.2: Calculating Heuristic Freshness # https://www.rfc-editor.org/rfc/rfc9111.html#section-4.2.2 # "a cache MAY assign a heuristic expiration time when an explicit time # is not specified" # # Certain status codes are defined as "heuristically cacheable": # 200 OK, 203 Non-Authoritative, 204 No Content, 206 Partial Content, # 300 Multiple Choices, 301 Moved Permanently, 308 Permanent Redirect, # 404 Not Found, 405 Method Not Allowed, 410 Gone, # 414 URI Too Long, 501 Not Implemented or response.status_code in HEURISTICALLY_CACHEABLE_STATUS_CODES ) # ============================================================================ # STEP 4: Determine Storage Decision # ============================================================================ # If ANY condition is False, the response cannot be stored if ( not method_understood_by_cache or not response_status_code_is_final or not understands_how_to_cache or not no_store_is_not_present or not private_directive_allows_storing or not can_cache_auth_request or not contains_required_component ): # -------------------------------------------------------------------- # Transition to: CouldNotBeStored # -------------------------------------------------------------------- # One or more storage requirements failed. Log the specific reason # and return a CouldNotBeStored state. # Detailed logging for debugging (only when DEBUG level is enabled) if logger.isEnabledFor(logging.DEBUG): if not method_understood_by_cache: logger.debug( "Cannot store the response because the request method is not understood by the cache. " "See: https://www.rfc-editor.org/rfc/rfc9111.html#section-3-2.1.1" ) elif not response_status_code_is_final: logger.debug( f"Cannot store the response because the response status code ({response.status_code}) " "is not final. See: https://www.rfc-editor.org/rfc/rfc9111.html#section-3-2.2.1" ) elif not understands_how_to_cache: logger.debug( "Cannot store the response because the cache does not understand how to cache the response. " "See: https://www.rfc-editor.org/rfc/rfc9111.html#section-3-2.3.2" ) elif not no_store_is_not_present: logger.debug( "Cannot store the response because the no-store cache directive is present in the response. " "See: https://www.rfc-editor.org/rfc/rfc9111.html#section-3-2.4.1" ) elif not private_directive_allows_storing: logger.debug( "Cannot store the response because the `private` response directive does not " "allow shared caches to store it. See: https://www.rfc-editor.org/rfc/rfc9111.html#section-3-2.5.1" ) elif not can_cache_auth_request: logger.debug( "Cannot store the response because the request contained an Authorization header " "and there was no explicit directive allowing shared caching. " "See: https://www.rfc-editor.org/rfc/rfc9111.html#section-3-5" ) elif not contains_required_component: logger.debug( "Cannot store the response because it does not contain any of the required components. " "See: https://www.rfc-editor.org/rfc/rfc9111.html#section-3-2.7.1" ) return CouldNotBeStored( response=response, options=self.options, after_revalidation=self.after_revalidation, ) # -------------------------------------------------------------------- # Transition to: StoreAndUse # -------------------------------------------------------------------- # All storage requirements are met. The response will be cached. logger.debug("Storing response in cache") # Remove headers that should not be stored # RFC 9111 Section 3.1: Storing Header and Trailer Fields # https://www.rfc-editor.org/rfc/rfc9111.html#section-3.1 # Certain headers (Connection, hop-by-hop headers, etc.) must be excluded cleaned_response = exclude_unstorable_headers(response, self.options.shared) return StoreAndUse( response=cleaned_response, options=self.options, after_revalidation=self.after_revalidation, ) @dataclass class NeedRevalidation(State): """ Represents a state where cached responses require validation before use. This state is reached when: 1. A stale cached response exists (from IdleClient) 2. The cached response cannot be served without validation 3. A conditional request has been sent to the origin server The validation mechanism uses HTTP conditional requests with validators (ETag, Last-Modified) to check if the cached response is still valid. State Transitions: ----------------- - NeedToBeUpdated: 304 response received, cached responses can be freshened - InvalidateEntries + CacheMiss: 2xx/5xx response received, new response must be cached - CacheMiss: No matching responses found during freshening RFC 9111 References: ------------------- - Section 4.3: Validation https://www.rfc-editor.org/rfc/rfc9111.html#section-4.3 - Section 4.3.3: Handling a Validation Response https://www.rfc-editor.org/rfc/rfc9111.html#section-4.3.3 - Section 4.3.4: Freshening Stored Responses https://www.rfc-editor.org/rfc/rfc9111.html#section-4.3.4 Attributes: ---------- request : Request The conditional request that was sent to the server for revalidation. This request contains If-None-Match (from ETag) or If-Modified-Since (from Last-Modified) headers. original_request : Request The original client request (without conditional headers) that initiated this revalidation. This is used when creating new cache entries. revalidating_entries : list[Entry] The cached request-response entries that are being revalidated. These are stale responses that might still be usable if the server confirms they haven't changed (304 response). options : CacheOptions Configuration options for cache behavior (inherited from State) """ request: Request """ The request that was sent to the server for revalidation. """ original_request: Request revalidating_entries: list[Entry] """ The stored entries that the request was sent for revalidation. """ def next( self, revalidation_response: Response ) -> Union["NeedToBeUpdated", "InvalidateEntries", "CacheMiss", "FromCache", "StoreAndUse", "CouldNotBeStored"]: """ Handles the response to a conditional request and determines the next state. This method implements the validation response handling logic from RFC 9111 Section 4.3.3. The behavior depends on the response status code: - 304 Not Modified: Cached responses are still valid, freshen and reuse them - 2xx Success: Cached responses are outdated, use new response - 5xx Server Error: Server cannot validate, use new error response - Other: Unexpected status code (should not happen in normal operation) Parameters: ---------- revalidation_response : Response The HTTP response received from the origin server in response to the conditional request Returns: ------- Union[NeedToBeUpdated, InvalidateEntries, CacheMiss] - NeedToBeUpdated: When 304 response allows cached responses to be freshened - InvalidateEntries: When old responses must be invalidated (wraps next state) - CacheMiss: When no matching responses found or storing new response RFC 9111 Compliance: ------------------- From RFC 9111 Section 4.3.3: "Cache handling of a response to a conditional request depends upon its status code: - A 304 (Not Modified) response status code indicates that the stored response can be updated and reused; see Section 4.3.4. - 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 (see Section 3). - However, if a cache receives a 5xx (Server Error) response while attempting to validate a response, it can either forward this response to the requesting client or act as if the server failed to respond. In the latter case, the cache can send a previously stored response, subject to its constraints on doing so (see Section 4.2.4), or retry the validation request." Implementation Notes: -------------------- - All revalidating pairs except the last are invalidated when receiving 2xx/5xx - The last pair's ID is reused for storing the new response - 5xx responses are treated the same as 2xx (both invalidate and store new response) - A full implementation might serve stale responses on 5xx errors Examples: -------- >>> # 304 Not Modified - freshen cached response >>> need_revalidation = NeedRevalidation( ... request=conditional_request, ... original_request=original_request, ... revalidating_entries=[cached_entry], ... options=default_options ... ) >>> response_304 = Response(status_code=304, headers=Headers({"etag": '"abc123"'})) >>> next_state = need_revalidation.next(response_304) >>> isinstance(next_state, NeedToBeUpdated) True >>> # 200 OK - use new response >>> response_200 = Response(status_code=200, headers=Headers({"cache-control": "max-age=3600"})) >>> next_state = need_revalidation.next(response_200) >>> isinstance(next_state, InvalidateEntries) True """ # ============================================================================ # STEP 1: Handle 304 Not Modified Response # ============================================================================ # RFC 9111 Section 4.3.3, paragraph 1: # https://www.rfc-editor.org/rfc/rfc9111.html#section-4.3.3 # # "A 304 (Not Modified) response status code indicates that the stored # response can be updated and reused; see Section 4.3.4." # # 304 means the cached response is still valid. The server is saying: # "The resource hasn't changed since the version you have cached." # We can freshen the cached response with new metadata (Date, Cache-Control) # from the 304 response and continue using the cached content. if revalidation_response.status_code == 304: return self.freshening_stored_responses(revalidation_response) # ============================================================================ # STEP 2: Handle 2xx Success Response (Full Response) # ============================================================================ # RFC 9111 Section 4.3.3, paragraph 2: # https://www.rfc-editor.org/rfc/rfc9111.html#section-4.3.3 # # "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 # (see Section 3)." # # 2xx responses mean the resource has changed. The server is sending a # complete new response that should replace the cached version. # We must: # 1. Invalidate old cached responses (they're outdated) # 2. Store the new response (if cacheable) # 3. Use the new response to satisfy the request elif revalidation_response.status_code // 100 == 2: # Invalidate all old entries except the last one # The last entry's ID will be reused for the new response return InvalidateEntries( options=self.options, entry_ids=[entry.id for entry in self.revalidating_entries[:-1]], # After invalidation, attempt to cache the new response next_state=CacheMiss( request=self.original_request, options=self.options, after_revalidation=True, # Mark that this occurred during revalidation ).next( revalidation_response, ), ) # ============================================================================ # STEP 3: Handle 5xx Server Error Response # ============================================================================ # RFC 9111 Section 4.3.3, paragraph 3: # https://www.rfc-editor.org/rfc/rfc9111.html#section-4.3.3 # # "However, if a cache receives a 5xx (Server Error) response while # attempting to validate a response, it can either forward this response # to the requesting client or act as if the server failed to respond. # In the latter case, the cache can send a previously stored response, # subject to its constraints on doing so (see Section 4.2.4), or retry # the validation request." # # 5xx errors during revalidation are tricky. The server is having problems, # but we don't know if the cached content is still valid. # # Options per RFC 9111: # A) Forward the error to the client (implemented here) # B) Serve the stale cached response (allowed_stale) # C) Retry the validation request # # This implementation chooses option A: forward the error and store it. # A full implementation might check allowed_stale and serve cached content. elif revalidation_response.status_code // 100 == 5: # Same as 2xx: invalidate old responses and store the error response # This ensures clients see the error rather than potentially stale data return InvalidateEntries( options=self.options, entry_ids=[entry.id for entry in self.revalidating_entries[:-1]], next_state=CacheMiss( request=self.original_request, options=self.options, after_revalidation=True, ).next( revalidation_response, ), ) else: # ============================================================================ # STEP 4: Handle Unexpected Status Codes # ============================================================================ # RFC 9111 does not define behavior for other status codes in this context. # In practice, we need to forward any unexpected responses to the client. return CacheMiss( request=self.revalidating_entries[-1].request, options=self.options, after_revalidation=True, ).next(revalidation_response) def freshening_stored_responses( self, revalidation_response: Response ) -> "NeedToBeUpdated" | "InvalidateEntries" | "CacheMiss": """ Freshens cached responses after receiving a 304 Not Modified response. When the server responds with 304, it means "the resource hasn't changed, but here's updated metadata." This method: 1. Identifies which cached responses match the 304 response 2. Updates their headers with fresh metadata from the 304 3. Invalidates any cached responses that don't match Matching is done using validators in this priority order: 1. Strong ETag (if present and not weak) 2. Last-Modified (if present) 3. Single response assumption (if only one cached response exists) Parameters: ---------- revalidation_response : Response The 304 Not Modified response from the server, containing updated metadata (Date, Cache-Control, ETag, etc.) Returns: ------- Union[NeedToBeUpdated, InvalidateEntries, CacheMiss] - NeedToBeUpdated: When matching responses are found and updated - InvalidateEntries: Wraps NeedToBeUpdated if non-matching responses exist - CacheMiss: When no matching responses are found RFC 9111 Compliance: ------------------- From RFC 9111 Section 4.3.4: "When a cache receives a 304 (Not Modified) response, it needs to identify stored responses that are suitable for updating with the new information provided, and then do so. The initial set of stored responses to update are those that could have been chosen for that request... Then, that initial set of stored responses is further filtered by the first match of: - If the 304 response contains a strong entity tag: the stored responses with the same strong entity tag. - If the 304 response contains a Last-Modified value: the stored responses with the same Last-Modified value. - If there is only a single stored response: that response." Implementation Notes: -------------------- - Weak ETags (starting with "W/") are not used for matching - Only strong ETags provide reliable validation - If no validators match, all responses are invalidated - Multiple responses can be freshened if they share the same validator Examples: -------- >>> # Matching by strong ETag >>> cached_response = Response(headers=Headers({"etag": '"abc123"'})) >>> revalidation_response = Response( ... status_code=304, ... headers=Headers({"etag": '"abc123"', "cache-control": "max-age=3600"}) ... ) >>> # Cached response will be freshened with new Cache-Control >>> # Non-matching ETag >>> cached_response = Response(headers=Headers({"etag": '"old123"'})) >>> revalidation_response = Response( ... status_code=304, ... headers=Headers({"etag": '"new456"'}) ... ) >>> # Cached response will be invalidated (doesn't match) """ # ============================================================================ # STEP 1: Identify Matching Responses Using Validators # ============================================================================ # RFC 9111 Section 4.3.4: Freshening Stored Responses # https://www.rfc-editor.org/rfc/rfc9111.html#section-4.3.4 # # The 304 response tells us "the resource is unchanged", but we need to # figure out WHICH of our cached responses match this confirmation. # # We use validators in priority order: # Priority 1: Strong ETag (most reliable) # Priority 2: Last-Modified timestamp # Priority 3: Single response assumption identified_for_revalidation: list[Entry] # MATCHING STRATEGY 1: Strong ETag # RFC 9110 Section 8.8.3: ETag # https://www.rfc-editor.org/rfc/rfc9110#section-8.8.3 # # "If the 304 response contains a strong entity tag: the stored responses # with the same strong entity tag." # # ETags come in two flavors: # - Strong: "abc123" (exact byte-for-byte match) # - Weak: W/"abc123" (semantically equivalent, but not byte-identical) # # Only strong ETags are reliable for caching decisions. Weak ETags # indicate semantic equivalence but the content might differ slightly # (e.g., gzip compression, whitespace changes). if "etag" in revalidation_response.headers and (not revalidation_response.headers["etag"].startswith("W/")): # Found a strong ETag in the 304 response # Partition cached responses: matching vs non-matching ETags identified_for_revalidation, need_to_be_invalidated = partition( self.revalidating_entries, lambda pair: pair.response.headers.get("etag") == revalidation_response.headers.get("etag"), # type: ignore[no-untyped-call] ) # MATCHING STRATEGY 2: Last-Modified # RFC 9110 Section 8.8.2: Last-Modified # https://www.rfc-editor.org/rfc/rfc9110#section-8.8.2 # # "If the 304 response contains a Last-Modified value: the stored responses # with the same Last-Modified value." # # Last-Modified is a timestamp indicating when the resource was last changed. # It's less precise than ETags (1-second granularity) but widely supported. # If the 304 has a Last-Modified, we can match it against cached responses. elif revalidation_response.headers.get("last-modified"): # Found Last-Modified in the 304 response # Partition cached responses: matching vs non-matching timestamps identified_for_revalidation, need_to_be_invalidated = partition( self.revalidating_entries, lambda pair: pair.response.headers.get("last-modified") == revalidation_response.headers.get("last-modified"), # type: ignore[no-untyped-call] ) # MATCHING STRATEGY 3: Single Response Assumption # RFC 9111 Section 4.3.4: # # "If there is only a single stored response: that response." # # If we only have one cached response and the server says "not modified", # we can safely assume that single response is the one being confirmed. # This handles cases where the server doesn't return validators in the 304. else: if len(self.revalidating_entries) == 1: # Only one cached response - it must be the matching one identified_for_revalidation, need_to_be_invalidated = ( [self.revalidating_entries[0]], [], ) else: # Multiple cached responses but no validators to match them # We cannot determine which (if any) are valid # Conservative approach: invalidate all of them identified_for_revalidation, need_to_be_invalidated = ( [], self.revalidating_entries, ) # ============================================================================ # STEP 2: Update Matching Responses or Create Cache Miss # ============================================================================ # If we found matching responses, freshen them with new metadata. # If we found no matches, treat it as a cache miss. next_state: "NeedToBeUpdated" | "CacheMiss" if identified_for_revalidation: # We found responses that match the 304 confirmation # Update their headers with fresh metadata from the 304 response # # RFC 9111 Section 3.2: Updating Stored Header Fields # https://www.rfc-editor.org/rfc/rfc9111.html#section-3.2 # # "When doing so, the cache MUST add each header field in the provided # response to the stored response, replacing field values that are # already present" # # The refresh_response_headers function handles this header merging # while excluding certain headers that shouldn't be updated # (Content-Encoding, Content-Type, Content-Range). next_state = NeedToBeUpdated( updating_entries=[ replace( pair, response=refresh_response_headers(pair.response, revalidation_response, self.options.shared), ) for pair in identified_for_revalidation ], original_request=self.original_request, options=self.options, ) else: # No matching responses found # This is unusual - the server said "not modified" but we can't figure # out which cached response it's referring to. # Treat this as a cache miss and let the normal flow handle it. next_state = CacheMiss( options=self.options, request=self.original_request, after_revalidation=True, ) # ============================================================================ # STEP 3: Invalidate Non-Matching Responses (if any) # ============================================================================ # If we had multiple cached responses and only some matched, we need to # invalidate the non-matching ones. They're outdated or incorrect. # # For example: # - Cached: Two responses with different ETags # - 304 response: Matches only one ETag # - Action: Update the matching one, invalidate the other if need_to_be_invalidated: # Wrap the next state in an invalidation operation return InvalidateEntries( options=self.options, entry_ids=[entry.id for entry in need_to_be_invalidated], next_state=next_state, ) # No invalidations needed, return the next state directly return next_state class StoreAndUse(State): """ The state that indicates that the response can be stored in the cache and used. Attributes: ---------- response : Response The HTTP response to be stored in the cache. after_revalidation : bool Indicates if the storage is occurring after a revalidation process. """ def __init__( self, response: Response, options: CacheOptions, after_revalidation: bool = False, ) -> None: super().__init__(options) self.response = response self.after_revalidation = after_revalidation response_meta = ResponseMetadata( hishel_created_at=time.time(), hishel_from_cache=False, hishel_revalidated=after_revalidation, hishel_stored=True, ) self.response.metadata.update(response_meta) # type: ignore def next(self) -> None: return None # @dataclass # class CouldNotBeStored(State): # """ # The state that indicates that the response could not be stored in the cache. # """ # response: Response # pair_id: uuid.UUID # def next(self) -> None: # return None # pragma: nocover class CouldNotBeStored(State): """ The state that indicates that the response could not be stored in the cache. Attributes: ---------- response : Response The HTTP response that could not be stored. pair_id : uuid.UUID The unique identifier for the cache pair. after_revalidation : bool Indicates if the storage attempt occurred after a revalidation process. """ def __init__( self, response: Response, options: CacheOptions, after_revalidation: bool = False, ) -> None: super().__init__(options) self.response = response response_meta = ResponseMetadata( hishel_created_at=time.time(), hishel_from_cache=False, hishel_revalidated=after_revalidation, hishel_stored=False, ) self.response.metadata.update(response_meta) # type: ignore def next(self) -> None: return None @dataclass class InvalidateEntries(State): """ The state that represents the deletion of cache entries. """ entry_ids: list[uuid.UUID] next_state: AnyState def next(self) -> AnyState: return self.next_state class FromCache(State): def __init__( self, entry: Entry, options: CacheOptions, after_revalidation: bool = False, ) -> None: super().__init__(options) self.entry = entry self.after_revalidation = after_revalidation response_meta = ResponseMetadata( hishel_created_at=entry.meta.created_at, hishel_from_cache=True, hishel_revalidated=after_revalidation, hishel_stored=False, ) self.entry.response.metadata.update(response_meta) # type: ignore def next(self) -> None: return None @dataclass class NeedToBeUpdated(State): updating_entries: list[Entry] original_request: Request def next(self) -> FromCache: return FromCache(entry=self.updating_entries[-1], options=self.options, after_revalidation=True) karpetrosyan-hishel-fd309b1/hishel/_core/_storages/000077500000000000000000000000001514113157400224535ustar00rootroot00000000000000karpetrosyan-hishel-fd309b1/hishel/_core/_storages/_async_base.py000066400000000000000000000040721514113157400252760ustar00rootroot00000000000000from __future__ import annotations import abc import time import typing as tp import uuid from ..models import Entry, Request, Response class AsyncBaseStorage(abc.ABC): @abc.abstractmethod async def create_entry(self, request: Request, response: Response, key: str, id_: uuid.UUID | None = None) -> Entry: raise NotImplementedError() @abc.abstractmethod async def get_entries(self, key: str) -> tp.List[Entry]: raise NotImplementedError() @abc.abstractmethod async def update_entry( self, id: uuid.UUID, new_entry: tp.Union[Entry, tp.Callable[[Entry], Entry]], ) -> tp.Optional[Entry]: raise NotImplementedError() @abc.abstractmethod async def remove_entry(self, id: uuid.UUID) -> None: raise NotImplementedError() async def close(self) -> None: pass def is_soft_deleted(self, pair: Entry) -> bool: """ Check if a pair is soft deleted based on its metadata. Args: pair: The request pair to check. Returns: True if the pair is soft deleted, False otherwise. """ return pair.meta.deleted_at is not None and pair.meta.deleted_at > 0 def is_safe_to_hard_delete(self, pair: Entry) -> bool: """ Check if a pair is safe to hard delete based on its metadata. If the pair has been soft deleted for more than 1 hour, it is considered safe to hard delete. Args: pair: The request pair to check. Returns: True if the pair is safe to hard delete, False otherwise. """ return bool(pair.meta.deleted_at is not None and (pair.meta.deleted_at + 3600 < time.time())) def mark_pair_as_deleted(self, pair: Entry) -> Entry: """ Mark a pair as soft deleted by setting its deleted_at timestamp. Args: pair: The request pair to mark as deleted. Returns: The updated request pair with the deleted_at timestamp set. """ pair.meta.deleted_at = time.time() return pair karpetrosyan-hishel-fd309b1/hishel/_core/_storages/_async_sqlite.py000066400000000000000000000414601514113157400256670ustar00rootroot00000000000000from __future__ import annotations import time import uuid from dataclasses import replace from pathlib import Path from typing import ( Any, AsyncIterable, AsyncIterator, Callable, List, Optional, Union, ) from hishel._core._storages._async_base import AsyncBaseStorage from hishel._core._storages._packing import pack, unpack from hishel._core.models import ( Entry, EntryMeta, Request, Response, ) from hishel._utils import ensure_cache_dict # Batch cleanup configuration # How often to run cleanup (seconds). Default: 1 hour. BATCH_CLEANUP_INTERVAL = 3600 # How long to wait after storage creation before allowing the first cleanup (seconds) BATCH_CLEANUP_START_DELAY = 5 * 60 # Number of rows to process per chunk when cleaning BATCH_CLEANUP_CHUNK_SIZE = 200 try: import anysqlite from anyio import Lock class AsyncSqliteStorage(AsyncBaseStorage): _COMPLETE_CHUNK_NUMBER = -1 def __init__( self, *, connection: Optional[anysqlite.Connection] = None, database_path: Union[str, Path] = "hishel_cache.db", default_ttl: Optional[float] = None, refresh_ttl_on_access: bool = True, ) -> None: self.connection = connection self.database_path: Path = database_path if isinstance(database_path, Path) else Path(database_path) self.default_ttl = default_ttl self.refresh_ttl_on_access = refresh_ttl_on_access self.last_cleanup = time.time() - BATCH_CLEANUP_INTERVAL + BATCH_CLEANUP_START_DELAY # When this storage instance was created. Used to delay the first cleanup. self._start_time = time.time() self._initialized = False self._lock = Lock() async def _ensure_connection(self) -> anysqlite.Connection: """ Ensure connection is established and database is initialized. Note: This method assumes the caller has already acquired the lock. """ if self.connection is None: # Create cache directory and resolve full path on first connection parent = self.database_path.parent if self.database_path.parent != Path(".") else None full_path = ensure_cache_dict(parent) / self.database_path.name self.connection = await anysqlite.connect(str(full_path)) if not self._initialized: await self._initialize_database() self._initialized = True return self.connection async def _initialize_database(self) -> None: """Initialize the database schema.""" assert self.connection is not None cursor = await self.connection.cursor() # Table for storing request/response pairs await cursor.execute(""" CREATE TABLE IF NOT EXISTS entries ( id BLOB PRIMARY KEY, cache_key BLOB, data BLOB NOT NULL, created_at REAL NOT NULL, deleted_at REAL ) """) # Table for storing response stream chunks only await cursor.execute(""" CREATE TABLE IF NOT EXISTS streams ( entry_id BLOB NOT NULL, chunk_number INTEGER NOT NULL, chunk_data BLOB NOT NULL, PRIMARY KEY (entry_id, chunk_number), FOREIGN KEY (entry_id) REFERENCES entries(id) ON DELETE CASCADE ) """) # Indexes for performance await cursor.execute("CREATE INDEX IF NOT EXISTS idx_entries_deleted_at ON entries(deleted_at)") await cursor.execute("CREATE INDEX IF NOT EXISTS idx_entries_cache_key ON entries(cache_key)") await self.connection.commit() async def create_entry( self, request: Request, response: Response, key: str, id_: uuid.UUID | None = None ) -> Entry: key_bytes = key.encode("utf-8") async with self._lock: connection = await self._ensure_connection() cursor = await connection.cursor() # Create a new entry directly with both request and response pair_id = id_ if id_ is not None else uuid.uuid4() pair_meta = EntryMeta( created_at=time.time(), ) assert isinstance(response.stream, (AsyncIterator, AsyncIterable)) response_with_stream = replace( response, stream=self._save_stream_unlocked(response.stream, pair_id.bytes), ) complete_entry = Entry( id=pair_id, request=request, response=response_with_stream, meta=pair_meta, cache_key=key_bytes, ) # Insert the complete entry into the database await cursor.execute( "INSERT INTO entries (id, cache_key, data, created_at, deleted_at) VALUES (?, ?, ?, ?, ?)", (pair_id.bytes, key_bytes, pack(complete_entry, kind="pair"), pair_meta.created_at, None), ) await connection.commit() return complete_entry async def get_entries(self, key: str) -> List[Entry]: final_pairs: List[Entry] = [] now = time.time() async with self._lock: if now - self.last_cleanup >= BATCH_CLEANUP_INTERVAL: try: await self._batch_cleanup() except Exception: # don't let cleanup prevent reads; failures are non-fatal pass connection = await self._ensure_connection() cursor = await connection.cursor() # Query entries directly by cache_key await cursor.execute( "SELECT id, data FROM entries WHERE cache_key = ?", (key.encode("utf-8"),), ) for row in await cursor.fetchall(): pair_data = unpack(row[1], kind="pair") if pair_data is None: continue # Skip entries without a response (incomplete) if not await self._is_stream_complete(pair_data.id, cursor=cursor): continue # Skip expired entries if await self._is_pair_expired(pair_data, cursor=cursor): continue # Skip soft-deleted entries if self.is_soft_deleted(pair_data): continue final_pairs.append(pair_data) pairs_with_streams: List[Entry] = [] # Only restore response streams from cache for pair in final_pairs: pairs_with_streams.append( replace( pair, response=replace( pair.response, stream=self._stream_data_from_cache(pair.id.bytes), ), ) ) return pairs_with_streams async def update_entry( self, id: uuid.UUID, new_pair: Union[Entry, Callable[[Entry], Entry]], ) -> Optional[Entry]: async with self._lock: connection = await self._ensure_connection() cursor = await connection.cursor() await cursor.execute("SELECT data FROM entries WHERE id = ?", (id.bytes,)) result = await cursor.fetchone() if result is None: return None pair = unpack(result[0], kind="pair") # Skip entries without a response (incomplete) if not isinstance(pair, Entry) or pair.response is None: return None if isinstance(new_pair, Entry): complete_pair = new_pair else: complete_pair = new_pair(pair) if pair.id != complete_pair.id: raise ValueError("Pair ID mismatch") await cursor.execute( "UPDATE entries SET data = ? WHERE id = ?", (pack(complete_pair, kind="pair"), id.bytes), ) if pair.cache_key != complete_pair.cache_key: await cursor.execute( "UPDATE entries SET cache_key = ? WHERE id = ?", (complete_pair.cache_key, complete_pair.id.bytes), ) await connection.commit() return complete_pair async def remove_entry(self, id: uuid.UUID) -> None: async with self._lock: connection = await self._ensure_connection() cursor = await connection.cursor() await cursor.execute("SELECT data FROM entries WHERE id = ?", (id.bytes,)) result = await cursor.fetchone() if result is None: return None pair = unpack(result[0], kind="pair") await self._soft_delete_pair(pair, cursor) await connection.commit() async def close(self) -> None: async with self._lock: if self.connection is not None: await self.connection.close() self.connection = None async def _is_stream_complete(self, pair_id: uuid.UUID, cursor: anysqlite.Cursor) -> bool: # Check if there's a completion marker (chunk_number = -1) for response stream await cursor.execute( "SELECT 1 FROM streams WHERE entry_id = ? AND chunk_number = ? LIMIT 1", (pair_id.bytes, self._COMPLETE_CHUNK_NUMBER), ) return await cursor.fetchone() is not None async def _soft_delete_pair( self, pair: Entry, cursor: anysqlite.Cursor, ) -> None: """ Mark the pair as deleted by setting the deleted_at timestamp. """ marked_pair = self.mark_pair_as_deleted(pair) await cursor.execute( "UPDATE entries SET data = ?, deleted_at = ? WHERE id = ?", ( pack(marked_pair, kind="pair"), marked_pair.meta.deleted_at, pair.id.bytes, ), ) async def _is_pair_expired(self, pair: Entry, cursor: anysqlite.Cursor) -> bool: """ Check if the pair is expired. """ ttl = pair.request.metadata["hishel_ttl"] if "hishel_ttl" in pair.request.metadata else self.default_ttl created_at = pair.meta.created_at if ttl is None: return False return created_at + ttl < time.time() async def _batch_cleanup( self, ) -> None: """ Cleanup expired entries in the database. """ should_mark_as_deleted: List[Entry] = [] should_hard_delete: List[Entry] = [] connection = await self._ensure_connection() cursor = await connection.cursor() # Process entries in chunks to avoid loading the entire table into memory. chunk_size = BATCH_CLEANUP_CHUNK_SIZE offset = 0 while True: await cursor.execute( "SELECT id, data FROM entries LIMIT ? OFFSET ?", (chunk_size, offset), ) rows = await cursor.fetchall() if not rows: break for row in rows: pair = unpack(row[1], kind="pair") if pair is None: continue # expired but not yet soft-deleted if await self._is_pair_expired(pair, cursor) and not self.is_soft_deleted(pair): should_mark_as_deleted.append(pair) # soft-deleted and safe to hard delete, or corrupted pair if (self.is_soft_deleted(pair) and self.is_safe_to_hard_delete(pair)) or await self._is_corrupted( pair, cursor ): should_hard_delete.append(pair) # advance pagination offset += len(rows) for pair in should_mark_as_deleted: await self._soft_delete_pair(pair, cursor) for pair in should_hard_delete: await self._hard_delete_pair(pair, cursor) await connection.commit() async def _is_corrupted(self, pair: Entry, cursor: anysqlite.Cursor) -> bool: # if entry was created more than 1 hour ago and still has no full response data if pair.meta.created_at + 3600 < time.time() and not (await self._is_stream_complete(pair.id, cursor)): return True return False async def _hard_delete_pair(self, pair: Entry, cursor: anysqlite.Cursor) -> None: """ Permanently delete the pair from the database. """ await cursor.execute("DELETE FROM entries WHERE id = ?", (pair.id.bytes,)) # Delete response stream for this entry await self._delete_stream(pair.id.bytes, cursor) async def _delete_stream( self, entry_id: bytes, cursor: anysqlite.Cursor, ) -> None: """ Delete response stream associated with the given entry ID. """ await cursor.execute("DELETE FROM streams WHERE entry_id = ?", (entry_id,)) async def _save_stream_unlocked( self, stream: AsyncIterator[bytes], entry_id: bytes, ) -> AsyncIterator[bytes]: """ Wrapper around an async iterator that also saves the response data to the cache in chunks. Note: This method assumes the caller has already acquired the lock. """ chunk_number = 0 content_length = 0 async for chunk in stream: content_length += len(chunk) async with self._lock: connection = await self._ensure_connection() cursor = await connection.cursor() await cursor.execute( "INSERT INTO streams (entry_id, chunk_number, chunk_data) VALUES (?, ?, ?)", (entry_id, chunk_number, chunk), ) await connection.commit() chunk_number += 1 yield chunk async with self._lock: # Mark end of stream with chunk_number = -1 connection = await self._ensure_connection() cursor = await connection.cursor() await cursor.execute( "INSERT INTO streams (entry_id, chunk_number, chunk_data) VALUES (?, ?, ?)", (entry_id, self._COMPLETE_CHUNK_NUMBER, b""), ) await connection.commit() async def _stream_data_from_cache( self, entry_id: bytes, ) -> AsyncIterator[bytes]: """ Get an async iterator that yields the response stream data from the cache. """ chunk_number = 0 while True: async with self._lock: connection = await self._ensure_connection() cursor = await connection.cursor() await cursor.execute( "SELECT chunk_data FROM streams WHERE entry_id = ? AND chunk_number = ?", (entry_id, chunk_number), ) result = await cursor.fetchone() if result is None: break chunk = result[0] # chunk_number = -1 is the completion marker with empty data if chunk == b"": break yield chunk chunk_number += 1 except ImportError: class AsyncSqliteStorage: # type: ignore[no-redef] def __init__(self, *args: Any, **kwargs: Any) -> None: raise ImportError( "The 'anysqlite' library is required to use the `AsyncSqliteStorage` integration. " "Install hishel with 'pip install hishel[async]'." ) karpetrosyan-hishel-fd309b1/hishel/_core/_storages/_packing.py000066400000000000000000000071741514113157400246110ustar00rootroot00000000000000from __future__ import annotations import uuid from typing import TYPE_CHECKING, Any, Mapping, Optional, Union, overload import msgpack from typing_extensions import Literal, cast from hishel._core._headers import Headers from hishel._core.models import EntryMeta, Request, Response def filter_out_hishel_metadata(data: Mapping[str, Any]) -> dict[str, Any]: return {k: v for k, v in data.items() if not k.startswith("hishel_")} if TYPE_CHECKING: from hishel import Entry @overload def pack( value: "Entry", /, kind: Literal["pair"], ) -> bytes: ... @overload def pack( value: uuid.UUID, /, kind: Literal["entry_db_key_index"], ) -> bytes: ... def pack( value: Union["Entry", uuid.UUID], /, kind: Literal["pair", "entry_db_key_index"], ) -> bytes: from hishel import Entry if kind == "entry_db_key_index": assert isinstance(value, uuid.UUID) return value.bytes elif kind == "pair": assert isinstance(value, Entry) return cast( bytes, msgpack.packb( { "id": value.id.bytes, "request": { "method": value.request.method, "url": value.request.url, "headers": value.request.headers._headers, "extra": filter_out_hishel_metadata(value.request.metadata), }, "response": { "status_code": value.response.status_code, "headers": value.response.headers._headers, "extra": filter_out_hishel_metadata(value.response.metadata), }, "meta": { "created_at": value.meta.created_at, "deleted_at": value.meta.deleted_at, }, "cache_key": value.cache_key, } ), ) assert False, f"Unexpected kind: {kind}" @overload def unpack( value: bytes, /, kind: Literal["pair"], ) -> "Entry": ... @overload def unpack( value: bytes, /, kind: Literal["entry_db_key_index"], ) -> uuid.UUID: ... @overload def unpack( value: Optional[bytes], /, kind: Literal["pair"], ) -> Optional["Entry"]: ... @overload def unpack( value: Optional[bytes], /, kind: Literal["entry_db_key_index"], ) -> Optional[uuid.UUID]: ... def unpack( value: Optional[bytes], /, kind: Literal["pair", "entry_db_key_index"], ) -> Union["Entry", uuid.UUID, None]: from hishel import Entry if value is None: return None if kind == "entry_db_key_index": return uuid.UUID(bytes=value) elif kind == "pair": data = msgpack.unpackb(value) id = uuid.UUID(bytes=data["id"]) return Entry( id=id, request=Request( method=data["request"]["method"], url=data["request"]["url"], headers=Headers(data["request"]["headers"]), metadata=data["request"]["extra"], stream=iter([]), ), response=( Response( status_code=data["response"]["status_code"], headers=Headers(data["response"]["headers"]), metadata=data["response"]["extra"], stream=iter([]), ) ), meta=EntryMeta( created_at=data["meta"]["created_at"], deleted_at=data["meta"]["deleted_at"], ), cache_key=data["cache_key"], ) karpetrosyan-hishel-fd309b1/hishel/_core/_storages/_sync_base.py000066400000000000000000000040331514113157400251320ustar00rootroot00000000000000from __future__ import annotations import abc import time import typing as tp import uuid from ..models import Entry, Request, Response class SyncBaseStorage(abc.ABC): @abc.abstractmethod def create_entry(self, request: Request, response: Response, key: str, id_: uuid.UUID | None = None) -> Entry: raise NotImplementedError() @abc.abstractmethod def get_entries(self, key: str) -> tp.List[Entry]: raise NotImplementedError() @abc.abstractmethod def update_entry( self, id: uuid.UUID, new_entry: tp.Union[Entry, tp.Callable[[Entry], Entry]], ) -> tp.Optional[Entry]: raise NotImplementedError() @abc.abstractmethod def remove_entry(self, id: uuid.UUID) -> None: raise NotImplementedError() def close(self) -> None: pass def is_soft_deleted(self, pair: Entry) -> bool: """ Check if a pair is soft deleted based on its metadata. Args: pair: The request pair to check. Returns: True if the pair is soft deleted, False otherwise. """ return pair.meta.deleted_at is not None and pair.meta.deleted_at > 0 def is_safe_to_hard_delete(self, pair: Entry) -> bool: """ Check if a pair is safe to hard delete based on its metadata. If the pair has been soft deleted for more than 1 hour, it is considered safe to hard delete. Args: pair: The request pair to check. Returns: True if the pair is safe to hard delete, False otherwise. """ return bool(pair.meta.deleted_at is not None and (pair.meta.deleted_at + 3600 < time.time())) def mark_pair_as_deleted(self, pair: Entry) -> Entry: """ Mark a pair as soft deleted by setting its deleted_at timestamp. Args: pair: The request pair to mark as deleted. Returns: The updated request pair with the deleted_at timestamp set. """ pair.meta.deleted_at = time.time() return pair karpetrosyan-hishel-fd309b1/hishel/_core/_storages/_sync_sqlite.py000066400000000000000000000403631514113157400255270ustar00rootroot00000000000000from __future__ import annotations import time import uuid from dataclasses import replace from pathlib import Path from typing import ( Any, Iterable, Iterator, Callable, List, Optional, Union, ) from hishel._core._storages._sync_base import SyncBaseStorage from hishel._core._storages._packing import pack, unpack from hishel._core.models import ( Entry, EntryMeta, Request, Response, ) from hishel._utils import ensure_cache_dict # Batch cleanup configuration # How often to run cleanup (seconds). Default: 1 hour. BATCH_CLEANUP_INTERVAL = 3600 # How long to wait after storage creation before allowing the first cleanup (seconds) BATCH_CLEANUP_START_DELAY = 5 * 60 # Number of rows to process per chunk when cleaning BATCH_CLEANUP_CHUNK_SIZE = 200 try: import sqlite3 from threading import RLock class SyncSqliteStorage(SyncBaseStorage): _COMPLETE_CHUNK_NUMBER = -1 def __init__( self, *, connection: Optional[sqlite3.Connection] = None, database_path: Union[str, Path] = "hishel_cache.db", default_ttl: Optional[float] = None, refresh_ttl_on_access: bool = True, ) -> None: self.connection = connection self.database_path: Path = database_path if isinstance(database_path, Path) else Path(database_path) self.default_ttl = default_ttl self.refresh_ttl_on_access = refresh_ttl_on_access self.last_cleanup = time.time() - BATCH_CLEANUP_INTERVAL + BATCH_CLEANUP_START_DELAY # When this storage instance was created. Used to delay the first cleanup. self._start_time = time.time() self._initialized = False self._lock = RLock() def _ensure_connection(self) -> sqlite3.Connection: """ Ensure connection is established and database is initialized. Note: This method assumes the caller has already acquired the lock. """ if self.connection is None: # Create cache directory and resolve full path on first connection parent = self.database_path.parent if self.database_path.parent != Path(".") else None full_path = ensure_cache_dict(parent) / self.database_path.name self.connection = sqlite3.connect(str(full_path)) if not self._initialized: self._initialize_database() self._initialized = True return self.connection def _initialize_database(self) -> None: """Initialize the database schema.""" assert self.connection is not None cursor = self.connection.cursor() # Table for storing request/response pairs cursor.execute(""" CREATE TABLE IF NOT EXISTS entries ( id BLOB PRIMARY KEY, cache_key BLOB, data BLOB NOT NULL, created_at REAL NOT NULL, deleted_at REAL ) """) # Table for storing response stream chunks only cursor.execute(""" CREATE TABLE IF NOT EXISTS streams ( entry_id BLOB NOT NULL, chunk_number INTEGER NOT NULL, chunk_data BLOB NOT NULL, PRIMARY KEY (entry_id, chunk_number), FOREIGN KEY (entry_id) REFERENCES entries(id) ON DELETE CASCADE ) """) # Indexes for performance cursor.execute("CREATE INDEX IF NOT EXISTS idx_entries_deleted_at ON entries(deleted_at)") cursor.execute("CREATE INDEX IF NOT EXISTS idx_entries_cache_key ON entries(cache_key)") self.connection.commit() def create_entry( self, request: Request, response: Response, key: str, id_: uuid.UUID | None = None ) -> Entry: key_bytes = key.encode("utf-8") with self._lock: connection = self._ensure_connection() cursor = connection.cursor() # Create a new entry directly with both request and response pair_id = id_ if id_ is not None else uuid.uuid4() pair_meta = EntryMeta( created_at=time.time(), ) assert isinstance(response.stream, (Iterator, Iterable)) response_with_stream = replace( response, stream=self._save_stream_unlocked(response.stream, pair_id.bytes), ) complete_entry = Entry( id=pair_id, request=request, response=response_with_stream, meta=pair_meta, cache_key=key_bytes, ) # Insert the complete entry into the database cursor.execute( "INSERT INTO entries (id, cache_key, data, created_at, deleted_at) VALUES (?, ?, ?, ?, ?)", (pair_id.bytes, key_bytes, pack(complete_entry, kind="pair"), pair_meta.created_at, None), ) connection.commit() return complete_entry def get_entries(self, key: str) -> List[Entry]: final_pairs: List[Entry] = [] now = time.time() with self._lock: if now - self.last_cleanup >= BATCH_CLEANUP_INTERVAL: try: self._batch_cleanup() except Exception: # don't let cleanup prevent reads; failures are non-fatal pass connection = self._ensure_connection() cursor = connection.cursor() # Query entries directly by cache_key cursor.execute( "SELECT id, data FROM entries WHERE cache_key = ?", (key.encode("utf-8"),), ) for row in cursor.fetchall(): pair_data = unpack(row[1], kind="pair") if pair_data is None: continue # Skip entries without a response (incomplete) if not self._is_stream_complete(pair_data.id, cursor=cursor): continue # Skip expired entries if self._is_pair_expired(pair_data, cursor=cursor): continue # Skip soft-deleted entries if self.is_soft_deleted(pair_data): continue final_pairs.append(pair_data) pairs_with_streams: List[Entry] = [] # Only restore response streams from cache for pair in final_pairs: pairs_with_streams.append( replace( pair, response=replace( pair.response, stream=self._stream_data_from_cache(pair.id.bytes), ), ) ) return pairs_with_streams def update_entry( self, id: uuid.UUID, new_pair: Union[Entry, Callable[[Entry], Entry]], ) -> Optional[Entry]: with self._lock: connection = self._ensure_connection() cursor = connection.cursor() cursor.execute("SELECT data FROM entries WHERE id = ?", (id.bytes,)) result = cursor.fetchone() if result is None: return None pair = unpack(result[0], kind="pair") # Skip entries without a response (incomplete) if not isinstance(pair, Entry) or pair.response is None: return None if isinstance(new_pair, Entry): complete_pair = new_pair else: complete_pair = new_pair(pair) if pair.id != complete_pair.id: raise ValueError("Pair ID mismatch") cursor.execute( "UPDATE entries SET data = ? WHERE id = ?", (pack(complete_pair, kind="pair"), id.bytes), ) if pair.cache_key != complete_pair.cache_key: cursor.execute( "UPDATE entries SET cache_key = ? WHERE id = ?", (complete_pair.cache_key, complete_pair.id.bytes), ) connection.commit() return complete_pair def remove_entry(self, id: uuid.UUID) -> None: with self._lock: connection = self._ensure_connection() cursor = connection.cursor() cursor.execute("SELECT data FROM entries WHERE id = ?", (id.bytes,)) result = cursor.fetchone() if result is None: return None pair = unpack(result[0], kind="pair") self._soft_delete_pair(pair, cursor) connection.commit() def close(self) -> None: with self._lock: if self.connection is not None: self.connection.close() self.connection = None def _is_stream_complete(self, pair_id: uuid.UUID, cursor: sqlite3.Cursor) -> bool: # Check if there's a completion marker (chunk_number = -1) for response stream cursor.execute( "SELECT 1 FROM streams WHERE entry_id = ? AND chunk_number = ? LIMIT 1", (pair_id.bytes, self._COMPLETE_CHUNK_NUMBER), ) return cursor.fetchone() is not None def _soft_delete_pair( self, pair: Entry, cursor: sqlite3.Cursor, ) -> None: """ Mark the pair as deleted by setting the deleted_at timestamp. """ marked_pair = self.mark_pair_as_deleted(pair) cursor.execute( "UPDATE entries SET data = ?, deleted_at = ? WHERE id = ?", ( pack(marked_pair, kind="pair"), marked_pair.meta.deleted_at, pair.id.bytes, ), ) def _is_pair_expired(self, pair: Entry, cursor: sqlite3.Cursor) -> bool: """ Check if the pair is expired. """ ttl = pair.request.metadata["hishel_ttl"] if "hishel_ttl" in pair.request.metadata else self.default_ttl created_at = pair.meta.created_at if ttl is None: return False return created_at + ttl < time.time() def _batch_cleanup( self, ) -> None: """ Cleanup expired entries in the database. """ should_mark_as_deleted: List[Entry] = [] should_hard_delete: List[Entry] = [] connection = self._ensure_connection() cursor = connection.cursor() # Process entries in chunks to avoid loading the entire table into memory. chunk_size = BATCH_CLEANUP_CHUNK_SIZE offset = 0 while True: cursor.execute( "SELECT id, data FROM entries LIMIT ? OFFSET ?", (chunk_size, offset), ) rows = cursor.fetchall() if not rows: break for row in rows: pair = unpack(row[1], kind="pair") if pair is None: continue # expired but not yet soft-deleted if self._is_pair_expired(pair, cursor) and not self.is_soft_deleted(pair): should_mark_as_deleted.append(pair) # soft-deleted and safe to hard delete, or corrupted pair if (self.is_soft_deleted(pair) and self.is_safe_to_hard_delete(pair)) or self._is_corrupted( pair, cursor ): should_hard_delete.append(pair) # advance pagination offset += len(rows) for pair in should_mark_as_deleted: self._soft_delete_pair(pair, cursor) for pair in should_hard_delete: self._hard_delete_pair(pair, cursor) connection.commit() def _is_corrupted(self, pair: Entry, cursor: sqlite3.Cursor) -> bool: # if entry was created more than 1 hour ago and still has no full response data if pair.meta.created_at + 3600 < time.time() and not (self._is_stream_complete(pair.id, cursor)): return True return False def _hard_delete_pair(self, pair: Entry, cursor: sqlite3.Cursor) -> None: """ Permanently delete the pair from the database. """ cursor.execute("DELETE FROM entries WHERE id = ?", (pair.id.bytes,)) # Delete response stream for this entry self._delete_stream(pair.id.bytes, cursor) def _delete_stream( self, entry_id: bytes, cursor: sqlite3.Cursor, ) -> None: """ Delete response stream associated with the given entry ID. """ cursor.execute("DELETE FROM streams WHERE entry_id = ?", (entry_id,)) def _save_stream_unlocked( self, stream: Iterator[bytes], entry_id: bytes, ) -> Iterator[bytes]: """ Wrapper around an async iterator that also saves the response data to the cache in chunks. Note: This method assumes the caller has already acquired the lock. """ chunk_number = 0 content_length = 0 for chunk in stream: content_length += len(chunk) with self._lock: connection = self._ensure_connection() cursor = connection.cursor() cursor.execute( "INSERT INTO streams (entry_id, chunk_number, chunk_data) VALUES (?, ?, ?)", (entry_id, chunk_number, chunk), ) connection.commit() chunk_number += 1 yield chunk with self._lock: # Mark end of stream with chunk_number = -1 connection = self._ensure_connection() cursor = connection.cursor() cursor.execute( "INSERT INTO streams (entry_id, chunk_number, chunk_data) VALUES (?, ?, ?)", (entry_id, self._COMPLETE_CHUNK_NUMBER, b""), ) connection.commit() def _stream_data_from_cache( self, entry_id: bytes, ) -> Iterator[bytes]: """ Get an async iterator that yields the response stream data from the cache. """ chunk_number = 0 while True: with self._lock: connection = self._ensure_connection() cursor = connection.cursor() cursor.execute( "SELECT chunk_data FROM streams WHERE entry_id = ? AND chunk_number = ?", (entry_id, chunk_number), ) result = cursor.fetchone() if result is None: break chunk = result[0] # chunk_number = -1 is the completion marker with empty data if chunk == b"": break yield chunk chunk_number += 1 except ImportError: class SyncSqliteStorage: # type: ignore[no-redef] def __init__(self, *args: Any, **kwargs: Any) -> None: raise ImportError( "The 'sqlite3' library is required to use the `SyncSqliteStorage` integration. " "Install hishel with 'pip install hishel[async]'." ) karpetrosyan-hishel-fd309b1/hishel/_core/models.py000066400000000000000000000174521514113157400223330ustar00rootroot00000000000000from __future__ import annotations import time import uuid from dataclasses import dataclass, field from typing import ( Any, AsyncIterable, AsyncIterator, Iterable, Iterator, Mapping, Optional, TypedDict, cast, ) from hishel._core._headers import Headers from hishel._utils import make_async_iterator, make_sync_iterator class AnyIterable: def __init__(self, content: bytes | None = None) -> None: self.consumed = False self.content = content def __next__(self) -> bytes: if self.content is not None and not self.consumed: self.consumed = True return self.content raise StopIteration() def __iter__(self) -> Iterator[bytes]: return self async def __anext__(self) -> bytes: if self.content is not None and not self.consumed: self.consumed = True return self.content raise StopAsyncIteration() def __aiter__(self) -> AsyncIterator[bytes]: return self def __eq__(self, value: Any) -> bool: return isinstance(value, AnyIterable) class RequestMetadata(TypedDict, total=False): # All the names here should be prefixed with "hishel_" to avoid collisions with user data hishel_ttl: float | None """When specified, hishel will remove the cached response after specified number of seconds.""" hishel_refresh_ttl_on_access: bool | None """ When True, accessing this entry refreshes its TTL. When False, the TTL remains fixed (default). """ hishel_spec_ignore: bool | None """ When True, hishel will ignore the caching specification for this request. """ hishel_body_key: bool | None """ When True, the request body is included in the cache key generation. This is useful for caching POST or QUERY requests with different bodies. """ def extract_metadata_from_headers( headers: Mapping[str, str], ) -> RequestMetadata: metadata: RequestMetadata = {} if "X-Hishel-Ttl" in headers: try: metadata["hishel_ttl"] = float(headers["X-Hishel-Ttl"]) except ValueError: pass if "X-Hishel-Refresh-Ttl-On-Access" in headers: value = headers["X-Hishel-Refresh-Ttl-On-Access"].lower() if value in ("1", "true", "yes", "on"): metadata["hishel_refresh_ttl_on_access"] = True elif value in ("0", "false", "no", "off"): metadata["hishel_refresh_ttl_on_access"] = False if "X-Hishel-Spec-Ignore" in headers: value = headers["X-Hishel-Spec-Ignore"].lower() if value in ("1", "true", "yes", "on"): metadata["hishel_spec_ignore"] = True elif value in ("0", "false", "no", "off"): metadata["hishel_spec_ignore"] = False return metadata @dataclass class Request: method: str url: str headers: Headers = field(default_factory=lambda: Headers({})) stream: Iterator[bytes] | AsyncIterator[bytes] = field(default_factory=lambda: iter(AnyIterable())) metadata: RequestMetadata | Mapping[str, Any] = field(default_factory=dict) def _iter_stream(self) -> Iterator[bytes]: if hasattr(self, "collected_body"): yield getattr(self, "collected_body") return if isinstance(self.stream, (Iterator, Iterable)): yield from self.stream return raise TypeError("Request stream is not an Iterator") async def _aiter_stream(self) -> AsyncIterator[bytes]: if hasattr(self, "collected_body"): yield getattr(self, "collected_body") return if isinstance(self.stream, (AsyncIterator, AsyncIterable)): async for chunk in self.stream: yield chunk return else: raise TypeError("Request stream is not an AsyncIterator") def read(self) -> bytes: """ Synchronously reads the entire request body without consuming the stream. """ if not isinstance(self.stream, Iterator): raise TypeError("Request stream is not an Iterator") if hasattr(self, "collected_body"): return cast(bytes, getattr(self, "collected_body")) collected = b"".join([chunk for chunk in self.stream]) setattr(self, "collected_body", collected) self.stream = make_sync_iterator([collected]) return collected async def aread(self) -> bytes: """ Asynchronously reads the entire request body without consuming the stream. """ if not isinstance(self.stream, AsyncIterator): raise TypeError("Request stream is not an AsyncIterator") if hasattr(self, "collected_body"): return cast(bytes, getattr(self, "collected_body")) collected = b"".join([chunk async for chunk in self.stream]) setattr(self, "collected_body", collected) self.stream = make_async_iterator([collected]) return collected class ResponseMetadata(TypedDict, total=False): # All the names here should be prefixed with "hishel_" to avoid collisions with user data hishel_from_cache: bool """Indicates whether the response was served from cache.""" hishel_revalidated: bool """Indicates whether the response was revalidated with the origin server.""" hishel_stored: bool """Indicates whether the response was stored in cache.""" hishel_created_at: float """Timestamp when the response was cached.""" @dataclass class Response: status_code: int headers: Headers = field(default_factory=lambda: Headers({})) stream: Iterator[bytes] | AsyncIterator[bytes] = field(default_factory=lambda: iter(AnyIterable())) metadata: ResponseMetadata | Mapping[str, Any] = field(default_factory=dict) def _iter_stream(self) -> Iterator[bytes]: if hasattr(self, "collected_body"): yield getattr(self, "collected_body") return if isinstance(self.stream, Iterator): yield from self.stream return raise TypeError("Response stream is not an Iterator") async def _aiter_stream(self) -> AsyncIterator[bytes]: if hasattr(self, "collected_body"): yield getattr(self, "collected_body") return if isinstance(self.stream, AsyncIterator): async for chunk in self.stream: yield chunk else: raise TypeError("Response stream is not an AsyncIterator") def read(self) -> bytes: """ Synchronously reads the entire request body without consuming the stream. """ if not isinstance(self.stream, Iterator): raise TypeError("Request stream is not an Iterator") if hasattr(self, "collected_body"): return cast(bytes, getattr(self, "collected_body")) collected = b"".join([chunk for chunk in self.stream]) setattr(self, "collected_body", collected) self.stream = make_sync_iterator([collected]) return collected async def aread(self) -> bytes: """ Asynchronously reads the entire request body without consuming the stream. """ if not isinstance(self.stream, AsyncIterator): raise TypeError("Request stream is not an AsyncIterator") if hasattr(self, "collected_body"): return cast(bytes, getattr(self, "collected_body")) collected = b"".join([chunk async for chunk in self.stream]) setattr(self, "collected_body", collected) self.stream = make_async_iterator([collected]) return collected @dataclass class EntryMeta: created_at: float = field(default_factory=time.time) deleted_at: Optional[float] = None @dataclass class Entry: id: uuid.UUID request: Request meta: EntryMeta response: Response cache_key: bytes extra: Mapping[str, Any] = field(default_factory=dict) karpetrosyan-hishel-fd309b1/hishel/_policies.py000066400000000000000000000021571514113157400217230ustar00rootroot00000000000000from __future__ import annotations import abc import typing as t from dataclasses import dataclass, field from typing import Generic from hishel import Request, Response from hishel._core._spec import ( CacheOptions, ) logger = __import__("logging").getLogger(__name__) T = t.TypeVar("T", Request, Response) class CachePolicy(abc.ABC): use_body_key: bool = False """Whether to include request body in cache key calculation.""" class BaseFilter(abc.ABC, Generic[T]): @abc.abstractmethod def needs_body(self) -> bool: pass @abc.abstractmethod def apply(self, item: T, body: bytes | None) -> bool: pass @dataclass class SpecificationPolicy(CachePolicy): """ Caching policy that respects HTTP caching specification. """ cache_options: CacheOptions = field(default_factory=CacheOptions) @dataclass class FilterPolicy(CachePolicy): """ Caching policy that applies user-defined filtering logic. """ request_filters: list[BaseFilter[Request]] = field(default_factory=list) response_filters: list[BaseFilter[Response]] = field(default_factory=list) karpetrosyan-hishel-fd309b1/hishel/_sync_cache.py000066400000000000000000000205641514113157400222150ustar00rootroot00000000000000from __future__ import annotations import hashlib import logging import time from dataclasses import replace from typing import Iterable, Iterator, Awaitable, Callable from typing_extensions import assert_never from hishel import ( AnyState, SyncBaseStorage, SyncSqliteStorage, CacheMiss, CouldNotBeStored, FromCache, IdleClient, NeedRevalidation, NeedToBeUpdated, Request, Response, StoreAndUse, ) from hishel._core._spec import InvalidateEntries, vary_headers_match from hishel._core.models import Entry, ResponseMetadata from hishel._policies import CachePolicy, FilterPolicy, SpecificationPolicy from hishel._utils import make_sync_iterator logger = logging.getLogger("hishel.integrations.clients") class SyncCacheProxy: """ A proxy for HTTP caching in clients. This class is independent of any specific HTTP library and works only with internal models. It delegates request execution to a user-provided callable, making it compatible with any HTTP client. Caching behavior is determined by the policy object. Args: request_sender: Callable that sends HTTP requests and returns responses. storage: Storage backend for cache entries. Defaults to SyncSqliteStorage. policy: Caching policy to use. Can be SpecificationPolicy (respects RFC 9111) or FilterPolicy (user-defined filtering). Defaults to SpecificationPolicy(). """ def __init__( self, request_sender: Callable[[Request], Response], storage: SyncBaseStorage | None = None, policy: CachePolicy | None = None, ) -> None: self.send_request = request_sender self.storage = storage if storage is not None else SyncSqliteStorage() self.policy = policy if policy is not None else SpecificationPolicy() def handle_request(self, request: Request) -> Response: if isinstance(self.policy, FilterPolicy): return self._handle_request_with_filters(request) return self._handle_request_respecting_spec(request) def _get_key_for_request(self, request: Request) -> str: if self.policy.use_body_key or request.metadata.get("hishel_body_key"): assert isinstance(request.stream, (Iterator, Iterable)) collected = b"".join([chunk for chunk in request.stream]) hash_ = hashlib.sha256(collected).hexdigest() request.stream = make_sync_iterator([collected]) return hash_ return hashlib.sha256(str(request.url).encode("utf-8")).hexdigest() def _maybe_refresh_entry_ttl(self, entry: Entry) -> None: if entry.request.metadata.get("hishel_refresh_ttl_on_access"): self.storage.update_entry( entry.id, lambda current_entry: replace( current_entry, meta=replace(current_entry.meta, created_at=time.time()), ), ) def _handle_request_with_filters(self, request: Request) -> Response: assert isinstance(self.policy, FilterPolicy) for request_filter in self.policy.request_filters: if request_filter.needs_body(): body = request.read() if not request_filter.apply(request, body): logger.debug("Request filtered out by request filter") return self.send_request(request) else: if not request_filter.apply(request, None): logger.debug("Request filtered out by request filter") return self.send_request(request) logger.debug("Trying to get cached response ignoring specification") cache_key = self._get_key_for_request(request) entries = self.storage.get_entries(cache_key) logger.debug(f"Found {len(entries)} cached entries for the request") for entry in entries: if ( str(entry.request.url) == str(request.url) and entry.request.method == request.method and vary_headers_match( request, entry, ) ): logger.debug( "Found matching cached response for the request", ) response_meta = ResponseMetadata( hishel_from_cache=True, hishel_created_at=entry.meta.created_at, hishel_revalidated=False, hishel_stored=False, ) entry.response.metadata.update(response_meta) # type: ignore self._maybe_refresh_entry_ttl(entry) return entry.response response = self.send_request(request) for response_filter in self.policy.response_filters: if response_filter.needs_body(): body = response.read() if not response_filter.apply(response, body): logger.debug("Response filtered out by response filter") return response else: if not response_filter.apply(response, None): logger.debug("Response filtered out by response filter") return response response_meta = ResponseMetadata( hishel_from_cache=False, hishel_created_at=time.time(), hishel_revalidated=False, hishel_stored=True, ) response.metadata.update(response_meta) # type: ignore logger.debug("Storing response in cache ignoring specification") entry = self.storage.create_entry( request, response, cache_key, ) return entry.response def _handle_request_respecting_spec(self, request: Request) -> Response: assert isinstance(self.policy, SpecificationPolicy) state: AnyState = IdleClient(options=self.policy.cache_options) while state: logger.debug(f"Handling state: {state.__class__.__name__}") if isinstance(state, IdleClient): state = self._handle_idle_state(state, request) elif isinstance(state, CacheMiss): state = self._handle_cache_miss(state) elif isinstance(state, StoreAndUse): return self._handle_store_and_use(state, request) elif isinstance(state, CouldNotBeStored): return state.response elif isinstance(state, NeedRevalidation): state = self._handle_revalidation(state) elif isinstance(state, FromCache): self._maybe_refresh_entry_ttl(state.entry) return state.entry.response elif isinstance(state, NeedToBeUpdated): state = self._handle_update(state) elif isinstance(state, InvalidateEntries): state = self._handle_invalidate_entries(state) else: assert_never(state) raise RuntimeError("Unreachable") def _handle_idle_state(self, state: IdleClient, request: Request) -> AnyState: stored_entries = self.storage.get_entries(self._get_key_for_request(request)) return state.next(request, stored_entries) def _handle_cache_miss(self, state: CacheMiss) -> AnyState: response = self.send_request(state.request) return state.next(response) def _handle_store_and_use(self, state: StoreAndUse, request: Request) -> Response: entry = self.storage.create_entry( request, state.response, self._get_key_for_request(request), ) return entry.response def _handle_revalidation(self, state: NeedRevalidation) -> AnyState: revalidation_response = self.send_request(state.request) return state.next(revalidation_response) def _handle_update(self, state: NeedToBeUpdated) -> AnyState: for updating_entry in state.updating_entries: self.storage.update_entry( updating_entry.id, lambda existing_entry: replace( existing_entry, response=replace(existing_entry.response, headers=updating_entry.response.headers), ), ) return state.next() def _handle_invalidate_entries(self, state: InvalidateEntries) -> AnyState: for entry_id in state.entry_ids: self.storage.remove_entry(entry_id) return state.next() karpetrosyan-hishel-fd309b1/hishel/_sync_httpx.py000066400000000000000000000174201514113157400223160ustar00rootroot00000000000000from __future__ import annotations import ssl import typing as t from typing import ( Iterable, Iterator, Iterator, Union, cast, overload, ) from httpx import RequestNotRead from hishel import SyncCacheProxy, Headers, Request, Response from hishel._core._storages._sync_base import SyncBaseStorage from hishel._core.models import RequestMetadata, extract_metadata_from_headers from hishel._policies import CachePolicy from hishel._utils import ( filter_mapping, make_sync_iterator, ) try: import httpx except ImportError as e: raise ImportError( "httpx is required to use hishel.httpx module. " "Please install hishel with the 'httpx' extra, " "e.g., 'pip install hishel[httpx]'." ) from e SOCKET_OPTION = t.Union[ t.Tuple[int, int, int], t.Tuple[int, int, t.Union[bytes, bytearray]], t.Tuple[int, int, None, int], ] # 128 KB CHUNK_SIZE = 131072 @overload def _internal_to_httpx( value: Request, ) -> httpx.Request: ... @overload def _internal_to_httpx( value: Response, ) -> httpx.Response: ... def _internal_to_httpx( value: Union[Request, Response], ) -> Union[httpx.Request, httpx.Response]: """ Convert internal Request/Response to httpx.Request/httpx.Response. """ if isinstance(value, Request): return httpx.Request( method=value.method, url=value.url, headers=value.headers, stream=_IteratorStream(value._iter_stream()), extensions=value.metadata, ) elif isinstance(value, Response): return httpx.Response( status_code=value.status_code, headers=value.headers, stream=_IteratorStream(value._iter_stream()), extensions=value.metadata, ) @overload def _httpx_to_internal( value: httpx.Request, ) -> Request: ... @overload def _httpx_to_internal( value: httpx.Response, ) -> Response: ... def _httpx_to_internal( value: Union[httpx.Request, httpx.Response], ) -> Union[Request, Response]: """ Convert httpx.Request/httpx.Response to internal Request/Response. """ headers = Headers( filter_mapping( Headers({key: value for key, value in value.headers.items()}), ["Transfer-Encoding"], ) ) if isinstance(value, httpx.Request): extension_metadata = RequestMetadata( hishel_refresh_ttl_on_access=value.extensions.get("hishel_refresh_ttl_on_access"), hishel_ttl=value.extensions.get("hishel_ttl"), hishel_spec_ignore=value.extensions.get("hishel_spec_ignore"), hishel_body_key=value.extensions.get("hishel_body_key"), ) headers_metadata = extract_metadata_from_headers(value.headers) for key, val in extension_metadata.items(): if key in value.extensions: headers_metadata[key] = val # type: ignore try: stream = make_sync_iterator([value.content]) except RequestNotRead: stream = cast(Iterator[bytes], value.stream) return Request( method=value.method, url=str(value.url), headers=headers, stream=stream, metadata=headers_metadata, ) elif isinstance(value, httpx.Response): stream = ( make_sync_iterator([value.content]) if value.is_stream_consumed else value.iter_raw(chunk_size=CHUNK_SIZE) ) if value.is_stream_consumed and "content-encoding" in value.headers: # If the stream was consumed and we don't know about # the original data and its size, fix the Content-Length # header and remove Content-Encoding so we can recreate it later properly. headers = Headers( { **filter_mapping( headers, ["content-encoding"], ), "content-length": str(len(value.content)), } ) return Response( status_code=value.status_code, headers=headers, stream=stream, metadata={}, ) class _IteratorStream(httpx.SyncByteStream, httpx.AsyncByteStream): def __init__(self, iterator: Iterator[bytes] | Iterator[bytes]) -> None: self.iterator = iterator def __iter__(self) -> Iterator[bytes]: assert isinstance(self.iterator, (Iterator, Iterable)) for chunk in self.iterator: yield chunk class SyncCacheTransport(httpx.BaseTransport): def __init__( self, next_transport: httpx.BaseTransport, storage: SyncBaseStorage | None = None, policy: CachePolicy | None = None, ) -> None: self.next_transport = next_transport self._cache_proxy: SyncCacheProxy = SyncCacheProxy( request_sender=self.request_sender, storage=storage, policy=policy, ) self.storage = self._cache_proxy.storage def handle_request( self, request: httpx.Request, ) -> httpx.Response: internal_request = _httpx_to_internal(request) internal_response = self._cache_proxy.handle_request(internal_request) response = _internal_to_httpx(internal_response) return response def close(self) -> None: self.next_transport.close() self.storage.close() super().close() def request_sender(self, request: Request) -> Response: httpx_request = _internal_to_httpx(request) httpx_response = self.next_transport.handle_request(httpx_request) if httpx_response.status_code == 304: # 304 should not have a body, but we read it to ensure we'll not let the stream unconsumed httpx_response.read() return _httpx_to_internal(httpx_response) class SyncCacheClient(httpx.Client): def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: self.storage: SyncBaseStorage | None = kwargs.pop("storage", None) self.policy: CachePolicy | None = kwargs.pop("policy", None) super().__init__(*args, **kwargs) def _init_transport( self, verify: ssl.SSLContext | str | bool = True, cert: t.Union[str, t.Tuple[str, str], t.Tuple[str, str, str], None] = None, trust_env: bool = True, http1: bool = True, http2: bool = False, limits: httpx.Limits = httpx.Limits(max_connections=100, max_keepalive_connections=20), transport: httpx.BaseTransport | None = None, **kwargs: t.Any, ) -> httpx.BaseTransport: if transport is not None: return transport return SyncCacheTransport( next_transport=httpx.HTTPTransport( verify=verify, cert=cert, trust_env=trust_env, http1=http1, http2=http2, limits=limits, ), storage=self.storage, policy=self.policy, ) def _init_proxy_transport( self, proxy: httpx.Proxy, verify: ssl.SSLContext | str | bool = True, cert: t.Union[str, t.Tuple[str, str], t.Tuple[str, str, str], None] = None, trust_env: bool = True, http1: bool = True, http2: bool = False, limits: httpx.Limits = httpx.Limits(max_connections=100, max_keepalive_connections=20), **kwargs: t.Any, ) -> httpx.BaseTransport: return SyncCacheTransport( next_transport=httpx.HTTPTransport( verify=verify, cert=cert, trust_env=trust_env, http1=http1, http2=http2, limits=limits, proxy=proxy, ), storage=self.storage, policy=self.policy, ) karpetrosyan-hishel-fd309b1/hishel/_utils.py000066400000000000000000000073751514113157400212630ustar00rootroot00000000000000from __future__ import annotations import calendar import time import typing as tp from email.utils import formatdate, parsedate_tz from pathlib import Path from typing import AsyncIterator, Iterable, Iterator HEADERS_ENCODING = "iso-8859-1" T = tp.TypeVar("T") 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 def sleep(seconds: tp.Union[int, float]) -> None: time.sleep(seconds) def partition(iterable: tp.Iterable[T], predicate: tp.Callable[[T], bool]) -> tp.Tuple[tp.List[T], tp.List[T]]: """ Partition an iterable into two lists: one for matching items and one for non-matching items. Args: iterable (tp.Iterable[T]): The input iterable to partition. predicate (tp.Callable[[T], bool]): A function that evaluates each item in the iterable. Returns: tp.Tuple[tp.List[T], tp.List[T]]: A tuple containing two lists: the first for matching items, and the second for non-matching items. Example: ``` iterable = [1, 2, 3, 4, 5] is_even = lambda x: x % 2 == 0 evens, odds = partition(iterable, is_even) ``` """ matching, non_matching = [], [] for item in iterable: if predicate(item): matching.append(item) else: non_matching.append(item) return matching, non_matching async def make_async_iterator( iterable: Iterable[bytes], ) -> AsyncIterator[bytes]: for item in iterable: yield item def filter_mapping(mapping: tp.Mapping[str, T], keys_to_exclude: tp.Iterable[str]) -> tp.Dict[str, T]: """ Filter out specified keys from a string-keyed mapping using case-insensitive comparison. Args: mapping: The input mapping with string keys to filter. keys_to_exclude: An iterable of string keys to exclude (case-insensitive). Returns: A new dictionary with the specified keys excluded. Example: ```python original = {'a': 1, 'B': 2, 'c': 3} filtered = filter_mapping(original, ['b']) # filtered will be {'a': 1, 'c': 3} ``` """ exclude_set = {k.lower() for k in keys_to_exclude} return {k: v for k, v in mapping.items() if k.lower() not in exclude_set} def make_sync_iterator(iterable: Iterable[bytes]) -> Iterator[bytes]: for item in iterable: yield item def snake_to_header(text: str) -> str: """ Convert snake_case string to Header-Case format. Args: text: Snake case string (e.g., "hishel_ttl") Returns: Header case string (e.g., "X-Hishel-Ttl") Examples: >>> snake_to_header("hishel_ttl") 'X-Hishel-Ttl' >>> snake_to_header("cache_control") 'X-Cache-Control' >>> snake_to_header("content_type") 'X-Content-Type' """ # Split by underscore, capitalize each word, join with dash, add X- prefix return "X-" + "-".join(word.capitalize() for word in text.split("_")) def ensure_cache_dict(base_path: Path | None = None) -> Path: _base_path = base_path if base_path is not None else Path(".cache/hishel") _gitignore_file = _base_path / ".gitignore" _base_path.mkdir(parents=True, exist_ok=True) if not _gitignore_file.is_file(): with open(_gitignore_file, "w", encoding="utf-8") as f: f.write("# Automatically created by Hishel\n*") return _base_path def generate_http_date() -> str: """ Generate a Date header value for HTTP responses. Returns date in RFC 1123 format (required by HTTP/1.1). Example output: 'Sun, 26 Oct 2025 12:34:56 GMT' """ return formatdate(timeval=None, localtime=False, usegmt=True) karpetrosyan-hishel-fd309b1/hishel/asgi.py000066400000000000000000000352261514113157400207030ustar00rootroot00000000000000from __future__ import annotations import logging import typing as t from email.utils import formatdate from typing import AsyncIterator from hishel import AsyncBaseStorage, Headers, Request, Response from hishel._async_cache import AsyncCacheProxy from hishel._policies import CachePolicy from hishel._utils import filter_mapping # Configure logger for this module logger = logging.getLogger(__name__) class _ASGIScope(t.TypedDict, total=False): """ASGI HTTP scope type.""" type: str asgi: dict[str, str] http_version: str method: str scheme: str path: str query_string: bytes root_path: str headers: list[tuple[bytes, bytes]] server: tuple[str, int | None] | None client: tuple[str, int] | None state: dict[str, t.Any] extensions: dict[str, t.Any] _Scope = _ASGIScope _Receive = t.Callable[[], t.Awaitable[dict[str, t.Any]]] _Send = t.Callable[[dict[str, t.Any]], t.Awaitable[None]] _ASGIApp = t.Callable[[_Scope, _Receive, _Send], t.Awaitable[None]] class ASGICacheMiddleware: """ ASGI middleware that provides HTTP caching capabilities. This middleware intercepts HTTP requests and responses, caching them according to HTTP caching specifications (RFC 9111) or custom rules. The middleware uses async iterators for request and response bodies, ensuring memory-efficient streaming without loading entire payloads into memory. This is particularly important for large file uploads or downloads. This implementation is thread-safe by creating a new cache proxy for each request with closures that capture the request context. Args: app: The ASGI application to wrap. storage: The storage backend to use for caching. Defaults to AsyncSqliteStorage. policy: Caching policy to use. Can be SpecificationPolicy (respects RFC 9111) or FilterPolicy (user-defined filtering). Defaults to SpecificationPolicy(). Example: ```python from hishel.asgi import ASGICacheMiddleware from hishel import AsyncSqliteStorage from hishel._policies import SpecificationPolicy, CacheOptions # Wrap your ASGI app app = ASGICacheMiddleware( app=my_asgi_app, storage=AsyncSqliteStorage(), policy=SpecificationPolicy(cache_options=CacheOptions()), ) ``` """ def __init__( self, app: _ASGIApp, storage: AsyncBaseStorage | None = None, policy: CachePolicy | None = None, ) -> None: self.app = app self.storage = storage self._policy = policy logger.info( "Initialized ASGICacheMiddleware with storage=%s, policy=%s", type(storage).__name__ if storage else "None", type(policy).__name__ if policy else "None", ) async def __call__(self, scope: _Scope, receive: _Receive, send: _Send) -> None: """ Handle an ASGI request. Args: scope: The ASGI scope dictionary. receive: The ASGI receive callable. send: The ASGI send callable. """ # Only handle HTTP requests if scope["type"] != "http": logger.debug("Skipping non-HTTP request: type=%s", scope["type"]) await self.app(scope, receive, send) return method = scope.get("method", "UNKNOWN") path = scope.get("path", "/") query_string = scope.get("query_string", b"").decode("latin1") full_path = f"{path}?{query_string}" if query_string else path logger.debug("Incoming HTTP request: method=%s path=%s", method, full_path) try: # Create a closure that captures scope and receive for this specific request # This makes the code thread-safe by avoiding shared instance state async def send_request_to_app(request: Request) -> Response: """ Send a request to the wrapped ASGI application and return the response. This closure captures 'scope' and 'receive' from the outer function scope. """ logger.debug("Sending request to wrapped application: url=%s", request.url) # Create a buffered receive callable that replays the request body from the stream body_iterator = request._aiter_stream() body_exhausted = False bytes_received = 0 async def inner_receive() -> dict[str, t.Any]: nonlocal body_exhausted, bytes_received if body_exhausted: return {"type": "http.disconnect"} try: chunk = await body_iterator.__anext__() bytes_received += len(chunk) logger.debug("Received request body chunk: size=%d bytes", len(chunk)) return { "type": "http.request", "body": chunk, "more_body": True, } except StopAsyncIteration: body_exhausted = True logger.debug( "Request body fully consumed: total_bytes=%d", bytes_received, ) return { "type": "http.request", "body": b"", "more_body": False, } # Collect response from the app response_started = False status_code = 200 response_headers: list[tuple[bytes, bytes]] = [] response_body_chunks: list[bytes] = [] bytes_sent = 0 async def inner_send(message: dict[str, t.Any]) -> None: nonlocal response_started, status_code, response_headers, bytes_sent if message["type"] == "http.response.start": response_started = True status_code = message["status"] response_headers = message.get("headers", []) logger.debug("Application response started: status=%d", status_code) elif message["type"] == "http.response.body": body_chunk = message.get("body", b"") if body_chunk: response_body_chunks.append(body_chunk) bytes_sent += len(body_chunk) logger.debug( "Received response body chunk: size=%d bytes", len(body_chunk), ) try: # Call the wrapped application with captured scope await self.app(scope, inner_receive, inner_send) logger.info( "Application response complete: status=%d total_bytes=%d chunks=%d", status_code, bytes_sent, len(response_body_chunks), ) except Exception as e: logger.error( "Error calling wrapped application: url=%s error=%s", request.url, str(e), exc_info=True, ) raise # Convert to internal Response headers_dict = {key.decode("latin1"): value.decode("latin1") for key, value in response_headers} # Add Date header if not present if not any(key.lower() == "date" for key in headers_dict.keys()): date_header = formatdate(timeval=None, localtime=False, usegmt=True) headers_dict["Date"] = date_header logger.debug("Added Date header to response: %s", date_header) async def response_stream() -> AsyncIterator[bytes]: for chunk in response_body_chunks: yield chunk return Response( status_code=status_code, headers=Headers(filter_mapping(headers_dict, ["Transfer-Encoding"])), stream=response_stream(), metadata={}, ) # Create a new cache proxy for this request with the closure # This ensures complete isolation between concurrent requests cache_proxy = AsyncCacheProxy( request_sender=send_request_to_app, storage=self.storage, policy=self._policy, ) # Convert ASGI request to internal Request (using async iterator, not reading into memory) request = self._asgi_to_internal_request(scope, receive) logger.debug("Converted ASGI request to internal format: url=%s", request.url) # Handle request through cache proxy logger.debug("Handling request through cache proxy") response = await cache_proxy.handle_request(request) logger.info( "Request processed: method=%s path=%s status=%d", method, full_path, response.status_code, ) # Send the cached or fresh response await self._send_internal_response(response, send) logger.debug("Response sent successfully") except Exception as e: logger.error( "Error processing request: method=%s path=%s error=%s", method, full_path, str(e), exc_info=True, ) raise def _asgi_to_internal_request(self, scope: _Scope, receive: _Receive) -> Request: """ Convert an ASGI HTTP scope to an internal Request object. Args: scope: The ASGI scope dictionary. receive: The ASGI receive callable. Returns: The internal Request object. """ # Build URL scheme = scope.get("scheme", "http") server = scope.get("server") if server is None: server = ("localhost", 80) logger.debug("No server info in scope, using default: localhost:80") host = server[0] port = server[1] if server[1] is not None else (443 if scheme == "https" else 80) # Add port to host if non-standard if (scheme == "http" and port != 80) or (scheme == "https" and port != 443): host = f"{host}:{port}" path = scope.get("path", "/") query_string = scope.get("query_string", b"") if query_string: path = f"{path}?{query_string.decode('latin1')}" url = f"{scheme}://{host}{path}" method = scope.get("method", "GET") # Extract headers headers_dict = {key.decode("latin1"): value.decode("latin1") for key, value in scope.get("headers", [])} logger.debug( "Building internal request: method=%s url=%s headers_count=%d", method, url, len(headers_dict), ) # Create async iterator for request body that reads from ASGI receive async def request_stream() -> AsyncIterator[bytes]: while True: message = await receive() if message["type"] == "http.request": body = message.get("body", b"") if body: yield body if not message.get("more_body", False): break elif message["type"] == "http.disconnect": logger.debug("Client disconnected during request body streaming") break return Request( method=method, url=url, headers=Headers(headers_dict), stream=request_stream(), # Metadatas don't make sense in ASGI scope, so we leave it empty metadata={}, ) async def _send_internal_response(self, response: Response, send: _Send) -> None: """ Send an internal Response to the ASGI send callable. Args: response: The internal Response object. send: The ASGI send callable. """ logger.debug( "Sending response to client: status=%d headers_count=%d", response.status_code, len(response.headers), ) # Convert headers to ASGI format headers: list[tuple[bytes, bytes]] = [ (key.encode("latin1"), value.encode("latin1")) for key, value in response.headers.items() ] try: # Send response.start await send( { "type": "http.response.start", "status": response.status_code, "headers": headers, } ) logger.debug("Response headers sent") # Send response body in chunks bytes_sent = 0 chunk_count = 0 async for chunk in response._aiter_stream(): await send( { "type": "http.response.body", "body": chunk, "more_body": True, } ) bytes_sent += len(chunk) chunk_count += 1 logger.debug("Sent response chunk: size=%d bytes", len(chunk)) # Send final empty chunk to signal end await send( { "type": "http.response.body", "body": b"", "more_body": False, } ) logger.info( "Response fully sent: status=%d total_bytes=%d chunks=%d", response.status_code, bytes_sent, chunk_count, ) except Exception as e: logger.error( "Error sending response: status=%d error=%s", response.status_code, str(e), exc_info=True, ) raise async def aclose(self) -> None: """Close the storage backend and release resources.""" logger.info("Closing ASGICacheMiddleware and storage backend") try: if self.storage: await self.storage.close() logger.info("Storage backend closed successfully") except Exception as e: logger.error("Error closing storage backend: %s", str(e), exc_info=True) raise karpetrosyan-hishel-fd309b1/hishel/fastapi.py000066400000000000000000000251121514113157400214000ustar00rootroot00000000000000from __future__ import annotations import typing as t from hishel._utils import generate_http_date try: import fastapi except ImportError as e: raise ImportError( "fastapi is required to use hishel.fastapi module. " "Please install hishel with the 'fastapi' extra, " "e.g., 'pip install hishel[fastapi]'." ) from e def cache( *, max_age: int | None = None, s_maxage: int | None = None, public: bool = False, private: bool | list[str] = False, no_cache: bool | list[str] = False, no_store: bool = False, no_transform: bool = False, must_revalidate: bool = False, must_understand: bool = False, proxy_revalidate: bool = False, immutable: bool = False, stale_while_revalidate: int | None = None, stale_if_error: int | None = None, ) -> t.Any: """ Add HTTP Cache-Control headers to FastAPI responses. This function provides a convenient way to set cache control directives on FastAPI responses according to RFC 9111 (HTTP Caching) and related standards. Args: max_age: Maximum time in seconds a response can be cached. [RFC 9111, Section 5.2.2.1] Example: max_age=3600 sets "Cache-Control: max-age=3600" Use for both private and shared caches. s_maxage: Maximum time in seconds for shared caches (proxies, CDNs). [RFC 9111, Section 5.2.2.10] Overrides max_age for shared caches only. Example: s_maxage=7200 sets "s-maxage=7200" Private caches (browsers) ignore this directive. public: Marks response as cacheable by any cache. [RFC 9111, Section 5.2.2.9] Explicitly allows caching even if Authorization header is present. Example: public=True adds "public" to Cache-Control private: Marks response as cacheable only by private caches (browsers). [RFC 9111, Section 5.2.2.7] Shared caches (proxies, CDNs) MUST NOT store the response. Can be True (applies to entire response) or a list of field names (applies only to specific headers). Examples: - private=True adds "private" to Cache-Control - private=["Set-Cookie"] adds 'private="Set-Cookie"' to Cache-Control Useful for user-specific data. no_cache: Response can be cached but MUST be revalidated before use. [RFC 9111, Section 5.2.2.4] Cache MUST check with origin server before serving cached copy. Can be True (requires revalidation for entire response) or a list of field names (requires revalidation only for specific headers). Examples: - no_cache=True adds "no-cache" to Cache-Control - no_cache=["Set-Cookie", "Authorization"] adds 'no-cache="Set-Cookie, Authorization"' Different from no_store - allows caching with mandatory validation. no_store: Response MUST NOT be stored in any cache. [RFC 9111, Section 5.2.2.5] Most restrictive directive - prevents all caching. Example: no_store=True adds "no-store" to Cache-Control Use for sensitive data (passwords, personal information). no_transform: Prohibits any transformations to the response. [RFC 9111, Section 5.2.2.6] Prevents proxies from modifying content (compression, format conversion). Example: no_transform=True adds "no-transform" to Cache-Control must_revalidate: Cache MUST revalidate stale responses. [RFC 9111, Section 5.2.2.2] Prevents serving stale content even if client accepts it. Example: must_revalidate=True adds "must-revalidate" to Cache-Control Stronger than no-cache - applies only when response becomes stale. must_understand: Cache MUST understand response status code to cache it. [RFC 9111, Section 5.2.2.3] Prevents caching of responses with unknown status codes. Example: must_understand=True adds "must-understand" to Cache-Control proxy_revalidate: Like must_revalidate but only for shared caches. [RFC 9111, Section 5.2.2.8] Shared caches MUST revalidate stale responses. Private caches can serve stale content without revalidation. Example: proxy_revalidate=True adds "proxy-revalidate" to Cache-Control immutable: Response body will never change. [RFC 8246] Optimization hint that revalidation is unnecessary during freshness period. Example: immutable=True adds "immutable" to Cache-Control Useful for versioned assets (e.g., /static/app.v123.js) stale_while_revalidate: Allow stale response while revalidating in background. [RFC 5861, Section 3] Time in seconds cache can serve stale content while fetching fresh copy. Example: stale_while_revalidate=86400 adds "stale-while-revalidate=86400" Improves performance by avoiding cache misses. stale_if_error: Allow stale response if origin server returns error. [RFC 5861, Section 4] Time in seconds cache can serve stale content when origin is unavailable. Example: stale_if_error=3600 adds "stale-if-error=3600" Improves availability during server failures. Returns: A dependency function that adds Cache-Control headers to the response. Examples: >>> from fastapi import FastAPI >>> from hishel.fastapi import cache >>> >>> app = FastAPI() >>> >>> # Static assets - cache for 1 year, immutable >>> @app.get("/static/logo.png") >>> async def get_logo( ... _: None = cache(max_age=31536000, public=True, immutable=True) ... ): ... return {"image": "logo.png"} >>> >>> # API endpoint - cache for 5 minutes, private >>> @app.get("/api/user/profile") >>> async def get_profile( ... _: None = cache(max_age=300, private=True) ... ): ... return {"name": "John"} >>> >>> # CDN with shared cache - different TTLs for browsers and proxies >>> @app.get("/api/public/data") >>> async def get_data( ... _: None = cache(max_age=300, s_maxage=3600, public=True) ... ): ... return {"data": "public"} >>> >>> # Sensitive data - no caching >>> @app.get("/api/secrets") >>> async def get_secrets( ... _: None = cache(no_store=True) ... ): ... return {"secret": "value"} >>> >>> # Cacheable but must revalidate >>> @app.get("/api/critical") >>> async def get_critical( ... _: None = cache(max_age=3600, must_revalidate=True) ... ): ... return {"critical": "data"} >>> >>> # Stale-while-revalidate for better performance >>> @app.get("/api/news") >>> async def get_news( ... _: None = cache(max_age=300, stale_while_revalidate=86400, public=True) ... ): ... return {"news": "articles"} >>> >>> # Private directive with specific field names >>> @app.get("/api/user/data") >>> async def get_user_data( ... _: None = cache(max_age=600, private=["Set-Cookie"]) ... ): ... return {"data": "user_specific"} >>> >>> # No-cache with field names - revalidate only specific headers >>> @app.get("/api/conditional") >>> async def get_conditional( ... _: None = cache(max_age=3600, no_cache=["Set-Cookie", "Authorization"]) ... ): ... return {"data": "conditional_cache"} Notes: - Conflicting directives (e.g., public and private) will both be set. Choose appropriate combinations based on your caching strategy. - no_store is the strongest directive and prevents all caching. - For CDNs, use s_maxage to set different TTLs for proxies vs browsers. - Use immutable with versioned URLs for maximum cache efficiency. - Combine stale_while_revalidate with max_age for better UX. - private and no_cache can accept field names to apply directives selectively to specific headers rather than the entire response. See Also: - RFC 9111: HTTP Caching (https://www.rfc-editor.org/rfc/rfc9111.html) - RFC 8246: HTTP Immutable Responses (https://www.rfc-editor.org/rfc/rfc8246.html) - RFC 5861: HTTP Cache-Control Extensions (https://www.rfc-editor.org/rfc/rfc5861.html) """ def add_cache_headers(response: fastapi.Response) -> t.Any: """Add Cache-Control headers to the response.""" directives: list[str] = [] # IMPORTANT response.headers["Date"] = generate_http_date() # Add directives with values if max_age is not None: directives.append(f"max-age={max_age}") if s_maxage is not None: directives.append(f"s-maxage={s_maxage}") if stale_while_revalidate is not None: directives.append(f"stale-while-revalidate={stale_while_revalidate}") if stale_if_error is not None: directives.append(f"stale-if-error={stale_if_error}") # Add boolean directives if public: directives.append("public") # Handle private (can be bool or list of field names) if private is True: directives.append("private") elif isinstance(private, list) and private: field_names = ", ".join(private) directives.append(f'private="{field_names}"') # Handle no_cache (can be bool or list of field names) if no_cache is True: directives.append("no-cache") elif isinstance(no_cache, list) and no_cache: field_names = ", ".join(no_cache) directives.append(f'no-cache="{field_names}"') if no_store: directives.append("no-store") if no_transform: directives.append("no-transform") if must_revalidate: directives.append("must-revalidate") if must_understand: directives.append("must-understand") if proxy_revalidate: directives.append("proxy-revalidate") if immutable: directives.append("immutable") # Set the Cache-Control header if any directives were specified if directives: response.headers["Cache-Control"] = ", ".join(directives) return fastapi.Depends(add_cache_headers) karpetrosyan-hishel-fd309b1/hishel/httpx.py000066400000000000000000000007231514113157400211210ustar00rootroot00000000000000try: import httpx # noqa: F401 except ImportError as e: raise ImportError( "httpx is required to use hishel.httpx module. " "Please install hishel with the 'httpx' extra, " "e.g., 'pip install hishel[httpx]'." ) from e from ._async_httpx import AsyncCacheClient as AsyncCacheClient, AsyncCacheTransport as AsyncCacheTransport from ._sync_httpx import SyncCacheClient as SyncCacheClient, SyncCacheTransport as SyncCacheTransport karpetrosyan-hishel-fd309b1/hishel/py.typed000066400000000000000000000000001514113157400210630ustar00rootroot00000000000000karpetrosyan-hishel-fd309b1/hishel/requests.py000066400000000000000000000152431514113157400216300ustar00rootroot00000000000000from __future__ import annotations from io import RawIOBase from typing import Any, Iterator, Mapping, Optional, overload from typing_extensions import assert_never from hishel import Headers, Request, Response as Response from hishel._core._storages._sync_base import SyncBaseStorage from hishel._core.models import extract_metadata_from_headers from hishel._policies import CachePolicy from hishel._sync_cache import SyncCacheProxy from hishel._utils import filter_mapping, snake_to_header try: import requests from requests.adapters import HTTPAdapter from urllib3 import HTTPResponse from urllib3.util.retry import Retry as Retry except ImportError: # pragma: no cover raise ImportError( "The 'requests' library is required to use the requests integration. " "Install hishel with 'pip install hishel[requests]'." ) # 128 KB CHUNK_SIZE = 131072 class _IteratorStream(RawIOBase): def __init__(self, iterator: Iterator[bytes]): self.iterator = iterator self.leftover = b"" def readable(self) -> bool: return True def readinto(self, b: bytearray) -> Optional[int]: # type: ignore chunk = self.read(len(b)) if not chunk: return 0 n = len(chunk) b[:n] = chunk return n def read(self, size: int = -1) -> bytes: if size is None or size < 0: result = self.leftover + b"".join(self.iterator) self.leftover = b"" return result while len(self.leftover) < size: try: self.leftover += next(self.iterator) except StopIteration: break result = self.leftover[:size] self.leftover = self.leftover[size:] return result @overload def _requests_to_internal( model: requests.models.PreparedRequest, ) -> Request: ... @overload def _requests_to_internal( model: requests.models.Response, ) -> Response: ... def _requests_to_internal( model: requests.models.PreparedRequest | requests.models.Response, ) -> Request | Response: if isinstance(model, requests.models.PreparedRequest): body: bytes if isinstance(model.body, str): body = model.body.encode("utf-8") elif isinstance(model.body, bytes): body = model.body else: body = b"" assert model.method return Request( method=model.method, url=str(model.url), headers=Headers(model.headers), stream=iter([body]), metadata=extract_metadata_from_headers(model.headers), ) elif isinstance(model, requests.models.Response): try: stream = model.raw.stream(amt=CHUNK_SIZE, decode_content=None) headers = Headers(filter_mapping(model.headers, ["transfer-encoding"])) except requests.exceptions.StreamConsumedError: stream = iter([model.content]) # If the stream was consumed and we don't know about the original # data and its size, fix the Content-Length header and remove # Content-Encoding so we can recreate it later properly. headers = Headers(filter_mapping(model.headers, ["content-encoding", "transfer-encoding"])) return Response( status_code=model.status_code, headers=headers, stream=stream, ) else: assert_never(model) raise RuntimeError("This line should never be reached, but is here to satisfy type checkers.") @overload def _internal_to_requests(model: Request) -> requests.models.PreparedRequest: ... @overload def _internal_to_requests(model: Response) -> requests.models.Response: ... def _internal_to_requests( model: Request | Response, ) -> requests.models.Response | requests.models.PreparedRequest: if isinstance(model, Response): response = requests.models.Response() assert isinstance(model.stream, Iterator) stream = _IteratorStream(model.stream) urllib_response = HTTPResponse( body=stream, headers={ **model.headers, **{snake_to_header(k): str(v) for k, v in model.metadata.items()}, }, status=model.status_code, preload_content=False, decode_content=False, ) # Set up the response object response.raw = urllib_response response.status_code = model.status_code response.headers.update(model.headers) response.headers.update({snake_to_header(k): str(v) for k, v in model.metadata.items()}) response.url = "" # Will be set by requests return response else: assert isinstance(model.stream, Iterator) request = requests.Request( method=model.method, url=model.url, headers=model.headers, data=b"".join(model.stream) if model.stream else None, ) return request.prepare() class CacheAdapter(HTTPAdapter): """ A custom HTTPAdapter that can be used with requests to capture HTTP interactions for snapshot testing. """ def __init__( self, pool_connections: int = 10, pool_maxsize: int = 10, max_retries: int = 0, pool_block: bool = False, storage: SyncBaseStorage | None = None, policy: CachePolicy | None = None, ): super().__init__(pool_connections, pool_maxsize, max_retries, pool_block) self._cache_proxy = SyncCacheProxy( request_sender=self._send_request, storage=storage, policy=policy, ) self.storage = self._cache_proxy.storage def send( self, request: requests.models.PreparedRequest, stream: bool = False, timeout: None | float | tuple[float, float] | tuple[float, None] = None, verify: bool | str = True, cert: None | bytes | str | tuple[bytes | str, bytes | str] = None, proxies: Mapping[str, str] | None = None, ) -> requests.models.Response: internal_request = _requests_to_internal(request) internal_response = self._cache_proxy.handle_request(internal_request) response = _internal_to_requests(internal_response) # Set the original request on the response response.request = request response.connection = self # type: ignore return response def _send_request(self, request: Request) -> Response: requests_request = _internal_to_requests(request) response = super().send( requests_request, stream=True, ) return _requests_to_internal(response) def close(self) -> Any: self.storage.close() karpetrosyan-hishel-fd309b1/mkdocs.yml000066400000000000000000000035331514113157400201310ustar00rootroot00000000000000site_name: Hishel repo_url: https://github.com/karpetrosyan/hishel site_url: https://hishel.com/ theme: name: material 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 features: - navigation.sections - navigation.footer plugins: - mike: {} - git-committers: repository: karpetrosyan/hishel branch: master enabled: true - git-revision-date-localized: enable_creation_date: true - search: separator: '[\s\u200b\-_,:!=\[\]()"`/]+|\.(?!\d)|&[lg]t;|(?!\b)(?=[A-Z][a-z])' extra: version: provider: mike social: - icon: fontawesome/brands/github link: https://github.com/karpetrosyan/hishel - icon: fontawesome/brands/python link: https://pypi.org/project/hishel/ # mkdocs.yml markdown_extensions: - pymdownx.tabbed: alternate_style: true # Required for === syntax - pymdownx.superfences # For code blocks inside tabs - admonition # For notes, warnings, etc. - pymdownx.superfences: custom_fences: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format nav: - Introduction: index.md - Policies: policies.md - Advanced: - Storages: storages.md - Metadata: metadata.md - Sans-I/O Caching: specification.md - Integrations: - httpx: integrations/httpx.md - requests: integrations/requests.md - ASGI: integrations/asgi.md - FastAPI: integrations/fastapi.md - Blacksheep: integrations/blacksheep.md - GraphQL: integrations/graphql.md - Custom Integrations: integrations/custom.md - Contributing: contributing.md extra_css: - docs/custom.csskarpetrosyan-hishel-fd309b1/pyproject.toml000066400000000000000000000065311514113157400210430ustar00rootroot00000000000000[build-system] requires = ["hatchling", "hatch-fancy-pypi-readme"] build-backend = "hatchling.build" [project] name = "hishel" version = "1.1.9" dynamic = ["readme"] description = " Elegant HTTP Caching for Python" license = "BSD-3-Clause" requires-python = ">=3.10" 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.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Topic :: Internet :: WWW/HTTP", ] dependencies = [ "msgpack>=1.1.2", "typing-extensions>=4.14.1", ] [project.optional-dependencies] async = [ "anyio>=4.9.0", "anysqlite>=0.0.5", ] requests = [ "requests>=2.32.5", ] httpx = [ "httpx>=0.28.1", # install async extra "anyio>=4.9.0", "anysqlite>=0.0.5", ] fastapi = [ "fastapi>=0.119.1", ] [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.mypy.overrides]] module = ["time_machine.*", "msgpack.*"] ignore_missing_imports = true [tool.pytest.ini_options] addopts = ["-rxXs", "--strict-config", "--strict-markers"] filterwarnings = [] [tool.coverage.report] exclude_also = ['__repr__', 'raise NotImplementedError()'] [tool.ruff] exclude = [ "hishel/_sync", "hishel/__init__.py", "tests/_sync", "hishel/_core/_sync", "tests/_core/_sync", "hishel/_sync_cache.py", "tests/test_sync_httpx.py", "hishel/_core/_storages/_sync_sqlite.py", "hishel/_core/_storages/_sync_base.py", "tests/test_sync_httpx.py", "hishel/_sync_httpx.py" ] line-length = 120 [tool.ruff.lint] select = ["E", "F", "W", "I"] [tool.ruff.lint.isort] combine-as-imports = true [dependency-groups] dev = [ "anyio==4.12.1", "anysqlite>=0.0.5", "coverage==7.10.7", "fastapi[standard]>=0.119.1", "hatch==1.15.1", "inline-snapshot>=0.28.0", "mike>=2.1.3", "mkdocs==1.6.1", "mkdocs-git-committers-plugin>=0.2.3", "mkdocs-git-revision-date-localized-plugin>=1.4.7", "mkdocs-material==9.7.1", "mypy==1.19.1", "pyright>=1.1.404", "pytest==8.4.2", "pytest-cov>=6.2.1", "pytest-icdiff>=0.9", "ruff==0.14.14", "time-machine>=2.19.0", "trio==0.31.0", "types-boto3==1.42.39", "types-pyyaml==6.0.12.20250915", "types-requests>=2.31.0.6", "zipp>=3.19.1", ] karpetrosyan-hishel-fd309b1/scripts/000077500000000000000000000000001514113157400176115ustar00rootroot00000000000000karpetrosyan-hishel-fd309b1/scripts/fix000077500000000000000000000003011514113157400203170ustar00rootroot00000000000000#!/usr/bin/env bash set -e echo "==> Running ruff" uv run ruff check --fix hishel tests uv run ruff format hishel tests echo "==> Make sure async/sync are consistent" uv run scripts/unasync karpetrosyan-hishel-fd309b1/scripts/get_doc_version.py000077500000000000000000000006371514113157400233450ustar00rootroot00000000000000#!/usr/bin/env -S uv run --script # /// script # requires-python = ">=3.10" # dependencies = [ # "hishel", # ] # # [tool.uv.sources] # hishel = { path = "../", editable = true } # /// from importlib.metadata import version def get_doc_version() -> str: ver = version("hishel") splited = ver.split(".") return f"{splited[0]}.{splited[1]}" if __name__ == "__main__": print(get_doc_version()) karpetrosyan-hishel-fd309b1/scripts/lint000077500000000000000000000011261514113157400205050ustar00rootroot00000000000000#!/usr/bin/env bash set -e echo "==> Running ruff" uv run ruff check hishel tests uv run ruff format --check echo "==> Make sure async/sync are consistent" uv run scripts/unasync --check echo "==> Running mypy" uv run --all-extras mypy hishel tests echo "==> Making sure it imports" uv run --with-editable . python -c 'import hishel' echo "==> Making sure it imports without extras" uv run --script - <<'EOF' # /// script # requires-python = ">=3.10" # dependencies = [ # "hishel", # ] # # [tool.uv.sources] # hishel = { path = ".", editable = true } # /// import hishel # noqa: F401 EOFkarpetrosyan-hishel-fd309b1/scripts/publish-docs000077500000000000000000000000621514113157400221310ustar00rootroot00000000000000#! /bin/bash -ex uv run mkdocs gh-deploy --force karpetrosyan-hishel-fd309b1/scripts/test000077500000000000000000000001501514113157400205120ustar00rootroot00000000000000#!/usr/bin/env bash set -e echo "==> Running tests" uv run pytest --cov=./ --cov-report=xml tests "$@"karpetrosyan-hishel-fd309b1/scripts/unasync000077500000000000000000000103761514113157400212260ustar00rootroot00000000000000#!/usr/bin/env -S uv run --script # /// script # requires-python = ">=3.10" # dependencies = [] # /// import os import re import sys SUBS = [ # builtins ("async def", "def"), ("async with", "with"), ("await ", ""), ("async for", "for"), ("AsyncIterator", "Iterator"), ("AsyncIterable", "Iterable"), ("__aiter__", "__iter__"), ("__anext__", "__next__"), ("AsyncMock", "MagicMock"), ("assert_awaited_once", "assert_called_once"), (r"Awaitable\[([^\]]+)\]", r"\1"), # our public API ("AsyncCacheProxy", "SyncCacheProxy"), ("AsyncBaseStorage", "SyncBaseStorage"), ("AsyncCacheClient", "SyncCacheClient"), ("AsyncSqliteStorage", "SyncSqliteStorage"), ("anysqlite", "sqlite3"), ("@pytest.mark.anyio", ""), ("from anyio import Lock", "from threading import RLock"), ("self._lock = Lock", "self._lock = RLock"), ("aiter_stream", "iter_stream"), ("aiter_raw", "iter_raw"), ("aprint_sqlite_state", "print_sqlite_state"), ("make_async_iterator", "make_sync_iterator"), ("AsyncCacheTransport", "SyncCacheTransport"), ( "hishel._core._storages._async_base", "hishel._core._storages._sync_base", ), # Third-party libraries ("AsyncClient", "Client"), ("aread", "read"), ("aclose", "close"), ("handle_async_request", "handle_request"), ("AsyncBaseTransport", "BaseTransport"), ("AsyncHTTPTransport", "HTTPTransport"), ] COMPILED_SUBS = [(re.compile(regex), 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 FILES = [ ( "tests/_core/_async/test_sqlite_storage.py", "tests/_core/_sync/test_sqlite_storage.py", ), ("tests/test_async_httpx.py", "tests/test_sync_httpx.py"), ("hishel/_async_cache.py", "hishel/_sync_cache.py"), ("hishel/_core/_storages/_async_base.py", "hishel/_core/_storages/_sync_base.py"), ("hishel/_core/_storages/_async_sqlite.py", "hishel/_core/_storages/_sync_sqlite.py"), ("hishel/_async_httpx.py", "hishel/_sync_httpx.py"), ] for in_path, out_path in FILES: if check_only: unasync_file_check(in_path, out_path) else: unasync_file(in_path, out_path) print(f"Wrote {out_path}") 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) if __name__ == "__main__": main() karpetrosyan-hishel-fd309b1/tests/000077500000000000000000000000001514113157400172645ustar00rootroot00000000000000karpetrosyan-hishel-fd309b1/tests/__init__.py000066400000000000000000000001441514113157400213740ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2023-present U.N. Owen # # SPDX-License-Identifier: MIT karpetrosyan-hishel-fd309b1/tests/_core/000077500000000000000000000000001514113157400203535ustar00rootroot00000000000000karpetrosyan-hishel-fd309b1/tests/_core/_async/000077500000000000000000000000001514113157400216275ustar00rootroot00000000000000karpetrosyan-hishel-fd309b1/tests/_core/_async/__init__.py000066400000000000000000000000001514113157400237260ustar00rootroot00000000000000karpetrosyan-hishel-fd309b1/tests/_core/_async/test_sqlite_storage.py000066400000000000000000000405251514113157400262730ustar00rootroot00000000000000import uuid from dataclasses import replace from datetime import datetime from typing import Any, AsyncIterator from unittest.mock import AsyncMock, patch from zoneinfo import ZoneInfo import anysqlite import pytest from inline_snapshot import snapshot from time_machine import travel from hishel import AsyncSqliteStorage, Request, Response from hishel._utils import make_async_iterator from tests.conftest import aprint_sqlite_state @pytest.mark.anyio async def test_custom_connection_does_not_create_directory() -> None: """Test that providing a custom connection doesn't call ensure_cache_dict.""" with patch("hishel._core._storages._async_sqlite.ensure_cache_dict") as mock_ensure: storage = AsyncSqliteStorage(connection=await anysqlite.connect(":memory:")) # Create an entry to trigger _ensure_connection entry = await storage.create_entry( request=Request(method="GET", url="https://example.com"), response=Response(status_code=200, stream=make_async_iterator([b"data"])), key="test_key", ) # Consume the stream await entry.response.aread() # ensure_cache_dict should still not have been called mock_ensure.assert_not_called() @pytest.mark.anyio @travel(datetime(2024, 1, 1, 0, 0, 0, tzinfo=ZoneInfo("UTC"))) async def test_add_entry() -> None: """Test adding a complete entry with request and response.""" storage = AsyncSqliteStorage(connection=await anysqlite.connect(":memory:")) entry = await storage.create_entry( request=Request( method="GET", url="https://example.com", ), response=Response( status_code=200, stream=make_async_iterator([b"response data"]), ), key="test_key", id_=uuid.UUID(int=0), ) # Consume the stream to save it async for _ in entry.response._aiter_stream(): ... conn = await storage._ensure_connection() assert await aprint_sqlite_state(conn) == snapshot("""\ ================================================================================ DATABASE SNAPSHOT ================================================================================ TABLE: entries -------------------------------------------------------------------------------- Rows: 1 Row 1: id = (bytes) 0x00000000000000000000000000000000 (16 bytes) cache_key = (str) 'test_key' data = (bytes) 0x85a26964c41000000000000000000000000000000000a772657175657374... (180 bytes) created_at = 2024-01-01 deleted_at = NULL TABLE: streams -------------------------------------------------------------------------------- Rows: 2 Row 1: entry_id = (bytes) 0x00000000000000000000000000000000 (16 bytes) chunk_number = 0 chunk_data = (str) 'response data' Row 2: entry_id = (bytes) 0x00000000000000000000000000000000 (16 bytes) chunk_number = -1 chunk_data = (str) '' ================================================================================\ """) @pytest.mark.anyio @travel(datetime(2024, 1, 1, 0, 0, 0, tzinfo=ZoneInfo("UTC"))) async def test_add_entry_with_stream() -> None: """Test adding an entry with a streaming response body.""" storage = AsyncSqliteStorage(connection=await anysqlite.connect(":memory:")) entry = await storage.create_entry( request=Request( method="POST", url="https://example.com/upload", ), response=Response( status_code=200, stream=make_async_iterator([b"chunk1", b"chunk2"]), ), key="stream_key", id_=uuid.UUID(int=0), ) # Consume the stream async for _ in entry.response._aiter_stream(): ... # Verify the entry was created with cache_key set conn = await storage._ensure_connection() assert await aprint_sqlite_state(conn) == snapshot("""\ ================================================================================ DATABASE SNAPSHOT ================================================================================ TABLE: entries -------------------------------------------------------------------------------- Rows: 1 Row 1: id = (bytes) 0x00000000000000000000000000000000 (16 bytes) cache_key = (str) 'stream_key' data = (bytes) 0x85a26964c41000000000000000000000000000000000a772657175657374... (190 bytes) created_at = 2024-01-01 deleted_at = NULL TABLE: streams -------------------------------------------------------------------------------- Rows: 3 Row 1: entry_id = (bytes) 0x00000000000000000000000000000000 (16 bytes) chunk_number = 0 chunk_data = (str) 'chunk1' Row 2: entry_id = (bytes) 0x00000000000000000000000000000000 (16 bytes) chunk_number = 1 chunk_data = (str) 'chunk2' Row 3: entry_id = (bytes) 0x00000000000000000000000000000000 (16 bytes) chunk_number = -1 chunk_data = (str) '' ================================================================================\ """) @pytest.mark.anyio @travel(datetime(2024, 1, 1, 0, 0, 0, tzinfo=ZoneInfo("UTC"))) async def test_get_entries() -> None: """Test retrieving entries by cache key.""" storage = AsyncSqliteStorage(connection=await anysqlite.connect(":memory:")) # Create two entries with the same cache key e1 = await storage.create_entry( request=Request(method="GET", url="https://example.com/1"), response=Response(status_code=200, stream=make_async_iterator([b"response1"])), key="shared_key", id_=uuid.UUID(int=1), ) await e1.response.aread() e2 = await storage.create_entry( request=Request(method="GET", url="https://example.com/2"), response=Response(status_code=200, stream=make_async_iterator([b"response2"])), key="shared_key", id_=uuid.UUID(int=2), ) await e2.response.aread() # Retrieve entries entries = await storage.get_entries("shared_key") assert len(entries) == 2 assert all(entry.cache_key == b"shared_key" for entry in entries) @pytest.mark.anyio @travel(datetime(2024, 1, 1, 0, 0, 0, tzinfo=ZoneInfo("UTC"))) async def test_multiple_entries_same_key() -> None: """Test creating multiple entries with the same cache key.""" storage = AsyncSqliteStorage(connection=await anysqlite.connect(":memory:")) # Create multiple complete entries with the same key e1 = await storage.create_entry( request=Request(method="GET", url="https://example.com/1"), response=Response(status_code=200, stream=make_async_iterator([b"response1"])), key="shared_key", id_=uuid.UUID(int=3), ) await e1.response.aread() e2 = await storage.create_entry( request=Request(method="GET", url="https://example.com/2"), response=Response(status_code=200, stream=make_async_iterator([b"response2"])), key="shared_key", id_=uuid.UUID(int=4), ) await e2.response.aread() # Should return both complete entries entries = await storage.get_entries("shared_key") assert len(entries) == 2 assert all(entry.response is not None for entry in entries) @pytest.mark.anyio @travel(datetime(2024, 1, 1, 0, 0, 0, tzinfo=ZoneInfo("UTC"))) async def test_update_entry() -> None: """Test updating an existing entry.""" storage = AsyncSqliteStorage(connection=await anysqlite.connect(":memory:")) entry = await storage.create_entry( request=Request(method="GET", url="https://example.com"), response=Response(status_code=200, stream=make_async_iterator([b"original"])), key="original_key", id_=uuid.UUID(int=5), ) await entry.response.aread() # Update with a callable def updater(pair): return replace(pair, cache_key=b"updated_key") result = await storage.update_entry(entry.id, updater) assert result is not None assert result.cache_key == b"updated_key" # Verify the update persisted entries = await storage.get_entries("updated_key") assert len(entries) == 1 assert entries[0].cache_key == b"updated_key" @pytest.mark.anyio @travel(datetime(2024, 1, 1, 0, 0, 0, tzinfo=ZoneInfo("UTC"))) async def test_update_entry_with_new_entry() -> None: """Test updating an entry by providing a new entry directly.""" storage = AsyncSqliteStorage(connection=await anysqlite.connect(":memory:")) entry = await storage.create_entry( request=Request(method="GET", url="https://example.com"), response=Response(status_code=200, stream=make_async_iterator([b"data"])), key="key1", id_=uuid.UUID(int=6), ) # Update with a new entry object new_entry = replace(entry, cache_key=b"key2") result = await storage.update_entry(entry.id, new_entry) assert result is not None assert result.cache_key == b"key2" @pytest.mark.anyio @travel(datetime(2024, 1, 1, 0, 0, 0, tzinfo=ZoneInfo("UTC"))) async def test_remove_entry() -> None: """Test soft-deleting an entry.""" storage = AsyncSqliteStorage(connection=await anysqlite.connect(":memory:")) entry = await storage.create_entry( request=Request(method="GET", url="https://example.com"), response=Response(status_code=200, stream=make_async_iterator([b"data"])), key="test_key", id_=uuid.UUID(int=7), ) # Remove the entry await storage.remove_entry(entry.id) # Verify deleted_at is set conn = await storage._ensure_connection() cursor = await conn.cursor() await cursor.execute("SELECT deleted_at FROM entries WHERE id = ?", (entry.id.bytes,)) result = await cursor.fetchone() assert result is not None assert result[0] is not None # deleted_at should be set @pytest.mark.anyio @travel(datetime(2024, 1, 1, 0, 0, 0, tzinfo=ZoneInfo("UTC"))) async def test_stream_persistence() -> None: """Test that streams are properly saved and retrieved.""" storage = AsyncSqliteStorage(connection=await anysqlite.connect(":memory:")) response_chunks = [b"resp1", b"resp2"] entry = await storage.create_entry( request=Request( method="POST", url="https://example.com", ), response=Response(status_code=200, stream=make_async_iterator(response_chunks)), key="stream_test", id_=uuid.UUID(int=8), ) async for _ in entry.response._aiter_stream(): ... # Retrieve and verify streams entries = await storage.get_entries("stream_test") assert len(entries) == 1 retrieved_response_chunks = [] async for chunk in entries[0].response._aiter_stream(): retrieved_response_chunks.append(chunk) assert retrieved_response_chunks == response_chunks @pytest.mark.anyio @travel(datetime(2024, 1, 1, 0, 0, 0, tzinfo=ZoneInfo("UTC"))) async def test_multiple_entries_different_keys() -> None: """Test that entries with different keys are properly isolated.""" storage = AsyncSqliteStorage(connection=await anysqlite.connect(":memory:")) # Create entries with different keys for i in range(3): entry = await storage.create_entry( request=Request(method="GET", url=f"https://example.com/{i}"), response=Response( status_code=200, stream=make_async_iterator([f"data{i}".encode()]), ), key=f"key_{i}", id_=uuid.UUID(int=9 + i), ) # Consume the stream to save it async for _ in entry.response._aiter_stream(): ... # Verify each key returns only its own entry for i in range(3): entries = await storage.get_entries(f"key_{i}") assert len(entries) == 1 assert entries[0].request.url == f"https://example.com/{i}" @pytest.mark.anyio @travel(datetime(2024, 1, 1, 0, 0, 0, tzinfo=ZoneInfo("UTC"))) async def test_remove_nonexistent_entry() -> None: """Test that removing a non-existent entry doesn't raise an error.""" storage = AsyncSqliteStorage(connection=await anysqlite.connect(":memory:")) # Should not raise await storage.remove_entry(uuid.UUID(int=999)) @pytest.mark.anyio @travel(datetime(2024, 1, 1, 0, 0, 0, tzinfo=ZoneInfo("UTC"))) async def test_update_nonexistent_entry() -> None: """Test that updating a non-existent entry returns None.""" storage = AsyncSqliteStorage(connection=await anysqlite.connect(":memory:")) result = await storage.update_entry(uuid.UUID(int=999), lambda p: replace(p, cache_key=b"new_key")) assert result is None @pytest.mark.anyio @travel(datetime(2024, 1, 1, 0, 0, 0, tzinfo=ZoneInfo("UTC"))) async def test_close_connection(monkeypatch: Any) -> None: """Test that close() properly closes the underlying SQLite connection.""" mock_connection = AsyncMock() mock_connection.close = AsyncMock() async def mock_connect(*args, **kwargs): return mock_connection monkeypatch.setattr("anysqlite.connect", mock_connect) storage = AsyncSqliteStorage(connection=await anysqlite.connect(":memory:")) conn = await storage._ensure_connection() assert conn is not None assert storage.connection is not None assert storage.connection is mock_connection await storage.close() assert storage.connection is None mock_connection.close.assert_awaited_once() @pytest.mark.anyio @travel(datetime(2024, 1, 1, 0, 0, 0, tzinfo=ZoneInfo("UTC"))) async def test_incomplete_entries() -> None: """Test incomplete entries""" storage = AsyncSqliteStorage(connection=await anysqlite.connect(":memory:")) entry = await storage.create_entry( request=Request(method="GET", url="https://example.com"), response=Response(status_code=200, stream=make_async_iterator([b"chunk1", b"chunk2"])), key="incomplete_key", id_=uuid.UUID(int=10), ) # read only part of the stream assert isinstance(entry.response.stream, AsyncIterator) await entry.response.stream.__anext__() # Verify the entry was created but is incomplete, so get_entries should skip it entries = await storage.get_entries("incomplete_key") assert len(entries) == 0 assert await aprint_sqlite_state(await storage._ensure_connection()) == snapshot("""\ ================================================================================ DATABASE SNAPSHOT ================================================================================ TABLE: entries -------------------------------------------------------------------------------- Rows: 1 Row 1: id = (bytes) 0x0000000000000000000000000000000a (16 bytes) cache_key = (str) 'incomplete_key' data = (bytes) 0x85a26964c4100000000000000000000000000000000aa772657175657374... (186 bytes) created_at = 2024-01-01 deleted_at = NULL TABLE: streams -------------------------------------------------------------------------------- Rows: 1 Row 1: entry_id = (bytes) 0x0000000000000000000000000000000a (16 bytes) chunk_number = 0 chunk_data = (str) 'chunk1' ================================================================================\ """) @pytest.mark.anyio async def test_expired_entries() -> None: """Test expired entries""" storage = AsyncSqliteStorage(connection=await anysqlite.connect(":memory:"), default_ttl=0) entry = await storage.create_entry( request=Request(method="GET", url="https://example.com"), response=Response(status_code=200, stream=make_async_iterator([b"data"])), key="expired_key", id_=uuid.UUID(int=11), ) await entry.response.aread() # Verify the entry is expired, so get_entries should skip it entries = await storage.get_entries("expired_key") assert len(entries) == 0 @pytest.mark.anyio async def test_soft_deleted_entries() -> None: """Test soft-deleted entries""" storage = AsyncSqliteStorage(connection=await anysqlite.connect(":memory:")) entry = await storage.create_entry( request=Request(method="GET", url="https://example.com"), response=Response(status_code=200, stream=make_async_iterator([b"data"])), key="soft_deleted_key", id_=uuid.UUID(int=12), ) await entry.response.aread() # Soft delete the entry await storage.remove_entry(entry.id) # Verify the entry is soft deleted, so get_entries should skip it entries = await storage.get_entries("soft_deleted_key") assert len(entries) == 0 karpetrosyan-hishel-fd309b1/tests/_core/_sync/000077500000000000000000000000001514113157400214665ustar00rootroot00000000000000karpetrosyan-hishel-fd309b1/tests/_core/_sync/__init__000066400000000000000000000000001514113157400231360ustar00rootroot00000000000000karpetrosyan-hishel-fd309b1/tests/_core/_sync/test_sqlite_storage.py000066400000000000000000000367031514113157400261350ustar00rootroot00000000000000import uuid from dataclasses import replace from datetime import datetime from typing import Any, Iterator from unittest.mock import MagicMock, patch from zoneinfo import ZoneInfo import sqlite3 import pytest from inline_snapshot import snapshot from time_machine import travel from hishel import SyncSqliteStorage, Request, Response from hishel._utils import make_sync_iterator from tests.conftest import print_sqlite_state def test_custom_connection_does_not_create_directory() -> None: """Test that providing a custom connection doesn't call ensure_cache_dict.""" with patch("hishel._core._storages._async_sqlite.ensure_cache_dict") as mock_ensure: storage = SyncSqliteStorage(connection=sqlite3.connect(":memory:")) # Create an entry to trigger _ensure_connection entry = storage.create_entry( request=Request(method="GET", url="https://example.com"), response=Response(status_code=200, stream=make_sync_iterator([b"data"])), key="test_key", ) # Consume the stream entry.response.read() # ensure_cache_dict should still not have been called mock_ensure.assert_not_called() @travel(datetime(2024, 1, 1, 0, 0, 0, tzinfo=ZoneInfo("UTC"))) def test_add_entry() -> None: """Test adding a complete entry with request and response.""" storage = SyncSqliteStorage(connection=sqlite3.connect(":memory:")) entry = storage.create_entry( request=Request( method="GET", url="https://example.com", ), response=Response( status_code=200, stream=make_sync_iterator([b"response data"]), ), key="test_key", id_=uuid.UUID(int=0), ) # Consume the stream to save it for _ in entry.response._iter_stream(): ... conn = storage._ensure_connection() assert print_sqlite_state(conn) == snapshot("""\ ================================================================================ DATABASE SNAPSHOT ================================================================================ TABLE: entries -------------------------------------------------------------------------------- Rows: 1 Row 1: id = (bytes) 0x00000000000000000000000000000000 (16 bytes) cache_key = (str) 'test_key' data = (bytes) 0x85a26964c41000000000000000000000000000000000a772657175657374... (180 bytes) created_at = 2024-01-01 deleted_at = NULL TABLE: streams -------------------------------------------------------------------------------- Rows: 2 Row 1: entry_id = (bytes) 0x00000000000000000000000000000000 (16 bytes) chunk_number = 0 chunk_data = (str) 'response data' Row 2: entry_id = (bytes) 0x00000000000000000000000000000000 (16 bytes) chunk_number = -1 chunk_data = (str) '' ================================================================================\ """) @travel(datetime(2024, 1, 1, 0, 0, 0, tzinfo=ZoneInfo("UTC"))) def test_add_entry_with_stream() -> None: """Test adding an entry with a streaming response body.""" storage = SyncSqliteStorage(connection=sqlite3.connect(":memory:")) entry = storage.create_entry( request=Request( method="POST", url="https://example.com/upload", ), response=Response( status_code=200, stream=make_sync_iterator([b"chunk1", b"chunk2"]), ), key="stream_key", id_=uuid.UUID(int=0), ) # Consume the stream for _ in entry.response._iter_stream(): ... # Verify the entry was created with cache_key set conn = storage._ensure_connection() assert print_sqlite_state(conn) == snapshot("""\ ================================================================================ DATABASE SNAPSHOT ================================================================================ TABLE: entries -------------------------------------------------------------------------------- Rows: 1 Row 1: id = (bytes) 0x00000000000000000000000000000000 (16 bytes) cache_key = (str) 'stream_key' data = (bytes) 0x85a26964c41000000000000000000000000000000000a772657175657374... (190 bytes) created_at = 2024-01-01 deleted_at = NULL TABLE: streams -------------------------------------------------------------------------------- Rows: 3 Row 1: entry_id = (bytes) 0x00000000000000000000000000000000 (16 bytes) chunk_number = 0 chunk_data = (str) 'chunk1' Row 2: entry_id = (bytes) 0x00000000000000000000000000000000 (16 bytes) chunk_number = 1 chunk_data = (str) 'chunk2' Row 3: entry_id = (bytes) 0x00000000000000000000000000000000 (16 bytes) chunk_number = -1 chunk_data = (str) '' ================================================================================\ """) @travel(datetime(2024, 1, 1, 0, 0, 0, tzinfo=ZoneInfo("UTC"))) def test_get_entries() -> None: """Test retrieving entries by cache key.""" storage = SyncSqliteStorage(connection=sqlite3.connect(":memory:")) # Create two entries with the same cache key e1 = storage.create_entry( request=Request(method="GET", url="https://example.com/1"), response=Response(status_code=200, stream=make_sync_iterator([b"response1"])), key="shared_key", id_=uuid.UUID(int=1), ) e1.response.read() e2 = storage.create_entry( request=Request(method="GET", url="https://example.com/2"), response=Response(status_code=200, stream=make_sync_iterator([b"response2"])), key="shared_key", id_=uuid.UUID(int=2), ) e2.response.read() # Retrieve entries entries = storage.get_entries("shared_key") assert len(entries) == 2 assert all(entry.cache_key == b"shared_key" for entry in entries) @travel(datetime(2024, 1, 1, 0, 0, 0, tzinfo=ZoneInfo("UTC"))) def test_multiple_entries_same_key() -> None: """Test creating multiple entries with the same cache key.""" storage = SyncSqliteStorage(connection=sqlite3.connect(":memory:")) # Create multiple complete entries with the same key e1 = storage.create_entry( request=Request(method="GET", url="https://example.com/1"), response=Response(status_code=200, stream=make_sync_iterator([b"response1"])), key="shared_key", id_=uuid.UUID(int=3), ) e1.response.read() e2 = storage.create_entry( request=Request(method="GET", url="https://example.com/2"), response=Response(status_code=200, stream=make_sync_iterator([b"response2"])), key="shared_key", id_=uuid.UUID(int=4), ) e2.response.read() # Should return both complete entries entries = storage.get_entries("shared_key") assert len(entries) == 2 assert all(entry.response is not None for entry in entries) @travel(datetime(2024, 1, 1, 0, 0, 0, tzinfo=ZoneInfo("UTC"))) def test_update_entry() -> None: """Test updating an existing entry.""" storage = SyncSqliteStorage(connection=sqlite3.connect(":memory:")) entry = storage.create_entry( request=Request(method="GET", url="https://example.com"), response=Response(status_code=200, stream=make_sync_iterator([b"original"])), key="original_key", id_=uuid.UUID(int=5), ) entry.response.read() # Update with a callable def updater(pair): return replace(pair, cache_key=b"updated_key") result = storage.update_entry(entry.id, updater) assert result is not None assert result.cache_key == b"updated_key" # Verify the update persisted entries = storage.get_entries("updated_key") assert len(entries) == 1 assert entries[0].cache_key == b"updated_key" @travel(datetime(2024, 1, 1, 0, 0, 0, tzinfo=ZoneInfo("UTC"))) def test_update_entry_with_new_entry() -> None: """Test updating an entry by providing a new entry directly.""" storage = SyncSqliteStorage(connection=sqlite3.connect(":memory:")) entry = storage.create_entry( request=Request(method="GET", url="https://example.com"), response=Response(status_code=200, stream=make_sync_iterator([b"data"])), key="key1", id_=uuid.UUID(int=6), ) # Update with a new entry object new_entry = replace(entry, cache_key=b"key2") result = storage.update_entry(entry.id, new_entry) assert result is not None assert result.cache_key == b"key2" @travel(datetime(2024, 1, 1, 0, 0, 0, tzinfo=ZoneInfo("UTC"))) def test_remove_entry() -> None: """Test soft-deleting an entry.""" storage = SyncSqliteStorage(connection=sqlite3.connect(":memory:")) entry = storage.create_entry( request=Request(method="GET", url="https://example.com"), response=Response(status_code=200, stream=make_sync_iterator([b"data"])), key="test_key", id_=uuid.UUID(int=7), ) # Remove the entry storage.remove_entry(entry.id) # Verify deleted_at is set conn = storage._ensure_connection() cursor = conn.cursor() cursor.execute("SELECT deleted_at FROM entries WHERE id = ?", (entry.id.bytes,)) result = cursor.fetchone() assert result is not None assert result[0] is not None # deleted_at should be set @travel(datetime(2024, 1, 1, 0, 0, 0, tzinfo=ZoneInfo("UTC"))) def test_stream_persistence() -> None: """Test that streams are properly saved and retrieved.""" storage = SyncSqliteStorage(connection=sqlite3.connect(":memory:")) response_chunks = [b"resp1", b"resp2"] entry = storage.create_entry( request=Request( method="POST", url="https://example.com", ), response=Response(status_code=200, stream=make_sync_iterator(response_chunks)), key="stream_test", id_=uuid.UUID(int=8), ) for _ in entry.response._iter_stream(): ... # Retrieve and verify streams entries = storage.get_entries("stream_test") assert len(entries) == 1 retrieved_response_chunks = [] for chunk in entries[0].response._iter_stream(): retrieved_response_chunks.append(chunk) assert retrieved_response_chunks == response_chunks @travel(datetime(2024, 1, 1, 0, 0, 0, tzinfo=ZoneInfo("UTC"))) def test_multiple_entries_different_keys() -> None: """Test that entries with different keys are properly isolated.""" storage = SyncSqliteStorage(connection=sqlite3.connect(":memory:")) # Create entries with different keys for i in range(3): entry = storage.create_entry( request=Request(method="GET", url=f"https://example.com/{i}"), response=Response( status_code=200, stream=make_sync_iterator([f"data{i}".encode()]), ), key=f"key_{i}", id_=uuid.UUID(int=9 + i), ) # Consume the stream to save it for _ in entry.response._iter_stream(): ... # Verify each key returns only its own entry for i in range(3): entries = storage.get_entries(f"key_{i}") assert len(entries) == 1 assert entries[0].request.url == f"https://example.com/{i}" @travel(datetime(2024, 1, 1, 0, 0, 0, tzinfo=ZoneInfo("UTC"))) def test_remove_nonexistent_entry() -> None: """Test that removing a non-existent entry doesn't raise an error.""" storage = SyncSqliteStorage(connection=sqlite3.connect(":memory:")) # Should not raise storage.remove_entry(uuid.UUID(int=999)) @travel(datetime(2024, 1, 1, 0, 0, 0, tzinfo=ZoneInfo("UTC"))) def test_update_nonexistent_entry() -> None: """Test that updating a non-existent entry returns None.""" storage = SyncSqliteStorage(connection=sqlite3.connect(":memory:")) result = storage.update_entry(uuid.UUID(int=999), lambda p: replace(p, cache_key=b"new_key")) assert result is None @travel(datetime(2024, 1, 1, 0, 0, 0, tzinfo=ZoneInfo("UTC"))) def test_close_connection(monkeypatch: Any) -> None: """Test that close() properly closes the underlying SQLite connection.""" mock_connection = MagicMock() mock_connection.close = MagicMock() def mock_connect(*args, **kwargs): return mock_connection monkeypatch.setattr("sqlite3.connect", mock_connect) storage = SyncSqliteStorage(connection=sqlite3.connect(":memory:")) conn = storage._ensure_connection() assert conn is not None assert storage.connection is not None assert storage.connection is mock_connection storage.close() assert storage.connection is None mock_connection.close.assert_called_once() @travel(datetime(2024, 1, 1, 0, 0, 0, tzinfo=ZoneInfo("UTC"))) def test_incomplete_entries() -> None: """Test incomplete entries""" storage = SyncSqliteStorage(connection=sqlite3.connect(":memory:")) entry = storage.create_entry( request=Request(method="GET", url="https://example.com"), response=Response(status_code=200, stream=make_sync_iterator([b"chunk1", b"chunk2"])), key="incomplete_key", id_=uuid.UUID(int=10), ) # read only part of the stream assert isinstance(entry.response.stream, Iterator) entry.response.stream.__next__() # Verify the entry was created but is incomplete, so get_entries should skip it entries = storage.get_entries("incomplete_key") assert len(entries) == 0 assert print_sqlite_state(storage._ensure_connection()) == snapshot("""\ ================================================================================ DATABASE SNAPSHOT ================================================================================ TABLE: entries -------------------------------------------------------------------------------- Rows: 1 Row 1: id = (bytes) 0x0000000000000000000000000000000a (16 bytes) cache_key = (str) 'incomplete_key' data = (bytes) 0x85a26964c4100000000000000000000000000000000aa772657175657374... (186 bytes) created_at = 2024-01-01 deleted_at = NULL TABLE: streams -------------------------------------------------------------------------------- Rows: 1 Row 1: entry_id = (bytes) 0x0000000000000000000000000000000a (16 bytes) chunk_number = 0 chunk_data = (str) 'chunk1' ================================================================================\ """) def test_expired_entries() -> None: """Test expired entries""" storage = SyncSqliteStorage(connection=sqlite3.connect(":memory:"), default_ttl=0) entry = storage.create_entry( request=Request(method="GET", url="https://example.com"), response=Response(status_code=200, stream=make_sync_iterator([b"data"])), key="expired_key", id_=uuid.UUID(int=11), ) entry.response.read() # Verify the entry is expired, so get_entries should skip it entries = storage.get_entries("expired_key") assert len(entries) == 0 def test_soft_deleted_entries() -> None: """Test soft-deleted entries""" storage = SyncSqliteStorage(connection=sqlite3.connect(":memory:")) entry = storage.create_entry( request=Request(method="GET", url="https://example.com"), response=Response(status_code=200, stream=make_sync_iterator([b"data"])), key="soft_deleted_key", id_=uuid.UUID(int=12), ) entry.response.read() # Soft delete the entry storage.remove_entry(entry.id) # Verify the entry is soft deleted, so get_entries should skip it entries = storage.get_entries("soft_deleted_key") assert len(entries) == 0 karpetrosyan-hishel-fd309b1/tests/_core/spec/000077500000000000000000000000001514113157400213055ustar00rootroot00000000000000karpetrosyan-hishel-fd309b1/tests/_core/spec/test_cache_miss.py000066400000000000000000000615721514113157400250270ustar00rootroot00000000000000""" Comprehensive tests for the CacheMiss state. Tests verify compliance with RFC 9111 Section 3: Storing Responses in Caches https://www.rfc-editor.org/rfc/rfc9111.html#section-3 Test Categories: --------------- 1. Transition to StoreAndUse state (cacheable responses) 2. Transition to CouldNotBeStored state (non-cacheable responses) 3. Metadata flags and observability 4. Edge cases and RFC 9111 compliance """ from datetime import datetime, timedelta from typing import Dict, Optional import pytest from hishel import Request, Response from hishel._core._headers import Headers from hishel._core._spec import ( HEURISTICALLY_CACHEABLE_STATUS_CODES, CacheMiss, CacheOptions, CouldNotBeStored, StoreAndUse, ) # ============================================================================= # Test Fixtures and Helpers # ============================================================================= @pytest.fixture def default_options() -> CacheOptions: """Default cache options (shared cache).""" return CacheOptions(shared=True, supported_methods=["GET", "HEAD"], allow_stale=False) @pytest.fixture def private_cache_options() -> CacheOptions: """Private cache options (like a browser cache).""" return CacheOptions(shared=False, supported_methods=["GET", "HEAD"], allow_stale=False) def create_request( method: str = "GET", url: str = "https://example.com/resource", headers: Optional[Dict[str, str]] = None, ) -> Request: """Helper to create a request.""" return Request( method=method, url=url, headers=Headers(headers or {}), metadata={}, ) def create_response( status_code: int = 200, headers: Optional[Dict[str, str]] = None, ) -> Response: """Helper to create a response.""" return Response( status_code=status_code, headers=Headers(headers or {}), metadata={}, ) # ============================================================================= # Test Suite 1: Transition to StoreAndUse (Cacheable Responses) # ============================================================================= class TestTransitionToStoreAndUse: """ Tests for successful storage transitions. StoreAndUse occurs when: - All RFC 9111 Section 3 storage requirements are met - Response contains explicit caching directives or is heuristically cacheable """ def test_response_with_max_age_is_stored(self, default_options: CacheOptions) -> None: """ Test: Response with max-age directive is stored. RFC 9111 Section 5.2.2.1: max-age Response Directive "The max-age response directive indicates that the response is to be considered stale after its age is greater than the specified number of seconds." max-age is the most common explicit caching directive. """ # Arrange request = create_request() cache_miss = CacheMiss(request=request, options=default_options) response = create_response(headers={"cache-control": "max-age=3600"}) # Act next_state = cache_miss.next(response) # Assert assert isinstance(next_state, StoreAndUse) assert response.metadata.get("hishel_stored") is True assert response.metadata.get("hishel_from_cache") is False def test_response_with_expires_header_is_stored(self, default_options: CacheOptions) -> None: """ Test: Response with Expires header is stored. RFC 9111 Section 5.3: Expires "The Expires header field gives the date/time after which the response is considered stale." Expires is an older but still valid caching mechanism. """ # Arrange request = create_request() cache_miss = CacheMiss(request=request, options=default_options) future_date = (datetime.utcnow() + timedelta(hours=1)).strftime("%a, %d %b %Y %H:%M:%S GMT") response = create_response(headers={"expires": future_date}) # Act next_state = cache_miss.next(response) # Assert assert isinstance(next_state, StoreAndUse) assert response.metadata.get("hishel_stored") is True def test_response_with_public_directive_is_stored(self, default_options: CacheOptions) -> None: """ Test: Response with public directive is stored. RFC 9111 Section 5.2.2.9: public Response Directive "The public response directive indicates that a cache MAY store the response even if it would otherwise be prohibited." public explicitly marks a response as cacheable. """ # Arrange request = create_request() cache_miss = CacheMiss(request=request, options=default_options) response = create_response(headers={"cache-control": "public"}) # Act next_state = cache_miss.next(response) # Assert assert isinstance(next_state, StoreAndUse) assert response.metadata.get("hishel_stored") is True def test_response_with_s_maxage_is_stored_in_shared_cache(self, default_options: CacheOptions) -> None: """ Test: Response with s-maxage is stored in shared cache. RFC 9111 Section 5.2.2.10: s-maxage Response Directive "The s-maxage response directive indicates that, for a shared cache, the maximum age specified by this directive overrides the maximum age specified by either the max-age directive or the Expires header field." s-maxage is specific to shared caches (proxies, CDNs). """ # Arrange request = create_request() cache_miss = CacheMiss(request=request, options=default_options) response = create_response(headers={"cache-control": "s-maxage=7200"}) # Act next_state = cache_miss.next(response) # Assert assert isinstance(next_state, StoreAndUse) assert response.metadata.get("hishel_stored") is True def test_private_response_stored_in_private_cache(self, private_cache_options: CacheOptions) -> None: """ Test: Response with private directive is stored in private cache. RFC 9111 Section 5.2.2.7: private Response Directive "The unqualified private response directive... indicates that... a private cache MAY store the response." Private caches (browser caches) can store private responses. """ # Arrange request = create_request() cache_miss = CacheMiss(request=request, options=private_cache_options) response = create_response(headers={"cache-control": "private, max-age=3600"}) # Act next_state = cache_miss.next(response) # Assert assert isinstance(next_state, StoreAndUse) assert response.metadata.get("hishel_stored") is True @pytest.mark.parametrize("status_code", HEURISTICALLY_CACHEABLE_STATUS_CODES) def test_heuristically_cacheable_status_codes_are_stored( self, default_options: CacheOptions, status_code: int, ) -> None: """ Test: Responses with heuristically cacheable status codes are stored. RFC 9111 Section 4.2.2: Calculating Heuristic Freshness "a cache MAY assign a heuristic expiration time when an explicit time is not specified" Certain status codes are defined as cacheable even without explicit caching directives: 200, 203, 204, 300, 301, 308, 404, 405, 410, 414, 501 """ # Arrange request = create_request() cache_miss = CacheMiss(request=request, options=default_options) # Response with heuristically cacheable status, no explicit caching directives response = create_response(status_code=status_code) # Act next_state = cache_miss.next(response) # Assert assert isinstance(next_state, StoreAndUse) assert response.metadata.get("hishel_stored") is True def test_response_after_revalidation_marked_correctly(self, default_options: CacheOptions) -> None: """ Test: Response received after revalidation is marked with metadata. The after_revalidation flag tracks whether this cache miss occurred during a revalidation attempt, which is important for observability. """ # Arrange request = create_request() cache_miss = CacheMiss( request=request, options=default_options, after_revalidation=True, # This was a revalidation attempt ) response = create_response(headers={"cache-control": "max-age=3600"}) # Act next_state = cache_miss.next(response) # Assert assert isinstance(next_state, StoreAndUse) assert response.metadata.get("hishel_revalidated") is True def test_response_with_multiple_caching_directives(self, default_options: CacheOptions) -> None: """ Test: Response with multiple caching directives is stored. Real-world responses often include multiple caching directives. The response should be stored if at least one valid directive is present. """ # Arrange request = create_request() cache_miss = CacheMiss(request=request, options=default_options) response = create_response( headers={ "cache-control": "public, max-age=3600, s-maxage=7200", "expires": (datetime.utcnow() + timedelta(hours=2)).strftime("%a, %d %b %Y %H:%M:%S GMT"), } ) # Act next_state = cache_miss.next(response) # Assert assert isinstance(next_state, StoreAndUse) # ============================================================================= # Test Suite 2: Transition to CouldNotBeStored (Non-Cacheable Responses) # ============================================================================= class TestTransitionToCouldNotBeStored: """ Tests for failed storage transitions. CouldNotBeStored occurs when: - One or more RFC 9111 Section 3 storage requirements fail - Response explicitly prohibits caching """ def test_unsupported_method_cannot_be_stored(self, default_options: CacheOptions) -> None: """ Test: Response to unsupported method cannot be stored. RFC 9111 Section 3, paragraph 2.1: "the request method is understood by the cache" By default, only GET and HEAD are supported for caching. """ # Arrange # POST is not in supported_methods request = create_request(method="POST") cache_miss = CacheMiss(request=request, options=default_options) response = create_response(headers={"cache-control": "max-age=3600"}) # Act next_state = cache_miss.next(response) # Assert assert isinstance(next_state, CouldNotBeStored) assert response.metadata.get("hishel_stored") is False @pytest.mark.parametrize("status_code", [100, 101, 102, 103]) def test_informational_status_codes_cannot_be_stored( self, default_options: CacheOptions, status_code: int, ) -> None: """ Test: 1xx informational responses cannot be stored. RFC 9111 Section 3, paragraph 2.2: "the response status code is final (see Section 15 of [HTTP])" 1xx status codes are informational and not final responses. They indicate the request is still being processed. """ # Arrange request = create_request() cache_miss = CacheMiss(request=request, options=default_options) response = create_response(status_code=status_code, headers={"cache-control": "max-age=3600"}) # Act next_state = cache_miss.next(response) # Assert assert isinstance(next_state, CouldNotBeStored) assert response.metadata.get("hishel_stored") is False @pytest.mark.parametrize("status_code", [206, 304]) def test_special_status_codes_cannot_be_stored( self, default_options: CacheOptions, status_code: int, ) -> None: """ Test: 206 and 304 status codes cannot be stored (simplified implementation). RFC 9111 Section 3, paragraph 2.3: "if the response status code is 206 or 304, or the must-understand cache directive is present: the cache understands the response status code" 206 Partial Content: Requires special handling for range requests 304 Not Modified: Used for conditional requests, updates existing cache This implementation marks them as not understood for simplicity. """ # Arrange request = create_request() cache_miss = CacheMiss(request=request, options=default_options) response = create_response(status_code=status_code, headers={"cache-control": "max-age=3600"}) # Act next_state = cache_miss.next(response) # Assert assert isinstance(next_state, CouldNotBeStored) def test_no_store_directive_prevents_storage(self, default_options: CacheOptions) -> None: """ Test: no-store directive prevents storage. RFC 9111 Section 5.2.2.5: no-store Response Directive "The no-store response directive indicates that a cache MUST NOT store any part of either the immediate request or the response" no-store is the strongest cache prevention directive. """ # Arrange request = create_request() cache_miss = CacheMiss(request=request, options=default_options) response = create_response(headers={"cache-control": "no-store"}) # Act next_state = cache_miss.next(response) # Assert assert isinstance(next_state, CouldNotBeStored) assert response.metadata.get("hishel_stored") is False def test_private_directive_prevents_storage_in_shared_cache(self, default_options: CacheOptions) -> None: """ Test: private directive prevents storage in shared cache. RFC 9111 Section 5.2.2.7: private Response Directive "The unqualified private response directive indicates that a shared cache MUST NOT store the response" Shared caches (proxies, CDNs) cannot store private responses. """ # Arrange request = create_request() # default_options is a shared cache cache_miss = CacheMiss(request=request, options=default_options) response = create_response(headers={"cache-control": "private, max-age=3600"}) # Act next_state = cache_miss.next(response) # Assert assert isinstance(next_state, CouldNotBeStored) assert response.metadata.get("hishel_stored") is False def test_authorization_header_prevents_storage_in_shared_cache(self, default_options: CacheOptions) -> None: """ Test: Authorization header prevents storage in shared cache. RFC 9111 Section 3.5: Caching Authenticated Responses "A shared cache MUST NOT use a cached response to a request with an Authorization header field unless... a response directive is present that explicitly allows shared caching" Requests with Authorization often contain user-specific data. """ # Arrange request = create_request(headers={"authorization": "Bearer token123"}) # default_options is a shared cache cache_miss = CacheMiss(request=request, options=default_options) response = create_response(headers={"cache-control": "max-age=3600"}) # Act next_state = cache_miss.next(response) # Assert assert isinstance(next_state, CouldNotBeStored) def test_response_without_caching_metadata_and_non_cacheable_status(self, default_options: CacheOptions) -> None: """ Test: Response without caching metadata and non-cacheable status cannot be stored. RFC 9111 Section 3, paragraph 2.7: "the response contains at least one of the following: - a public response directive - a private response directive (if cache is not shared) - an Expires header field - a max-age response directive - an s-maxage response directive (if cache is shared) - a status code that is defined as heuristically cacheable" Without explicit caching directives or a heuristically cacheable status, the response cannot be stored. """ # Arrange request = create_request() cache_miss = CacheMiss(request=request, options=default_options) # 201 Created is not heuristically cacheable response = create_response(status_code=201) # Act next_state = cache_miss.next(response) # Assert assert isinstance(next_state, CouldNotBeStored) assert response.metadata.get("hishel_stored") is False def test_response_with_no_store_overrides_other_directives(self, default_options: CacheOptions) -> None: """ Test: no-store overrides other caching directives. RFC 9111 Section 5.2.2.5: no-store Response Directive no-store is the strongest directive and must be honored even if other caching directives are present. """ # Arrange request = create_request() cache_miss = CacheMiss(request=request, options=default_options) # Response has both max-age and no-store response = create_response(headers={"cache-control": "max-age=3600, no-store"}) # Act next_state = cache_miss.next(response) # Assert assert isinstance(next_state, CouldNotBeStored) # ============================================================================= # Test Suite 3: Metadata and Observability # ============================================================================= class TestMetadataAndObservability: """ Tests for metadata flags that provide observability into cache behavior. """ def test_metadata_flags_set_on_stored_response(self, default_options: CacheOptions) -> None: """ Test: Correct metadata flags are set when response is stored. Metadata flags help track the lifecycle and behavior of cached responses. """ # Arrange request = create_request() cache_miss = CacheMiss(request=request, options=default_options) response = create_response(headers={"cache-control": "max-age=3600"}) # Act next_state = cache_miss.next(response) # Assert assert isinstance(next_state, StoreAndUse) assert response.metadata.get("hishel_from_cache") is False assert response.metadata.get("hishel_stored") is True def test_metadata_flags_set_on_not_stored_response(self, default_options: CacheOptions) -> None: """ Test: Correct metadata flags are set when response cannot be stored. """ # Arrange request = create_request() cache_miss = CacheMiss(request=request, options=default_options) response = create_response(headers={"cache-control": "no-store"}) # Act next_state = cache_miss.next(response) # Assert assert isinstance(next_state, CouldNotBeStored) assert response.metadata.get("hishel_from_cache") is False assert response.metadata.get("hishel_stored") is False def test_revalidation_flag_set_when_after_revalidation(self, default_options: CacheOptions) -> None: """ Test: hishel_revalidated flag is set when after_revalidation is True. This helps track which responses were obtained through revalidation vs initial cache misses. """ # Arrange request = create_request() cache_miss = CacheMiss(request=request, options=default_options, after_revalidation=True) response = create_response(headers={"cache-control": "max-age=3600"}) # Act cache_miss.next(response) # Assert assert response.metadata.get("hishel_revalidated") is True def test_revalidation_flag_not_set_when_not_after_revalidation(self, default_options: CacheOptions) -> None: """ Test: hishel_revalidated flag is not set for normal cache misses. """ # Arrange request = create_request() cache_miss = CacheMiss(request=request, options=default_options, after_revalidation=False) response = create_response(headers={"cache-control": "max-age=3600"}) # Act cache_miss.next(response) # Assert assert response.metadata.get("hishel_revalidated") is False # ============================================================================= # Test Suite 4: Edge Cases and Cache Type Differences # ============================================================================= class TestEdgeCasesAndCacheTypes: """ Tests for edge cases and differences between shared and private caches. """ def test_shared_cache_vs_private_cache_with_private_directive(self) -> None: """ Test: private directive behavior differs between shared and private caches. RFC 9111 Section 5.2.2.7: private Response Directive - Shared cache: MUST NOT store - Private cache: MAY store """ request = create_request() response = create_response(headers={"cache-control": "private, max-age=3600"}) # Test with shared cache shared_options = CacheOptions(shared=True, supported_methods=["GET", "HEAD"]) cache_miss_shared = CacheMiss(request=request, options=shared_options) next_state_shared = cache_miss_shared.next(response) assert isinstance(next_state_shared, CouldNotBeStored) # Test with private cache private_options = CacheOptions(shared=False, supported_methods=["GET", "HEAD"]) cache_miss_private = CacheMiss(request=request, options=private_options) # Create fresh response for private cache test response_private = create_response(headers={"cache-control": "private, max-age=3600"}) next_state_private = cache_miss_private.next(response_private) assert isinstance(next_state_private, StoreAndUse) def test_s_maxage_only_applies_to_shared_cache(self) -> None: """ Test: s-maxage directive only provides caching metadata for shared caches. RFC 9111 Section 5.2.2.10: s-maxage Response Directive "The s-maxage response directive indicates that, for a shared cache..." For private caches, s-maxage alone is not sufficient. """ request = create_request() # Response with only s-maxage (no other caching directives) response_shared = create_response( status_code=201, # Not heuristically cacheable headers={"cache-control": "s-maxage=3600"}, ) response_private = create_response( status_code=201, # Not heuristically cacheable headers={"cache-control": "s-maxage=3600"}, ) # Test with shared cache - should be stored shared_options = CacheOptions(shared=True, supported_methods=["GET", "HEAD"]) cache_miss_shared = CacheMiss(request=request, options=shared_options) next_state_shared = cache_miss_shared.next(response_shared) assert isinstance(next_state_shared, StoreAndUse) # Test with private cache - should NOT be stored (s-maxage doesn't apply) private_options = CacheOptions(shared=False, supported_methods=["GET", "HEAD"]) cache_miss_private = CacheMiss(request=request, options=private_options) next_state_private = cache_miss_private.next(response_private) assert isinstance(next_state_private, CouldNotBeStored) def test_options_propagated_to_next_state(self, default_options: CacheOptions) -> None: """ Test: Cache options are propagated to the next state. """ request = create_request() cache_miss = CacheMiss(request=request, options=default_options) # Test StoreAndUse response_storable = create_response(headers={"cache-control": "max-age=3600"}) next_state_store = cache_miss.next(response_storable) assert next_state_store.options == default_options # Test CouldNotBeStored response_not_storable = create_response(headers={"cache-control": "no-store"}) next_state_not_store = cache_miss.next(response_not_storable) assert next_state_not_store.options == default_options def test_response_with_no_cache_control_header(self, default_options: CacheOptions) -> None: """ Test: Response without Cache-Control header. Without Cache-Control, the response can still be cached if it has other caching metadata (Expires) or is heuristically cacheable. """ # Arrange request = create_request() cache_miss = CacheMiss(request=request, options=default_options) # Heuristically cacheable status, no cache-control response = create_response(status_code=200) # Act next_state = cache_miss.next(response) # Assert assert isinstance(next_state, StoreAndUse) karpetrosyan-hishel-fd309b1/tests/_core/spec/test_helper_functions.py000066400000000000000000000760351514113157400263000ustar00rootroot00000000000000""" Comprehensive tests for cache helper functions. Tests verify compliance with various sections of RFC 9111 https://www.rfc-editor.org/rfc/rfc9111.html Test Categories: --------------- 1. vary_headers_match - RFC 9111 Section 4.1 2. get_freshness_lifetime - RFC 9111 Section 4.2.1 3. allowed_stale - RFC 9111 Section 4.2.4 4. get_heuristic_freshness - RFC 9111 Section 4.2.2 5. get_age - RFC 9111 Section 4.2.3 6. make_conditional_request - RFC 9111 Section 4.3.1 7. exclude_unstorable_headers - RFC 9111 Section 3.1 8. refresh_response_headers - RFC 9111 Section 3.2 """ import uuid from datetime import datetime, timedelta from typing import Dict, Optional import pytest from hishel import Entry, EntryMeta, Request, Response from hishel._core._headers import Headers from hishel._core._spec import ( allowed_stale, exclude_unstorable_headers, get_age, get_freshness_lifetime, get_heuristic_freshness, make_conditional_request, refresh_response_headers, vary_headers_match, ) # ============================================================================= # Test Helpers # ============================================================================= def create_request( method: str = "GET", url: str = "https://example.com/resource", headers: Optional[Dict[str, str]] = None, ) -> Request: """Helper to create a request.""" return Request( method=method, url=url, headers=Headers(headers or {}), metadata={}, ) def create_response( status_code: int = 200, headers: Optional[Dict[str, str]] = None, ) -> Response: """Helper to create a response.""" return Response( status_code=status_code, headers=Headers(headers or {}), metadata={}, ) def create_pair( request: Optional[Request] = None, response: Optional[Response] = None, ) -> Entry: """Helper to create a request-response pair.""" return Entry( id=uuid.uuid4(), request=request or create_request(), response=response or create_response(), meta=EntryMeta(), cache_key=b"test-cache-key", ) # ============================================================================= # Test Suite 1: vary_headers_match # ============================================================================= class TestVaryHeadersMatch: """ Tests for vary_headers_match function. RFC 9111 Section 4.1: Calculating Cache Keys """ def test_no_vary_header_always_matches(self) -> None: """ Test: Responses without Vary header match any request. RFC 9111 Section 4.1: Without a Vary header, the response doesn't vary based on request headers. """ # Arrange request1 = create_request(headers={"accept": "application/json"}) request2 = create_request(headers={"accept": "application/xml"}) response = create_response() # No Vary header pair = create_pair(request=request1, response=response) # Act & Assert assert vary_headers_match(request2, pair) is True def test_vary_header_with_matching_values(self) -> None: """ Test: Matching Vary header values allow cache reuse. RFC 9111 Section 4.1: "all the presented request header fields nominated by that Vary field value match those fields in the original request" """ # Arrange headers = {"accept": "application/json"} request1 = create_request(headers=headers) request2 = create_request(headers=headers) # Same headers response = create_response(headers={"vary": "Accept"}) pair = create_pair(request=request1, response=response) # Act & Assert assert vary_headers_match(request2, pair) is True def test_vary_header_with_non_matching_values(self) -> None: """ Test: Non-matching Vary header values prevent cache reuse. """ # Arrange request1 = create_request(headers={"accept": "application/json"}) request2 = create_request(headers={"accept": "application/xml"}) response = create_response(headers={"vary": "Accept"}) pair = create_pair(request=request1, response=response) # Act & Assert assert vary_headers_match(request2, pair) is False def test_vary_star_always_fails(self) -> None: """ Test: Vary: * always fails to match. RFC 9111 Section 4.1: "A stored response with a Vary header field value containing a member '*' always fails to match." """ # Arrange request1 = create_request(headers={"accept": "application/json"}) request2 = create_request(headers={"accept": "application/json"}) response = create_response(headers={"vary": "*"}) pair = create_pair(request=request1, response=response) # Act & Assert assert vary_headers_match(request2, pair) is False def test_vary_multiple_headers_all_must_match(self) -> None: """ Test: With multiple Vary headers, all must match. RFC 9111 Section 4.1: All nominated headers must match for the response to be reusable. """ # Arrange headers1 = {"accept": "application/json", "accept-encoding": "gzip"} headers2 = { "accept": "application/json", "accept-encoding": "gzip", # All match } headers3 = { "accept": "application/json", "accept-encoding": "br", # One doesn't match } request1 = create_request(headers=headers1) request2 = create_request(headers=headers2) request3 = create_request(headers=headers3) response = create_response(headers={"vary": "Accept, Accept-Encoding"}) pair = create_pair(request=request1, response=response) # Act & Assert assert vary_headers_match(request2, pair) is True # All match assert vary_headers_match(request3, pair) is False # One differs def test_vary_with_missing_header_in_both_requests(self) -> None: """ Test: Missing headers in both requests are considered matching. RFC 9111 Section 4.1: "If... a header field is absent from a request, it can only match another request if it is also absent there." """ # Arrange request1 = create_request(headers={}) # No Accept-Language request2 = create_request(headers={}) # No Accept-Language response = create_response(headers={"vary": "Accept-Language"}) pair = create_pair(request=request1, response=response) # Act & Assert assert vary_headers_match(request2, pair) is True def test_vary_with_header_present_in_one_request_only(self) -> None: """ Test: Header present in one request but not the other is non-matching. """ # Arrange request1 = create_request(headers={"accept-language": "en-US"}) request2 = create_request(headers={}) # No Accept-Language response = create_response(headers={"vary": "Accept-Language"}) pair = create_pair(request=request1, response=response) # Act & Assert assert vary_headers_match(request2, pair) is False # ============================================================================= # Test Suite 2: get_freshness_lifetime # ============================================================================= class TestGetFreshnessLifetime: """ Tests for get_freshness_lifetime function. RFC 9111 Section 4.2.1: Calculating Freshness Lifetime """ def test_max_age_directive(self) -> None: """ Test: max-age directive provides freshness lifetime. RFC 9111 Section 5.2.2.1: max-age Response Directive """ # Arrange response = create_response(headers={"cache-control": "max-age=3600"}) # Act lifetime = get_freshness_lifetime(response, is_cache_shared=True) # Assert assert lifetime == 3600 def test_s_maxage_overrides_max_age_for_shared_cache(self) -> None: """ Test: s-maxage overrides max-age for shared caches. RFC 9111 Section 5.2.2.10: s-maxage Response Directive "for a shared cache, the maximum age specified by this directive overrides the maximum age specified by either the max-age directive or the Expires header field" """ # Arrange response = create_response(headers={"cache-control": "max-age=3600, s-maxage=7200"}) # Act shared_lifetime = get_freshness_lifetime(response, is_cache_shared=True) private_lifetime = get_freshness_lifetime(response, is_cache_shared=False) # Assert assert shared_lifetime == 7200 # s-maxage for shared assert private_lifetime == 3600 # max-age for private def test_expires_header_provides_freshness(self) -> None: """ Test: Expires header provides freshness lifetime. RFC 9111 Section 5.3: Expires """ # Arrange now = datetime.utcnow() expires = (now + timedelta(hours=2)).strftime("%a, %d %b %Y %H:%M:%S GMT") date = now.strftime("%a, %d %b %Y %H:%M:%S GMT") response = create_response(headers={"expires": expires, "date": date}) # Act lifetime = get_freshness_lifetime(response, is_cache_shared=True) # Assert assert lifetime is not None # Should be approximately 2 hours (7200 seconds) assert 7190 <= lifetime <= 7210 # Allow small timing variance def test_max_age_takes_precedence_over_expires(self) -> None: """ Test: max-age takes precedence over Expires. RFC 9111 Section 5.2.2.1: max-age is evaluated before Expires in the priority order. """ # Arrange expires = (datetime.utcnow() + timedelta(hours=2)).strftime("%a, %d %b %Y %H:%M:%S GMT") response = create_response(headers={"cache-control": "max-age=3600", "expires": expires}) # Act lifetime = get_freshness_lifetime(response, is_cache_shared=True) # Assert assert lifetime == 3600 # max-age, not Expires def test_heuristic_freshness_when_no_explicit_expiration(self) -> None: """ Test: Heuristic freshness is used when no explicit expiration exists. RFC 9111 Section 4.2.2: Calculating Heuristic Freshness """ # Arrange last_modified = (datetime.utcnow() - timedelta(days=10)).strftime("%a, %d %b %Y %H:%M:%S GMT") response = create_response(headers={"last-modified": last_modified}) # Act lifetime = get_freshness_lifetime(response, is_cache_shared=True) # Assert assert lifetime is not None # Should be ~10% of 10 days = 1 day = 86400 seconds assert 80000 <= lifetime <= 90000 def test_no_freshness_info_returns_none(self) -> None: """ Test: Returns None when no freshness information is available. """ # Arrange response = create_response() # No caching headers # Act lifetime = get_freshness_lifetime(response, is_cache_shared=True) # Assert assert lifetime is None # ============================================================================= # Test Suite 3: allowed_stale # ============================================================================= class TestAllowedStale: """ Tests for allowed_stale function. RFC 9111 Section 4.2.4: Serving Stale Responses """ def test_allow_stale_option_false_disallows_stale(self) -> None: """ Test: Configuration disallows serving stale responses. RFC 9111 Section 4.2.4: "A cache MUST NOT generate a stale response unless... doing so is explicitly permitted by the client or origin server" """ # Arrange response = create_response(headers={"cache-control": "max-age=3600"}) # Act result = allowed_stale(response, allow_stale_option=False) # Assert assert result is False def test_no_cache_directive_prevents_serving_stale(self) -> None: """ Test: no-cache directive prevents serving stale responses. RFC 9111 Section 5.2.2.4: no-cache Response Directive """ # Arrange response = create_response(headers={"cache-control": "max-age=3600, no-cache"}) # Act result = allowed_stale(response, allow_stale_option=True) # Assert assert result is False def test_must_revalidate_prevents_serving_stale(self) -> None: """ Test: must-revalidate directive prevents serving stale responses. RFC 9111 Section 5.2.2.2: must-revalidate Response Directive "once the response has become stale, a cache MUST NOT reuse that response to satisfy another request until it has been successfully validated by the origin" """ # Arrange response = create_response(headers={"cache-control": "max-age=3600, must-revalidate"}) # Act result = allowed_stale(response, allow_stale_option=True) # Assert assert result is False def test_allowed_stale_with_permissive_configuration(self) -> None: """ Test: Stale responses allowed with permissive configuration. """ # Arrange response = create_response(headers={"cache-control": "max-age=3600"}) # Act result = allowed_stale(response, allow_stale_option=True) # Assert assert result is True # ============================================================================= # Test Suite 4: get_heuristic_freshness # ============================================================================= class TestGetHeuristicFreshness: """ Tests for get_heuristic_freshness function. RFC 9111 Section 4.2.2: Calculating Heuristic Freshness """ def test_heuristic_freshness_from_last_modified(self) -> None: """ Test: Heuristic freshness calculated as 10% of age since Last-Modified. RFC 9111 Section 4.2.2: "caches are encouraged to use a heuristic expiration value that is no more than some fraction of the interval since that time. A typical setting of this fraction might be 10%." """ # Arrange # Resource last modified 10 days ago last_modified = (datetime.utcnow() - timedelta(days=10)).strftime("%a, %d %b %Y %H:%M:%S GMT") response = create_response(headers={"last-modified": last_modified}) # Act freshness = get_heuristic_freshness(response) # Assert # 10% of 10 days = 1 day = 86400 seconds assert freshness is not None assert 80000 <= freshness <= 90000 # Allow timing variance def test_heuristic_freshness_capped_at_one_week(self) -> None: """ Test: Heuristic freshness is capped at one week maximum. RFC 9111 Section 4.2.2: Conservative maximum to avoid excessive staleness. """ # Arrange # Resource last modified 1000 days ago (would be 100 days * 0.1 = 10 days) last_modified = (datetime.utcnow() - timedelta(days=1000)).strftime("%a, %d %b %Y %H:%M:%S GMT") response = create_response(headers={"last-modified": last_modified}) # Act freshness = get_heuristic_freshness(response) # Assert ONE_WEEK = 604_800 assert freshness == ONE_WEEK def test_no_last_modified_returns_none(self) -> None: """ Test: Returns None when Last-Modified header is absent. """ # Arrange response = create_response() # No Last-Modified # Act freshness = get_heuristic_freshness(response) # Assert assert freshness is None # ============================================================================= # Test Suite 5: get_age # ============================================================================= class TestGetAge: """ Tests for get_age function. RFC 9111 Section 4.2.3: Calculating Age """ def test_age_calculation_from_date_header(self) -> None: """ Test: Age is calculated from Date header. RFC 9111 Section 4.2.3: "A response's 'age' is the time that has passed since it was generated by, or successfully validated with, the origin server." """ # Arrange # Response from 1 hour ago date = (datetime.utcnow() - timedelta(hours=1)).strftime("%a, %d %b %Y %H:%M:%S GMT") response = create_response(headers={"date": date}) # Act age = get_age(response) # Assert # Should be approximately 3600 seconds (1 hour) assert 3590 <= age <= 3610 def test_age_is_zero_without_date_header(self) -> None: """ Test: Age is 0 when Date header is missing. RFC 9110 Section 6.6.1: Date "A recipient with a clock that receives a response with an invalid Date header field value MAY replace that value with the time that response was received." """ # Arrange response = create_response() # No Date header # Act age = get_age(response) # Assert assert age == 0 def test_age_is_zero_for_future_date(self) -> None: """ Test: Age is 0 when Date is in the future (clock skew protection). """ # Arrange # Date 1 hour in the future future_date = (datetime.utcnow() + timedelta(hours=1)).strftime("%a, %d %b %Y %H:%M:%S GMT") response = create_response(headers={"date": future_date}) # Act age = get_age(response) # Assert assert age == 0 # max(0, now - future) = 0 # ============================================================================= # Test Suite 6: make_conditional_request # ============================================================================= class TestMakeConditionalRequest: """ Tests for make_conditional_request function. RFC 9111 Section 4.3.1: Sending a Validation Request """ def test_adds_if_none_match_from_etag(self) -> None: """ Test: If-None-Match is added when response has ETag. RFC 9111 Section 4.3.1: "MUST send the relevant entity tags (using If-Match, If-None-Match, or If-Range) if the entity tags were provided in the stored response(s) being validated." """ # Arrange request = create_request() response = create_response(headers={"etag": '"abc123"'}) # Act conditional = make_conditional_request(request, response) # Assert assert "if-none-match" in conditional.headers assert conditional.headers["if-none-match"] == '"abc123"' def test_adds_if_modified_since_from_last_modified(self) -> None: """ Test: If-Modified-Since is added when response has Last-Modified. RFC 9111 Section 4.3.1: "SHOULD send the Last-Modified value (using If-Modified-Since)" """ # Arrange request = create_request() response = create_response(headers={"last-modified": "Mon, 01 Jan 2024 00:00:00 GMT"}) # Act conditional = make_conditional_request(request, response) # Assert assert "if-modified-since" in conditional.headers assert conditional.headers["if-modified-since"] == "Mon, 01 Jan 2024 00:00:00 GMT" def test_adds_both_validators_when_available(self) -> None: """ Test: Both If-None-Match and If-Modified-Since are added when available. RFC 9111 Section 4.3.1: Both validators should be sent if available for maximum compatibility. """ # Arrange request = create_request() response = create_response( headers={ "etag": '"abc123"', "last-modified": "Mon, 01 Jan 2024 00:00:00 GMT", } ) # Act conditional = make_conditional_request(request, response) # Assert assert "if-none-match" in conditional.headers assert "if-modified-since" in conditional.headers def test_preserves_original_request_headers(self) -> None: """ Test: Original request headers are preserved. """ # Arrange request = create_request( headers={ "accept": "application/json", "authorization": "Bearer token", } ) response = create_response(headers={"etag": '"abc123"'}) # Act conditional = make_conditional_request(request, response) # Assert assert conditional.headers["accept"] == "application/json" assert conditional.headers["authorization"] == "Bearer token" assert "if-none-match" in conditional.headers def test_no_validators_returns_unchanged_request(self) -> None: """ Test: Request is unchanged when response has no validators. """ # Arrange request = create_request(headers={"accept": "application/json"}) response = create_response() # No ETag or Last-Modified # Act conditional = make_conditional_request(request, response) # Assert assert "if-none-match" not in conditional.headers assert "if-modified-since" not in conditional.headers assert conditional.headers["accept"] == "application/json" # ============================================================================= # Test Suite 7: exclude_unstorable_headers # ============================================================================= class TestExcludeUnstorableHeaders: """ Tests for exclude_unstorable_headers function. RFC 9111 Section 3.1: Storing Header and Trailer Fields """ @pytest.mark.parametrize( "header", [ "connection", "keep-alive", "proxy-connection", "transfer-encoding", "upgrade", "te", "proxy-authenticate", "proxy-authorization", ], ) def test_connection_specific_headers_excluded(self, header: str) -> None: """ Test: Connection-specific headers are excluded. RFC 9110 Section 7.6.1: Connection-Specific Header Fields """ # Arrange response = create_response( headers={ header: "value", "cache-control": "max-age=3600", "content-type": "application/json", } ) # Act filtered = exclude_unstorable_headers(response, is_cache_shared=True) # Assert assert header not in filtered.headers assert "cache-control" in filtered.headers assert "content-type" in filtered.headers def test_no_cache_directive_with_field_names(self) -> None: """ Test: Headers listed in no-cache directive are excluded. RFC 9111 Section 5.2.2.4: no-cache Response Directive 'no-cache="Set-Cookie"' means exclude Set-Cookie from cache. """ # Arrange response = create_response( headers={ "cache-control": 'no-cache="Set-Cookie"', "set-cookie": "session=abc123", "content-type": "application/json", } ) # Act filtered = exclude_unstorable_headers(response, is_cache_shared=True) # Assert assert "set-cookie" not in filtered.headers assert "content-type" in filtered.headers def test_private_directive_with_field_names_in_shared_cache(self) -> None: """ Test: Headers listed in private directive are excluded from shared cache. RFC 9111 Section 5.2.2.7: private Response Directive 'private="Authorization"' means shared caches must exclude Authorization. """ # Arrange response = create_response( headers={ "cache-control": 'private="Authorization"', "authorization": "Bearer token", "content-type": "application/json", } ) # Act shared_filtered = exclude_unstorable_headers(response, is_cache_shared=True) private_filtered = exclude_unstorable_headers(response, is_cache_shared=False) # Assert # Shared cache: Authorization excluded assert "authorization" not in shared_filtered.headers # Private cache: Authorization kept (private directive doesn't apply) assert "authorization" in private_filtered.headers def test_normal_headers_preserved(self) -> None: """ Test: Normal cacheable headers are preserved. """ # Arrange response = create_response( headers={ "content-type": "application/json", "cache-control": "max-age=3600", "etag": '"abc123"', "last-modified": "Mon, 01 Jan 2024 00:00:00 GMT", } ) # Act filtered = exclude_unstorable_headers(response, is_cache_shared=True) # Assert assert "content-type" in filtered.headers assert "cache-control" in filtered.headers assert "etag" in filtered.headers assert "last-modified" in filtered.headers # ============================================================================= # Test Suite 8: refresh_response_headers for shared cache # ============================================================================= class TestRefreshResponseHeaders: """ Tests for refresh_response_headers function. RFC 9111 Section 3.2: Updating Stored Header Fields """ is_cache_shared = True def test_updates_cache_control_from_304(self) -> None: """ Test: Cache-Control is updated from 304 response. RFC 9111 Section 3.2: "the cache MUST add each header field in the provided response to the stored response, replacing field values that are already present" """ # Arrange stored = create_response( headers={ "cache-control": "max-age=3600", "content-type": "application/json", } ) revalidation = create_response(status_code=304, headers={"cache-control": "max-age=7200"}) # Act refreshed = refresh_response_headers(stored, revalidation, self.is_cache_shared) # Assert assert refreshed.headers["cache-control"] == "max-age=7200" def test_preserves_content_type_from_stored_response(self) -> None: """ Test: Content-Type is NOT updated from 304 response. RFC 9111 Section 3.2: "Header fields that provide metadata about the message content and/or the selected representation (e.g., Content-Encoding, Content-Type, Content-Range) MUST NOT be updated" """ # Arrange stored = create_response( headers={ "content-type": "application/json", "cache-control": "max-age=3600", } ) revalidation = create_response( status_code=304, headers={ "content-type": "text/plain", # Should be ignored "cache-control": "max-age=7200", }, ) # Act refreshed = refresh_response_headers(stored, revalidation, self.is_cache_shared) # Assert assert refreshed.headers["content-type"] == "application/json" # Preserved assert refreshed.headers["cache-control"] == "max-age=7200" # Updated def test_preserves_content_encoding_from_stored_response(self) -> None: """ Test: Content-Encoding is NOT updated from 304 response. """ # Arrange stored = create_response( headers={ "content-encoding": "gzip", "content-type": "application/json", } ) revalidation = create_response( status_code=304, headers={ "content-encoding": "br", # Should be ignored "cache-control": "max-age=7200", }, ) # Act refreshed = refresh_response_headers(stored, revalidation, self.is_cache_shared) # Assert assert refreshed.headers["content-encoding"] == "gzip" # Preserved def test_preserves_content_range_from_stored_response(self) -> None: """ Test: Content-Range is NOT updated from 304 response. """ # Arrange stored = create_response( headers={ "content-range": "bytes 0-1023/2048", "content-type": "application/octet-stream", } ) revalidation = create_response( status_code=304, headers={ "content-range": "bytes 1024-2047/2048", # Should be ignored "cache-control": "max-age=7200", }, ) # Act refreshed = refresh_response_headers(stored, revalidation, self.is_cache_shared) # Assert assert refreshed.headers["content-range"] == "bytes 0-1023/2048" # Preserved def test_updates_date_and_etag_from_304(self) -> None: """ Test: Non-content headers like Date and ETag are updated. """ # Arrange stored = create_response( headers={ "date": "Mon, 01 Jan 2024 00:00:00 GMT", "etag": '"old123"', "content-type": "application/json", } ) revalidation = create_response( status_code=304, headers={ "date": "Mon, 01 Jan 2024 12:00:00 GMT", "etag": '"new456"', }, ) # Act refreshed = refresh_response_headers(stored, revalidation, self.is_cache_shared) # Assert assert refreshed.headers["date"] == "Mon, 01 Jan 2024 12:00:00 GMT" assert refreshed.headers["etag"] == '"new456"' assert refreshed.headers["content-type"] == "application/json" def test_excludes_unstorable_headers_after_merge(self) -> None: """ Test: Unstorable headers are excluded after merging. Connection-specific headers in the 304 response should not be cached. """ # Arrange stored = create_response(headers={"content-type": "application/json"}) revalidation = create_response( status_code=304, headers={ "cache-control": "max-age=7200", "connection": "keep-alive", # Should be excluded "keep-alive": "timeout=5", # Should be excluded }, ) # Act refreshed = refresh_response_headers(stored, revalidation, self.is_cache_shared) # Assert assert "cache-control" in refreshed.headers assert "connection" not in refreshed.headers assert "keep-alive" not in refreshed.headers # ============================================================================= # Test Suite 9: refresh_response_headers for private cache # ============================================================================= class TestRefreshResponseHeadersPrivateCache(TestRefreshResponseHeaders): is_cache_shared = False karpetrosyan-hishel-fd309b1/tests/_core/spec/test_idle_client.py000066400000000000000000001213111514113157400251700ustar00rootroot00000000000000""" Comprehensive tests for the IdleClient state. Tests verify compliance with RFC 9111 Section 4: Constructing Responses from Caches https://www.rfc-editor.org/rfc/rfc9111.html#section-4 Test Categories: --------------- 1. Transition to CacheMiss state 2. Transition to FromCache state 3. Transition to NeedRevalidation state 4. Edge cases and RFC 9111 compliance """ import uuid from datetime import datetime, timedelta from typing import Any, Dict, Optional import pytest from hishel import Entry, EntryMeta, Request, Response from hishel._core._headers import Headers from hishel._core._spec import ( CacheMiss, CacheOptions, FromCache, IdleClient, NeedRevalidation, ) # ============================================================================= # Test Fixtures and Helpers # ============================================================================= @pytest.fixture def default_options() -> CacheOptions: """Default cache options for testing.""" return CacheOptions(shared=True, supported_methods=["GET", "HEAD"], allow_stale=False) @pytest.fixture def allow_stale_options() -> CacheOptions: """Cache options that allow serving stale responses.""" return CacheOptions(shared=True, supported_methods=["GET", "HEAD"], allow_stale=True) @pytest.fixture def idle_client(default_options: CacheOptions) -> IdleClient: """Create an IdleClient instance with default options.""" return IdleClient(options=default_options) @pytest.fixture def idle_client_allow_stale(allow_stale_options: CacheOptions) -> IdleClient: """Create an IdleClient instance that allows stale responses.""" return IdleClient(options=allow_stale_options) def create_request( method: str = "GET", url: str = "https://example.com/resource", headers: Optional[Dict[str, str]] = None, metadata: Optional[Dict[str, Any]] = None, ) -> Request: """Helper to create a request with common defaults.""" return Request( method=method, url=url, headers=Headers(headers or {}), metadata=metadata or {}, ) def create_response( status_code: int = 200, headers: Optional[Dict[str, str]] = None, age_seconds: int = 0, max_age_seconds: Optional[int] = 3600, metadata: Optional[Dict[str, Any]] = None, ) -> Response: """ Helper to create a response with cache-related headers. Parameters: ---------- status_code : int HTTP status code headers : dict Additional headers to include age_seconds : int Age of the response in seconds (for Date header calculation) max_age_seconds : int or None Max-age value for Cache-Control header (None to omit) metadata : dict Response metadata """ response_headers = headers or {} # Set Date header to current time minus age_seconds date_value = datetime.utcnow() - timedelta(seconds=age_seconds) response_headers.setdefault("date", date_value.strftime("%a, %d %b %Y %H:%M:%S GMT")) # Set Cache-Control with max-age if provided if max_age_seconds is not None: cache_control = response_headers.get("cache-control", "") if cache_control: cache_control += f", max-age={max_age_seconds}" else: cache_control = f"max-age={max_age_seconds}" response_headers["cache-control"] = cache_control return Response( status_code=status_code, headers=Headers(response_headers), metadata=metadata or {}, ) def create_pair( request: Optional[Request] = None, response: Optional[Response] = None, pair_id: Optional[uuid.UUID] = None, cache_key: bytes = b"test-cache-key", ) -> Entry: """Helper to create a request-response pair.""" return Entry( id=pair_id or uuid.uuid4(), request=request or create_request(), response=response or create_response(), meta=EntryMeta(), cache_key=cache_key, ) # ============================================================================= # Test Suite 1: Transition to CacheMiss State # ============================================================================= # RFC 9111 Section 4 defines conditions when a cache MUST NOT reuse a stored # response. These tests verify that CacheMiss is returned in appropriate cases. # ============================================================================= class TestTransitionToCacheMiss: """ Tests for transitions to the CacheMiss state. CacheMiss occurs when: - No suitable cached responses exist - The request cannot be satisfied from cache - RFC 9111 conditions for cache reuse are not met """ def test_no_cached_responses(self, idle_client: IdleClient) -> None: """ Test: Empty cache results in CacheMiss. RFC 9111 Section 4.1, paragraph 8: "If no stored response matches, the cache cannot satisfy the presented request. Typically, the request is forwarded to the origin server." """ # Arrange request = create_request() associated_pairs: list[Entry] = [] # Empty cache # Act next_state = idle_client.next(request, associated_pairs) # Assert assert isinstance(next_state, CacheMiss) assert next_state.request == request assert next_state.options == idle_client.options def test_range_request_causes_cache_miss(self, idle_client: IdleClient) -> None: """ Test: Requests with Range header result in CacheMiss. RFC 9111 Section 3.3: Storing Incomplete Responses https://www.rfc-editor.org/rfc/rfc9111.html#section-3.3 This implementation takes a simplified approach and treats all range requests as cache misses. A full implementation could handle partial content and range combinations. """ # Arrange request = create_request(headers={"range": "bytes=0-1023"}) cached_pair = create_pair(request=create_request(), response=create_response()) # Act next_state = idle_client.next(request, [cached_pair]) # Assert assert isinstance(next_state, CacheMiss) assert next_state.request == request def test_unsafe_method_causes_cache_miss(self, idle_client: IdleClient) -> None: """ Test: Unsafe methods (POST, PUT, DELETE, etc.) result in CacheMiss. RFC 9111 Section 4, paragraph 5: "A cache MUST write through requests with methods that are unsafe (Section 9.2.1 of [HTTP]) to the origin server; i.e., a cache is not allowed to generate a reply to such a request before having forwarded the request and having received a corresponding response." Unsafe methods must always be forwarded to the origin server. """ unsafe_methods = ["POST", "PUT", "DELETE", "PATCH"] for method in unsafe_methods: # Arrange request = create_request(method=method) cached_pair = create_pair( request=create_request(method=method), response=create_response(), ) # Act next_state = idle_client.next(request, [cached_pair]) # Assert assert isinstance(next_state, CacheMiss), f"{method} should result in CacheMiss" assert next_state.request == request def test_url_mismatch_causes_cache_miss(self, idle_client: IdleClient) -> None: """ Test: Different URLs result in CacheMiss. RFC 9111 Section 4, paragraph 2.1: "the presented target URI (Section 7.1 of [HTTP]) and that of the stored response match" The cache key is primarily based on the URI. Different URIs cannot share cached responses. """ # Arrange request = create_request(url="https://example.com/resource-a") cached_pair = create_pair( request=create_request(url="https://example.com/resource-b"), response=create_response(), ) # Act next_state = idle_client.next(request, [cached_pair]) # Assert assert isinstance(next_state, CacheMiss) def test_method_mismatch_causes_cache_miss(self, idle_client: IdleClient) -> None: """ Test: Different request methods result in CacheMiss. RFC 9111 Section 4, paragraph 2.2: "the request method associated with the stored response allows it to be used for the presented request" Generally, only responses to the same method can be reused. """ # Arrange get_request = create_request(method="GET") head_request = create_request(method="HEAD") cached_pair = create_pair( request=get_request, response=create_response(), ) # Act next_state = idle_client.next(head_request, [cached_pair]) # Assert assert isinstance(next_state, CacheMiss) def test_vary_header_mismatch_causes_cache_miss(self, idle_client: IdleClient) -> None: """ Test: Mismatched Vary headers result in CacheMiss. RFC 9111 Section 4.1: Calculating Cache Keys "When a cache receives a request that can be satisfied by a stored response and that stored response contains a Vary header field, the cache MUST NOT use that stored response without revalidation unless all the presented request header fields nominated by that Vary field value match those fields in the original request." The Vary header specifies which request headers must match for the cached response to be reused. """ # Arrange # Original request had Accept-Encoding: gzip original_request = create_request(headers={"accept-encoding": "gzip"}) response = create_response(headers={"vary": "Accept-Encoding"}) cached_pair = create_pair(request=original_request, response=response) # New request has different Accept-Encoding new_request = create_request(headers={"accept-encoding": "br"}) # Act next_state = idle_client.next(new_request, [cached_pair]) # Assert assert isinstance(next_state, CacheMiss) def test_vary_star_always_causes_cache_miss(self, idle_client: IdleClient) -> None: """ Test: Vary: * always results in CacheMiss. RFC 9111 Section 4.1, paragraph 5: "A stored response with a Vary header field value containing a member '*' always fails to match." Vary: * indicates that the response varies on factors beyond the request headers (e.g., user agent state), so it can never be matched. """ # Arrange original_request = create_request() response = create_response(headers={"vary": "*"}) cached_pair = create_pair(request=original_request, response=response) # Even identical request should not match identical_request = create_request() # Act next_state = idle_client.next(identical_request, [cached_pair]) # Assert assert isinstance(next_state, CacheMiss) def test_no_cache_directive_requires_revalidation(self, idle_client: IdleClient) -> None: """ Test: Cached response with no-cache directive requires revalidation. RFC 9111 Section 5.2.2.4: no-cache Response Directive "The no-cache response directive... indicates that the response MUST NOT be used to satisfy any other request without forwarding it for validation and receiving a successful response." Responses with no-cache cannot be used without validation. """ # Arrange request = create_request() response = create_response( age_seconds=0, max_age_seconds=3600, headers={"cache-control": "no-cache, max-age=3600"}, ) cached_pair = create_pair(request=request, response=response) # Act next_state = idle_client.next(request, [cached_pair]) # Assert # With no-cache, the response is filtered out, leading to CacheMiss assert isinstance(next_state, CacheMiss) def test_all_responses_stale_and_not_allowed_causes_revalidation(self, idle_client: IdleClient) -> None: """ Test: When all cached responses are stale and cannot be served stale. RFC 9111 Section 4.2.4: Serving Stale Responses "A cache MUST NOT generate a stale response if it is prohibited by an explicit in-protocol directive (e.g., by a no-cache response directive, a must-revalidate response directive, or an applicable s-maxage or proxy-revalidate response directive)." Stale responses that cannot be served stale require revalidation. """ # Arrange request = create_request() # Response is stale (age 7200s > max-age 3600s) stale_response = create_response(age_seconds=7200, max_age_seconds=3600) cached_pair = create_pair(request=request, response=stale_response) # Act next_state = idle_client.next(request, [cached_pair]) # Assert assert isinstance(next_state, NeedRevalidation) # ============================================================================= # Test Suite 2: Transition to FromCache State # ============================================================================= # These tests verify that FromCache is returned when a suitable cached response # is available and can be served immediately. # ============================================================================= class TestTransitionToFromCache: """ Tests for transitions to the FromCache state. FromCache occurs when: - A fresh cached response is available - A stale cached response is explicitly allowed to be served - All RFC 9111 conditions for cache reuse are met """ def test_fresh_response_served_from_cache(self, idle_client: IdleClient) -> None: """ Test: Fresh cached response is served from cache. RFC 9111 Section 4.2: Freshness "A 'fresh' response is one whose age has not yet exceeded its freshness lifetime." RFC 9111 Section 4, paragraph 4: "When a stored response is used to satisfy a request without validation, a cache MUST generate an Age header field (Section 5.1), replacing any present in the response with a value equal to the stored response's current_age." """ # Arrange request = create_request() # Response is fresh: age 1800s < max-age 3600s fresh_response = create_response(age_seconds=1800, max_age_seconds=3600) cached_pair = create_pair(request=request, response=fresh_response) # Act next_state = idle_client.next(request, [cached_pair]) # Assert assert isinstance(next_state, FromCache) assert next_state.entry.request == request # Verify Age header is present and updated assert "age" in next_state.entry.response.headers age_value = int(next_state.entry.response.headers["age"]) assert age_value >= 1800 # Should be at least the initial age # Verify metadata flag assert next_state.entry.response.metadata.get("hishel_from_cache") is True def test_stale_but_allowed_response_served_from_cache(self, idle_client_allow_stale: IdleClient) -> None: """ Test: Stale response is served when explicitly allowed. RFC 9111 Section 4.2.4: Serving Stale Responses "A cache MUST NOT generate a stale response unless it is disconnected or doing so is explicitly permitted by the client or origin server (e.g., by the max-stale request directive)." When allow_stale option is enabled, stale responses can be served. """ # Arrange request = create_request() # Response is stale: age 7200s > max-age 3600s stale_response = create_response(age_seconds=7200, max_age_seconds=3600) cached_pair = create_pair(request=request, response=stale_response) # Act next_state = idle_client_allow_stale.next(request, [cached_pair]) # Assert assert isinstance(next_state, FromCache) assert next_state.entry.request == request # Verify Age header reflects staleness age_value = int(next_state.entry.response.headers["age"]) assert age_value >= 7200 def test_most_recent_response_selected_when_multiple_available(self, idle_client: IdleClient) -> None: """ Test: Most recent response is selected when multiple are cached. RFC 9111 Section 4, paragraph 8: "When more than one suitable response is stored, a cache MUST use the most recent one (as determined by the Date header field)." This ensures clients receive the most up-to-date cached information. """ # Arrange request = create_request() # Create three responses with different dates older_response = create_response( age_seconds=3000, max_age_seconds=3600, ) middle_response = create_response( age_seconds=2000, max_age_seconds=3600, ) newest_response = create_response( age_seconds=1000, max_age_seconds=3600, ) cached_pairs = [ create_pair(request=request, response=older_response), create_pair(request=request, response=middle_response), create_pair(request=request, response=newest_response), ] # Act next_state = idle_client.next(request, cached_pairs) # Assert assert isinstance(next_state, FromCache) # Verify the newest response was selected selected_age = int(next_state.entry.response.headers["age"]) # The newest response has age ~1000s, others have 2000s and 3000s assert selected_age >= 1000 assert selected_age < 2000 # Should be closer to 1000 than 2000 def test_age_header_updated_correctly(self, idle_client: IdleClient) -> None: """ Test: Age header is calculated and updated correctly. RFC 9111 Section 5.1: Age "The Age header field is used to convey an estimated age of the response message when obtained from a cache. The Age field value is the cache's estimate of the number of seconds since the origin server generated or validated the response." RFC 9111 Section 4.2.3: Calculating Age Provides the formula for calculating the current age of a response. """ # Arrange request = create_request() # Create response with known age initial_age = 1800 # 30 minutes response = create_response(age_seconds=initial_age, max_age_seconds=3600) cached_pair = create_pair(request=request, response=response) # Act next_state = idle_client.next(request, [cached_pair]) # Assert assert isinstance(next_state, FromCache) # Age should be at least the initial age age_value = int(next_state.entry.response.headers["age"]) assert age_value >= initial_age assert age_value < initial_age + 5 # Shouldn't increase by more than a few seconds def test_matching_vary_headers_allows_cache_hit(self, idle_client: IdleClient) -> None: """ Test: Matching Vary headers allow cache hit. RFC 9111 Section 4.1: Calculating Cache Keys "The header fields from two requests are defined to match if and only if those in the first request can be transformed to those in the second request by applying any of the following: - adding or removing whitespace..." When Vary headers match, the cached response can be reused. """ # Arrange original_request = create_request(headers={"accept-encoding": "gzip"}) response = create_response(headers={"vary": "Accept-Encoding"}) cached_pair = create_pair(request=original_request, response=response) # New request with same Accept-Encoding new_request = create_request(headers={"accept-encoding": "gzip"}) # Act next_state = idle_client.next(new_request, [cached_pair]) # Assert assert isinstance(next_state, FromCache) def test_cache_hit_with_all_conditions_met(self, idle_client: IdleClient) -> None: """ Test: Cache hit when all RFC 9111 conditions are met. This is an integration test verifying that when all five conditions from RFC 9111 Section 4 are satisfied, the response is served from cache. Conditions verified: 1. URI matches 2. Method matches 3. Vary headers match (if present) 4. No no-cache directive 5. Response is fresh """ # Arrange request = create_request( method="GET", url="https://example.com/api/data", headers={"accept": "application/json", "accept-encoding": "gzip"}, ) response = create_response( age_seconds=1000, max_age_seconds=3600, headers={ "vary": "Accept, Accept-Encoding", "content-type": "application/json", }, ) cached_pair = create_pair(request=request, response=response) # Act next_state = idle_client.next(request, [cached_pair]) # Assert assert isinstance(next_state, FromCache) assert next_state.entry.response.metadata.get("hishel_from_cache") is True # ============================================================================= # Test Suite 3: Transition to NeedRevalidation State # ============================================================================= # These tests verify revalidation behavior for stale cached responses. # ============================================================================= class TestTransitionToNeedRevalidation: """ Tests for transitions to the NeedRevalidation state. NeedRevalidation occurs when: - A stale cached response exists - The response cannot be served stale - Validation is required before serving """ def test_stale_response_requires_revalidation(self, idle_client: IdleClient) -> None: """ Test: Stale response that cannot be served stale requires revalidation. RFC 9111 Section 4.3: Validation "When a cache has one or more stored responses for a requested URI, but cannot serve any of them (e.g., because they are not fresh, or one cannot be chosen), it can use the conditional request mechanism in the forwarded request to give the next inbound server an opportunity to choose a valid stored response to use." """ # Arrange request = create_request() # Response is stale: age 7200s > max-age 3600s stale_response = create_response(age_seconds=7200, max_age_seconds=3600) cached_pair = create_pair(request=request, response=stale_response) # Act next_state = idle_client.next(request, [cached_pair]) # Assert assert isinstance(next_state, NeedRevalidation) assert next_state.original_request == request assert len(next_state.revalidating_entries) == 1 assert next_state.revalidating_entries[0] == cached_pair def test_conditional_request_created_for_revalidation(self, idle_client: IdleClient) -> None: """ Test: Conditional request is created with validators from cached response. RFC 9111 Section 4.3.1: Sending a Validation Request "When generating a conditional request for validation, a cache... updates that request with one or more precondition header fields. These contain validator metadata sourced from a stored response(s)." Validators include: - ETag -> If-None-Match - Last-Modified -> If-Modified-Since """ # Arrange request = create_request() stale_response = create_response( age_seconds=7200, max_age_seconds=3600, headers={ "etag": '"abc123"', "last-modified": "Mon, 01 Jan 2024 00:00:00 GMT", }, ) cached_pair = create_pair(request=request, response=stale_response) # Act next_state = idle_client.next(request, [cached_pair]) # Assert assert isinstance(next_state, NeedRevalidation) # Verify conditional request headers are added conditional_request = next_state.request # Should have If-None-Match (from ETag) or If-Modified-Since (from Last-Modified) assert "if-none-match" in conditional_request.headers or "if-modified-since" in conditional_request.headers def test_must_revalidate_directive_forces_revalidation(self) -> None: """ Test: must-revalidate directive forces revalidation of stale responses. RFC 9111 Section 5.2.2.2: must-revalidate Response Directive "The must-revalidate response directive indicates that once the response has become stale, a cache MUST NOT reuse that response to satisfy another request until it has been successfully validated by the origin." Even with allow_stale enabled, must-revalidate takes precedence. """ # Arrange idle_client_allow_stale = IdleClient( options=CacheOptions(shared=True, supported_methods=["GET", "HEAD"], allow_stale=True) ) request = create_request() # Stale response with must-revalidate stale_response = create_response( age_seconds=7200, max_age_seconds=3600, headers={"cache-control": "max-age=3600, must-revalidate"}, ) cached_pair = create_pair(request=request, response=stale_response) # Act next_state = idle_client_allow_stale.next(request, [cached_pair]) # Assert # must-revalidate should override allow_stale assert isinstance(next_state, NeedRevalidation) def test_multiple_stale_responses_all_included_in_revalidation(self, idle_client: IdleClient) -> None: """ Test: Multiple stale responses are all passed to revalidation state. This allows the validation logic to potentially use any of them if the server responds with 304 Not Modified. """ # Arrange request = create_request() stale_response_1 = create_response(age_seconds=7200, max_age_seconds=3600) stale_response_2 = create_response(age_seconds=8000, max_age_seconds=3600) cached_pairs = [ create_pair(request=request, response=stale_response_1), create_pair(request=request, response=stale_response_2), ] # Act next_state = idle_client.next(request, cached_pairs) # Assert assert isinstance(next_state, NeedRevalidation) assert len(next_state.revalidating_entries) == 2 def test_request_no_cache_forces_revalidation_of_fresh_response(self, idle_client: IdleClient) -> None: """ Test: Request no-cache directive forces revalidation even for fresh responses. RFC 9111 Section 5.2.1.4: no-cache Request Directive "The no-cache request directive indicates that a cache MUST NOT use a stored response to satisfy the request without successful validation on the origin server." Even if a cached response is fresh, the request no-cache directive forces the cache to revalidate it with the origin server. """ # Arrange # Request with no-cache directive request = create_request(headers={"cache-control": "no-cache"}) # Fresh cached response: age 1800s < max-age 3600s fresh_response = create_response( age_seconds=1800, max_age_seconds=3600, headers={"etag": '"abc123"'}, ) cached_pair = create_pair(request=request, response=fresh_response) # Act next_state = idle_client.next(request, [cached_pair]) # Assert # Despite being fresh, response must be revalidated due to request no-cache assert isinstance(next_state, NeedRevalidation) assert next_state.original_request == request assert len(next_state.revalidating_entries) == 1 assert next_state.revalidating_entries[0] == cached_pair # Verify conditional request is created with validators assert "if-none-match" in next_state.request.headers assert next_state.request.headers["if-none-match"] == '"abc123"' # ============================================================================= # Test Suite 4: Edge Cases and RFC 9111 Compliance # ============================================================================= # These tests cover special scenarios and ensure strict RFC compliance. # ============================================================================= class TestEdgeCasesAndCompliance: """ Tests for edge cases and specific RFC 9111 compliance scenarios. """ def test_response_without_explicit_freshness_info(self, idle_client: IdleClient) -> None: """ Test: Response without explicit freshness information. RFC 9111 Section 4.2.2: Calculating Heuristic Freshness "Since origin servers do not always provide explicit expiration times, a cache MAY assign a heuristic expiration time when an explicit time is not specified." Without max-age or Expires, freshness is determined heuristically. """ # Arrange request = create_request() # Response without max-age or Expires, but with Last-Modified for heuristic freshness response_headers = { "date": datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S GMT"), "last-modified": (datetime.utcnow() - timedelta(days=7)).strftime("%a, %d %b %Y %H:%M:%S GMT"), } response = Response( status_code=200, headers=Headers(response_headers), metadata={}, ) cached_pair = create_pair(request=request, response=response) # Act next_state = idle_client.next(request, [cached_pair]) # Assert # With heuristic freshness (10% of age since Last-Modified, max 1 week), # a 7-day old document would have ~17 hours of freshness remaining # Since it's fresh, it should be FromCache assert isinstance(next_state, (FromCache, NeedRevalidation, CacheMiss)) def test_safe_methods_can_be_cached(self, idle_client: IdleClient) -> None: """ Test: Safe methods (GET, HEAD) can be cached. RFC 9111 Section 2: Overview of Cache Operation "Most commonly, caches store the successful result of a retrieval request: i.e., a 200 (OK) response to a GET request." Safe methods are cacheable by default. """ safe_methods = ["GET", "HEAD"] for method in safe_methods: # Arrange request = create_request(method=method) response = create_response() cached_pair = create_pair(request=create_request(method=method), response=response) # Act next_state = idle_client.next(request, [cached_pair]) # Assert assert isinstance(next_state, FromCache), f"{method} should be cacheable" def test_options_propagated_to_next_state(self, idle_client: IdleClient) -> None: """ Test: Cache options are propagated to the next state. This ensures configuration is maintained throughout state transitions. """ # Arrange request = create_request() # Test CacheMiss next_state_miss = idle_client.next(request, []) assert next_state_miss.options == idle_client.options # Test FromCache cached_pair = create_pair(request=request, response=create_response()) next_state_cache = idle_client.next(request, [cached_pair]) assert next_state_cache.options == idle_client.options # Test NeedRevalidation stale_pair = create_pair( request=request, response=create_response(age_seconds=7200, max_age_seconds=3600), ) next_state_revalidate = idle_client.next(request, [stale_pair]) assert next_state_revalidate.options == idle_client.options def test_metadata_flag_set_on_cache_hit(self, idle_client: IdleClient) -> None: """ Test: Metadata flag is set when response is served from cache. The 'hishel_from_cache' flag helps with observability and debugging. """ # Arrange request = create_request() cached_pair = create_pair(request=request, response=create_response()) # Act next_state = idle_client.next(request, [cached_pair]) # Assert assert isinstance(next_state, FromCache) assert next_state.entry.response.metadata.get("hishel_from_cache") is True def test_empty_vary_header_treated_as_no_vary(self, idle_client: IdleClient) -> None: """ Test: Empty or missing Vary header means no variance on request headers. Without a Vary header, the cached response can be used for any request with matching URI and method. """ # Arrange original_request = create_request(headers={"accept": "application/json"}) response = create_response() # No Vary header cached_pair = create_pair(request=original_request, response=response) # New request with different headers new_request = create_request(headers={"accept": "application/xml"}) # Act next_state = idle_client.next(new_request, [cached_pair]) # Assert assert isinstance(next_state, FromCache) def test_age_close_to_zero_for_newly_generated_response(self, idle_client: IdleClient) -> None: """ Test: Age should be close to zero for a freshly cached response. RFC 9111 Section 4.2.3: Calculating Age A response that was just generated should have minimal age. """ # Arrange request = create_request() # Response generated "now" (age_seconds=0) fresh_response = create_response(age_seconds=0, max_age_seconds=3600) cached_pair = create_pair(request=request, response=fresh_response) # Act next_state = idle_client.next(request, [cached_pair]) # Assert assert isinstance(next_state, FromCache) age_value = int(next_state.entry.response.headers["age"]) assert age_value < 10 # Should be very close to 0 def test_sorting_handles_responses_without_date_header(self, idle_client: IdleClient) -> None: """ Test: Sorting handles missing Date headers gracefully. RFC 9111 Section 4.2, paragraph 12: "When calculating freshness, to avoid common problems in date parsing..." Missing dates should not crash the sorting logic. """ # Arrange request = create_request() # Create responses: one without Date, one with Date response_no_date = Response( status_code=200, headers=Headers({"cache-control": "max-age=3600"}), metadata={}, ) response_with_date = create_response() cached_pairs = [ create_pair(request=request, response=response_no_date), create_pair(request=request, response=response_with_date), ] # Act & Assert # Should not raise an exception next_state = idle_client.next(request, cached_pairs) assert isinstance(next_state, (FromCache, NeedRevalidation, CacheMiss)) # ============================================================================= # Test Suite 5: Integration Tests # ============================================================================= # Complex scenarios combining multiple conditions. # ============================================================================= class TestIntegrationScenarios: """ Integration tests simulating real-world caching scenarios. """ def test_content_negotiation_with_vary(self, idle_client: IdleClient) -> None: """ Test: Content negotiation scenario with Vary header. Simulates a scenario where the server provides different content based on Accept headers (e.g., JSON vs XML). """ # Arrange: Original request for JSON json_request = create_request( url="https://api.example.com/data", headers={"accept": "application/json"}, ) json_response = create_response( headers={ "vary": "Accept", "content-type": "application/json", } ) cached_json = create_pair(request=json_request, response=json_response) # Act 1: Request for JSON (should hit cache) next_state_json = idle_client.next(json_request, [cached_json]) assert isinstance(next_state_json, FromCache) # Act 2: Request for XML (should miss cache) xml_request = create_request( url="https://api.example.com/data", headers={"accept": "application/xml"}, ) next_state_xml = idle_client.next(xml_request, [cached_json]) assert isinstance(next_state_xml, CacheMiss) def test_compression_negotiation_with_vary_accept_encoding(self, idle_client: IdleClient) -> None: """ Test: Compression negotiation with Vary: Accept-Encoding. Simulates caching of compressed responses. """ # Arrange: Cached gzip response gzip_request = create_request(headers={"accept-encoding": "gzip"}) gzip_response = create_response( headers={ "vary": "Accept-Encoding", "content-encoding": "gzip", } ) cached_gzip = create_pair(request=gzip_request, response=gzip_response) # Act 1: Request with gzip (should hit) next_state_gzip = idle_client.next(gzip_request, [cached_gzip]) assert isinstance(next_state_gzip, FromCache) # Act 2: Request with brotli (should miss) br_request = create_request(headers={"accept-encoding": "br"}) next_state_br = idle_client.next(br_request, [cached_gzip]) assert isinstance(next_state_br, CacheMiss) def test_lifecycle_fresh_to_stale_transition(self, idle_client: IdleClient) -> None: """ Test: Simulates the lifecycle of a response from fresh to stale. This test demonstrates how the same cached response transitions from FromCache to NeedRevalidation as time passes. """ # Arrange: Response that is currently fresh request = create_request() # Initially fresh (age 10s < max-age 20s) response = create_response(age_seconds=10, max_age_seconds=20) cached_pair = create_pair(request=request, response=response) # Act 1: Request while fresh next_state_fresh = idle_client.next(request, [cached_pair]) assert isinstance(next_state_fresh, FromCache) # Arrange: Simulate passage of time (now stale: age 30s > max-age 20s) stale_response = create_response(age_seconds=30, max_age_seconds=20) stale_pair = create_pair(request=request, response=stale_response) # Act 2: Request when stale next_state_stale = idle_client.next(request, [stale_pair]) assert isinstance(next_state_stale, NeedRevalidation) def test_multiple_vary_headers(self, idle_client: IdleClient) -> None: """ Test: Response with multiple Vary headers. RFC 9111 Section 4.1: The Vary header can list multiple request headers that must all match for a cached response to be reused. """ # Arrange original_request = create_request( headers={ "accept": "application/json", "accept-encoding": "gzip", "accept-language": "en-US", } ) response = create_response(headers={"vary": "Accept, Accept-Encoding, Accept-Language"}) cached_pair = create_pair(request=original_request, response=response) # Act 1: Request with all matching headers (should hit) matching_request = create_request( headers={ "accept": "application/json", "accept-encoding": "gzip", "accept-language": "en-US", } ) next_state_match = idle_client.next(matching_request, [cached_pair]) assert isinstance(next_state_match, FromCache) # Act 2: Request with one different header (should miss) non_matching_request = create_request( headers={ "accept": "application/json", "accept-encoding": "gzip", "accept-language": "fr-FR", # Different language } ) next_state_no_match = idle_client.next(non_matching_request, [cached_pair]) assert isinstance(next_state_no_match, CacheMiss) karpetrosyan-hishel-fd309b1/tests/_core/spec/test_need_revalidation.py000066400000000000000000000644461514113157400264100ustar00rootroot00000000000000""" Comprehensive tests for the NeedRevalidation state. Tests verify compliance with RFC 9111 Section 4.3: Validation https://www.rfc-editor.org/rfc/rfc9111.html#section-4.3 Test Categories: --------------- 1. 304 Not Modified responses (freshening) 2. 2xx Success responses (new content) 3. 5xx Server Error responses 4. Validator matching strategies 5. Edge cases and error handling """ import uuid from typing import Dict, Optional import pytest from hishel import Entry, EntryMeta, Request, Response from hishel._core._headers import Headers from hishel._core._spec import ( CacheMiss, CacheOptions, CouldNotBeStored, InvalidateEntries, NeedRevalidation, NeedToBeUpdated, StoreAndUse, ) # ============================================================================= # Test Fixtures and Helpers # ============================================================================= @pytest.fixture def default_options() -> CacheOptions: """Default cache options.""" return CacheOptions(shared=True, supported_methods=["GET", "HEAD"], allow_stale=False) def create_request( method: str = "GET", url: str = "https://example.com/resource", headers: Optional[Dict[str, str]] = None, ) -> Request: """Helper to create a request.""" return Request( method=method, url=url, headers=Headers(headers or {}), metadata={}, ) def create_response( status_code: int = 200, headers: Optional[Dict[str, str]] = None, ) -> Response: """Helper to create a response.""" return Response( status_code=status_code, headers=Headers(headers or {}), metadata={}, ) def create_pair( request: Optional[Request] = None, response: Optional[Response] = None, pair_id: Optional[uuid.UUID] = None, ) -> Entry: """Helper to create a request-response pair.""" return Entry( id=pair_id or uuid.uuid4(), request=request or create_request(), response=response or create_response(), meta=EntryMeta(), cache_key=b"test-cache-key", ) # ============================================================================= # Test Suite 1: 304 Not Modified Responses (Freshening) # ============================================================================= class TestNotModifiedResponses: """ Tests for 304 Not Modified responses that freshen cached responses. RFC 9111 Section 4.3.4: Freshening Stored Responses Upon Validation """ def test_304_with_matching_strong_etag_freshens_response(self, default_options: CacheOptions) -> None: """ Test: 304 response with matching strong ETag freshens cached response. RFC 9111 Section 4.3.4: "If the 304 response contains a strong entity tag: the stored responses with the same strong entity tag." Strong ETags indicate exact byte-for-byte match. """ # Arrange original_request = create_request() conditional_request = create_request(headers={"if-none-match": '"abc123"'}) # Cached response with matching ETag cached_response = create_response( headers={ "etag": '"abc123"', "cache-control": "max-age=3600", "date": "Mon, 01 Jan 2024 00:00:00 GMT", } ) cached_pair = create_pair(request=original_request, response=cached_response) need_revalidation = NeedRevalidation( request=conditional_request, original_request=original_request, revalidating_entries=[cached_pair], options=default_options, ) # 304 response with same ETag and fresh metadata revalidation_response = create_response( status_code=304, headers={ "etag": '"abc123"', "cache-control": "max-age=7200", # New max-age "date": "Mon, 01 Jan 2024 12:00:00 GMT", # New date }, ) # Act next_state = need_revalidation.next(revalidation_response) # Assert assert isinstance(next_state, NeedToBeUpdated) assert len(next_state.updating_entries) == 1 # Response should be updated with new cache-control updated_response = next_state.updating_entries[0].response assert "cache-control" in updated_response.headers def test_304_with_weak_etag_uses_last_modified_fallback(self, default_options: CacheOptions) -> None: """ Test: 304 with weak ETag falls back to Last-Modified matching. RFC 9110 Section 8.8.3: ETag Weak ETags (W/"...") indicate semantic equivalence but not byte-identity. They're not reliable for caching decisions, so we fall back to Last-Modified. """ # Arrange original_request = create_request() conditional_request = create_request() # Cached response with Last-Modified cached_response = create_response( headers={ "last-modified": "Mon, 01 Jan 2024 00:00:00 GMT", "cache-control": "max-age=3600", } ) cached_pair = create_pair(request=original_request, response=cached_response) need_revalidation = NeedRevalidation( request=conditional_request, original_request=original_request, revalidating_entries=[cached_pair], options=default_options, ) # 304 response with weak ETag (ignored) and matching Last-Modified revalidation_response = create_response( status_code=304, headers={ "etag": 'W/"abc123"', # Weak ETag - ignored "last-modified": "Mon, 01 Jan 2024 00:00:00 GMT", "cache-control": "max-age=7200", }, ) # Act next_state = need_revalidation.next(revalidation_response) # Assert assert isinstance(next_state, NeedToBeUpdated) assert len(next_state.updating_entries) == 1 def test_304_with_matching_last_modified_freshens_response(self, default_options: CacheOptions) -> None: """ Test: 304 response with matching Last-Modified freshens cached response. RFC 9111 Section 4.3.4: "If the 304 response contains a Last-Modified value: the stored responses with the same Last-Modified value." Last-Modified is used when ETag is not available. """ # Arrange original_request = create_request() conditional_request = create_request(headers={"if-modified-since": "Mon, 01 Jan 2024 00:00:00 GMT"}) # Cached response with Last-Modified cached_response = create_response( headers={ "last-modified": "Mon, 01 Jan 2024 00:00:00 GMT", "cache-control": "max-age=3600", } ) cached_pair = create_pair(request=original_request, response=cached_response) need_revalidation = NeedRevalidation( request=conditional_request, original_request=original_request, revalidating_entries=[cached_pair], options=default_options, ) # 304 response with same Last-Modified revalidation_response = create_response( status_code=304, headers={ "last-modified": "Mon, 01 Jan 2024 00:00:00 GMT", "cache-control": "max-age=7200", }, ) # Act next_state = need_revalidation.next(revalidation_response) # Assert assert isinstance(next_state, NeedToBeUpdated) assert len(next_state.updating_entries) == 1 def test_304_with_single_cached_response_and_no_validators(self, default_options: CacheOptions) -> None: """ Test: 304 with no validators but single cached response freshens it. RFC 9111 Section 4.3.4: "If there is only a single stored response: that response." When only one response is cached and server says "not modified", we can safely assume that response is valid. """ # Arrange original_request = create_request() conditional_request = create_request() # Cached response without validators cached_response = create_response(headers={"cache-control": "max-age=3600"}) cached_pair = create_pair(request=original_request, response=cached_response) need_revalidation = NeedRevalidation( request=conditional_request, original_request=original_request, revalidating_entries=[cached_pair], # Only one pair options=default_options, ) # 304 response without validators revalidation_response = create_response(status_code=304, headers={"cache-control": "max-age=7200"}) # Act next_state = need_revalidation.next(revalidation_response) # Assert assert isinstance(next_state, NeedToBeUpdated) assert len(next_state.updating_entries) == 1 def test_304_with_multiple_responses_and_no_validators_invalidates_all(self, default_options: CacheOptions) -> None: """ Test: 304 with no validators and multiple responses invalidates all. RFC 9111 Section 4.3.4: When multiple responses are cached but the 304 has no validators, we cannot determine which response is valid. Conservative approach is to invalidate all and treat as cache miss. """ # Arrange original_request = create_request() conditional_request = create_request() # Multiple cached responses without validators cached_pair_1 = create_pair( request=original_request, response=create_response(headers={"cache-control": "max-age=3600"}), ) cached_pair_2 = create_pair( request=original_request, response=create_response(headers={"cache-control": "max-age=1800"}), ) need_revalidation = NeedRevalidation( request=conditional_request, original_request=original_request, revalidating_entries=[cached_pair_1, cached_pair_2], options=default_options, ) # 304 response without validators revalidation_response = create_response(status_code=304, headers={"cache-control": "max-age=7200"}) # Act next_state = need_revalidation.next(revalidation_response) # Assert # Should invalidate all and result in cache miss assert isinstance(next_state, InvalidateEntries) assert len(next_state.entry_ids) == 2 assert isinstance(next_state.next_state, CacheMiss) def test_304_with_non_matching_etag_invalidates_response(self, default_options: CacheOptions) -> None: """ Test: 304 with non-matching ETag invalidates cached response. If the 304's ETag doesn't match the cached response's ETag, the cached response is outdated and should be invalidated. """ # Arrange original_request = create_request() conditional_request = create_request() # Cached response with old ETag cached_response = create_response(headers={"etag": '"old123"', "cache-control": "max-age=3600"}) cached_pair = create_pair(request=original_request, response=cached_response) need_revalidation = NeedRevalidation( request=conditional_request, original_request=original_request, revalidating_entries=[cached_pair], options=default_options, ) # 304 response with different ETag revalidation_response = create_response( status_code=304, headers={"etag": '"new456"', "cache-control": "max-age=7200"}, ) # Act next_state = need_revalidation.next(revalidation_response) # Assert assert isinstance(next_state, InvalidateEntries) assert len(next_state.entry_ids) == 1 assert isinstance(next_state.next_state, CacheMiss) def test_304_with_multiple_responses_freshens_matching_invalidates_others( self, default_options: CacheOptions ) -> None: """ Test: 304 with multiple cached responses freshens matching, invalidates others. When multiple responses are cached with different ETags, only the one matching the 304's ETag should be freshened. Others should be invalidated. """ # Arrange original_request = create_request() conditional_request = create_request() # Two cached responses with different ETags cached_pair_1 = create_pair( request=original_request, response=create_response(headers={"etag": '"match123"', "cache-control": "max-age=3600"}), ) cached_pair_2 = create_pair( request=original_request, response=create_response( headers={ "etag": '"nomatch456"', "cache-control": "max-age=3600", } ), ) need_revalidation = NeedRevalidation( request=conditional_request, original_request=original_request, revalidating_entries=[cached_pair_1, cached_pair_2], options=default_options, ) # 304 response matches first cached response's ETag revalidation_response = create_response( status_code=304, headers={"etag": '"match123"', "cache-control": "max-age=7200"}, ) # Act next_state = need_revalidation.next(revalidation_response) # Assert assert isinstance(next_state, InvalidateEntries) assert len(next_state.entry_ids) == 1 # One invalidated assert isinstance(next_state.next_state, NeedToBeUpdated) assert len(next_state.next_state.updating_entries) == 1 # One freshened # ============================================================================= # Test Suite 2: 2xx Success Responses (New Content) # ============================================================================= class TestSuccessResponses: """ Tests for 2xx success responses indicating the resource has changed. RFC 9111 Section 4.3.3, paragraph 2 """ @pytest.mark.parametrize("status_code", [200, 201, 202, 203, 204, 206]) def test_2xx_response_invalidates_old_pairs_and_stores_new( self, default_options: CacheOptions, status_code: int ) -> None: """ Test: 2xx response invalidates old cached pairs and stores new response. RFC 9111 Section 4.3.3: "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." 2xx means the resource has changed - use the new response. """ # Arrange original_request = create_request() conditional_request = create_request() # Old cached pairs (will be invalidated) cached_pair_1 = create_pair( response=create_response(headers={"etag": '"old1"', "cache-control": "max-age=3600"}) ) cached_pair_2 = create_pair( response=create_response(headers={"etag": '"old2"', "cache-control": "max-age=3600"}) ) need_revalidation = NeedRevalidation( request=conditional_request, original_request=original_request, revalidating_entries=[cached_pair_1, cached_pair_2], options=default_options, ) # New response with different content new_response = create_response( status_code=status_code, headers={"etag": '"new123"', "cache-control": "max-age=7200"}, ) # Act next_state = need_revalidation.next(new_response) # Assert assert isinstance(next_state, InvalidateEntries) # First pair is invalidated assert len(next_state.entry_ids) == 1 assert next_state.entry_ids[0] == cached_pair_1.id # Next state should be StoreAndUse or CouldNotBeStored inner_state = next_state.next_state assert isinstance(inner_state, (StoreAndUse, CouldNotBeStored)) def test_200_response_marks_as_after_revalidation(self, default_options: CacheOptions) -> None: """ Test: 200 response during revalidation is marked with after_revalidation flag. This helps track that the new response was obtained during a revalidation attempt rather than an initial cache miss. """ # Arrange original_request = create_request() conditional_request = create_request() cached_pair = create_pair() need_revalidation = NeedRevalidation( request=conditional_request, original_request=original_request, revalidating_entries=[cached_pair], options=default_options, ) # New response new_response = create_response(status_code=200, headers={"cache-control": "max-age=3600"}) # Act next_state = need_revalidation.next(new_response) # Assert assert isinstance(next_state, InvalidateEntries) inner_state = next_state.next_state assert isinstance(inner_state, StoreAndUse) # Response should be marked as revalidated assert new_response.metadata.get("hishel_revalidated") is True def test_2xx_reuses_last_pair_id_for_new_response(self, default_options: CacheOptions) -> None: """ Test: 2xx response reuses the last pair's ID for storing the new response. The implementation invalidates all pairs except the last, then reuses the last pair's ID for the new response. """ # Arrange original_request = create_request() conditional_request = create_request() pair_1_id = uuid.uuid4() pair_2_id = uuid.uuid4() cached_pair_1 = create_pair(pair_id=pair_1_id) cached_pair_2 = create_pair(pair_id=pair_2_id) need_revalidation = NeedRevalidation( request=conditional_request, original_request=original_request, revalidating_entries=[cached_pair_1, cached_pair_2], options=default_options, ) # New response new_response = create_response(status_code=200, headers={"cache-control": "max-age=3600"}) # Act next_state = need_revalidation.next(new_response) # Assert assert isinstance(next_state, InvalidateEntries) # Only first pair is invalidated assert pair_1_id in next_state.entry_ids assert pair_2_id not in next_state.entry_ids # Second pair's ID should be reused inner_state = next_state.next_state assert isinstance(inner_state, StoreAndUse) # ============================================================================= # Test Suite 3: 5xx Server Error Responses # ============================================================================= class TestServerErrorResponses: """ Tests for 5xx server error responses during revalidation. RFC 9111 Section 4.3.3, paragraph 3 """ @pytest.mark.parametrize("status_code", [500, 502, 503, 504]) def test_5xx_response_invalidates_and_forwards_error(self, default_options: CacheOptions, status_code: int) -> None: """ Test: 5xx response invalidates cached pairs and forwards the error. RFC 9111 Section 4.3.3: "However, if a cache receives a 5xx (Server Error) response while attempting to validate a response, it can either forward this response to the requesting client or act as if the server failed to respond." This implementation chooses to forward the error response. """ # Arrange original_request = create_request() conditional_request = create_request() cached_pair = create_pair() need_revalidation = NeedRevalidation( request=conditional_request, original_request=original_request, revalidating_entries=[cached_pair], options=default_options, ) # Server error response error_response = create_response( status_code=status_code, headers={"cache-control": "no-store"}, # Errors typically not cacheable ) # Act next_state = need_revalidation.next(error_response) # Assert assert isinstance(next_state, InvalidateEntries) inner_state = next_state.next_state # Error might not be stored (depends on cacheability) assert isinstance(inner_state, (StoreAndUse, CouldNotBeStored)) def test_503_service_unavailable_handled_like_2xx(self, default_options: CacheOptions) -> None: """ Test: 503 Service Unavailable is handled the same as 2xx responses. The current implementation treats 5xx the same as 2xx: invalidate old pairs and attempt to cache the error response. Alternative implementations might serve stale content on 5xx errors. """ # Arrange original_request = create_request() conditional_request = create_request() pair_1 = create_pair() pair_2 = create_pair() need_revalidation = NeedRevalidation( request=conditional_request, original_request=original_request, revalidating_entries=[pair_1, pair_2], options=default_options, ) # 503 error error_response = create_response(status_code=503, headers={"retry-after": "60"}) # Act next_state = need_revalidation.next(error_response) # Assert # Same behavior as 2xx assert isinstance(next_state, InvalidateEntries) assert len(next_state.entry_ids) == 1 # First pair invalidated # ============================================================================= # Test Suite 4: Edge Cases and Error Handling # ============================================================================= class TestEdgeCases: """ Tests for edge cases and error conditions. """ def test_redirect_status_code_is_handled(self, default_options: CacheOptions) -> None: """ Test: 3xx redirect response during revalidation is handled correctly. """ # Arrange original_request = create_request() conditional_request = create_request() cached_pair = create_pair() need_revalidation = NeedRevalidation( request=conditional_request, original_request=original_request, revalidating_entries=[cached_pair], options=default_options, ) # Unexpected 3xx redirect redirect_response = create_response(status_code=301, headers={"location": "https://example.com/moved"}) # Act & Assert next_state = need_revalidation.next(redirect_response) assert isinstance(next_state, (StoreAndUse, CouldNotBeStored)) def test_options_propagated_to_next_states(self, default_options: CacheOptions) -> None: """ Test: Cache options are propagated to all next states. """ original_request = create_request() conditional_request = create_request() cached_pair = create_pair() need_revalidation = NeedRevalidation( request=conditional_request, original_request=original_request, revalidating_entries=[cached_pair], options=default_options, ) # Test with 304 response_304 = create_response(status_code=304, headers={"cache-control": "max-age=3600"}) next_state_304 = need_revalidation.next(response_304) assert next_state_304.options == default_options # Test with 200 response_200 = create_response(status_code=200, headers={"cache-control": "max-age=3600"}) # Create new instance for second test need_revalidation_2 = NeedRevalidation( request=conditional_request, original_request=original_request, revalidating_entries=[cached_pair], options=default_options, ) next_state_200 = need_revalidation_2.next(response_200) assert next_state_200.options == default_options def test_original_request_preserved_in_next_states(self, default_options: CacheOptions) -> None: """ Test: Original request is preserved through state transitions. The original request (without conditional headers) is important for creating new cache entries if needed. """ # Arrange original_request = create_request(url="https://example.com/test") conditional_request = create_request( url="https://example.com/test", headers={"if-none-match": '"abc123"'}, ) cached_pair = create_pair() need_revalidation = NeedRevalidation( request=conditional_request, original_request=original_request, revalidating_entries=[cached_pair], options=default_options, ) # Test with 304 response_304 = create_response(status_code=304) next_state = need_revalidation.next(response_304) if isinstance(next_state, NeedToBeUpdated): assert next_state.original_request == original_request elif isinstance(next_state, InvalidateEntries): if isinstance(next_state.next_state, NeedToBeUpdated): assert next_state.next_state.original_request == original_request def test_empty_revalidating_pairs_handled_gracefully(self, default_options: CacheOptions) -> None: """ Test: Empty revalidating_entries list is handled without errors. This shouldn't happen in normal operation, but the code should handle it gracefully if it does. """ # Arrange original_request = create_request() conditional_request = create_request() need_revalidation = NeedRevalidation( request=conditional_request, original_request=original_request, revalidating_entries=[], # Empty list options=default_options, ) # 304 response with no pairs to freshen response_304 = create_response(status_code=304) # Act next_state = need_revalidation.next(response_304) # Assert # Should result in cache miss since no responses to freshen assert isinstance(next_state, CacheMiss) karpetrosyan-hishel-fd309b1/tests/_core/test_headers.py000066400000000000000000000517011514113157400234030ustar00rootroot00000000000000""" Comprehensive pytest tests for HTTP Cache-Control parser. Run with: pytest test_cache_control.py -v """ from hishel._core._headers import CacheControl, parse_cache_control class TestBasicParsing: """Test basic parsing functionality.""" def test_empty_string(self): """Empty string should return default CacheControl.""" cc = parse_cache_control("") assert cc.max_age is None assert cc.no_cache is False assert cc.no_store is False def test_whitespace_only(self): """Whitespace-only string should return default CacheControl.""" cc = parse_cache_control(" ") assert cc.max_age is None assert cc.no_cache is False def test_single_directive(self): """Single directive without value.""" cc = parse_cache_control("no-store") assert cc.no_store is True def test_multiple_directives(self): """Multiple directives separated by commas.""" cc = parse_cache_control("no-store, no-transform, must-revalidate") assert cc.no_store is True assert cc.no_transform is True assert cc.must_revalidate is True class TestMaxAge: """Test max-age directive [RFC9111, Section 5.2.1.1, 5.2.2.1].""" def test_max_age_valid(self): """Valid max-age value.""" cc = parse_cache_control("max-age=3600") assert cc.max_age == 3600 def test_max_age_zero(self): """max-age can be zero.""" cc = parse_cache_control("max-age=0") assert cc.max_age == 0 def test_max_age_large_value(self): """Large max-age value.""" cc = parse_cache_control("max-age=31536000") assert cc.max_age == 31536000 def test_max_age_overflow(self): """max-age should cap at max int32.""" cc = parse_cache_control("max-age=9999999999999") assert cc.max_age == 2147483647 def test_max_age_invalid(self): """Invalid max-age should result in None.""" cc = parse_cache_control("max-age=invalid") assert cc.max_age is None def test_max_age_negative(self): """Negative max-age should result in None.""" cc = parse_cache_control("max-age=-100") assert cc.max_age is None def test_max_age_with_other_directives(self): """max-age combined with other directives.""" cc = parse_cache_control("max-age=3600, public, must-revalidate") assert cc.max_age == 3600 assert cc.public is True assert cc.must_revalidate is True class TestSMaxAge: """Test s-maxage directive [RFC9111, Section 5.2.2.10].""" def test_s_maxage_valid(self): """Valid s-maxage value.""" cc = parse_cache_control("s-maxage=7200") assert cc.s_maxage == 7200 def test_s_maxage_with_max_age(self): """s-maxage and max-age can coexist.""" cc = parse_cache_control("max-age=3600, s-maxage=7200") assert cc.max_age == 3600 assert cc.s_maxage == 7200 def test_s_maxage_zero(self): """s-maxage can be zero.""" cc = parse_cache_control("s-maxage=0") assert cc.s_maxage == 0 def test_s_maxage_invalid(self): """Invalid s-maxage should result in None.""" cc = parse_cache_control("s-maxage=bad") assert cc.s_maxage is None class TestMaxStale: """Test max-stale directive [RFC9111, Section 5.2.1.2].""" def test_max_stale_with_value(self): """max-stale with specific value.""" cc = parse_cache_control("max-stale=600") assert cc.max_stale == 600 def test_max_stale_without_value(self): """max-stale without value means accept any stale.""" cc = parse_cache_control("max-stale") assert cc.max_stale == 2147483647 def test_max_stale_zero(self): """max-stale can be zero.""" cc = parse_cache_control("max-stale=0") assert cc.max_stale == 0 def test_max_stale_invalid(self): """Invalid max-stale should result in None.""" cc = parse_cache_control("max-stale=xyz") assert cc.max_stale is None class TestMinFresh: """Test min-fresh directive [RFC9111, Section 5.2.1.3].""" def test_min_fresh_valid(self): """Valid min-fresh value.""" cc = parse_cache_control("min-fresh=300") assert cc.min_fresh == 300 def test_min_fresh_zero(self): """min-fresh can be zero.""" cc = parse_cache_control("min-fresh=0") assert cc.min_fresh == 0 def test_min_fresh_invalid(self): """Invalid min-fresh should result in None.""" cc = parse_cache_control("min-fresh=bad") assert cc.min_fresh is None class TestNoCache: """Test no-cache directive [RFC9111, Section 5.2.1.4, 5.2.2.4].""" def test_no_cache_without_fields(self): """no-cache without field names.""" cc = parse_cache_control("no-cache") assert cc.no_cache is True def test_no_cache_with_single_field(self): """no-cache with single field name.""" cc = parse_cache_control('no-cache="Set-Cookie"') assert isinstance(cc.no_cache, list) assert "Set-Cookie" in cc.no_cache def test_no_cache_with_multiple_fields(self): """no-cache with multiple field names.""" cc = parse_cache_control('no-cache="Set-Cookie, Authorization"') assert isinstance(cc.no_cache, list) assert "Set-Cookie" in cc.no_cache assert "Authorization" in cc.no_cache def test_no_cache_field_canonicalization(self): """Field names should be canonicalized to Title-Case.""" cc = parse_cache_control('no-cache="set-cookie, authorization"') assert isinstance(cc.no_cache, list) assert "Set-Cookie" in cc.no_cache assert "Authorization" in cc.no_cache def test_no_cache_with_whitespace(self): """no-cache with whitespace around field names.""" cc = parse_cache_control('no-cache=" Set-Cookie , Authorization "') assert isinstance(cc.no_cache, list) assert "Set-Cookie" in cc.no_cache assert "Authorization" in cc.no_cache class TestPrivate: """Test private directive [RFC9111, Section 5.2.2.7].""" def test_private_without_fields(self): """private without field names.""" cc = parse_cache_control("private") assert cc.private is True def test_private_with_single_field(self): """private with single field name.""" cc = parse_cache_control('private="Set-Cookie"') assert isinstance(cc.private, list) assert "Set-Cookie" in cc.private def test_private_with_multiple_fields(self): """private with multiple field names.""" cc = parse_cache_control('private="Set-Cookie, X-Custom-Header"') assert isinstance(cc.private, list) assert "Set-Cookie" in cc.private assert "X-Custom-Header" in cc.private def test_private_field_canonicalization(self): """Field names should be canonicalized.""" cc = parse_cache_control('private="x-custom-header"') assert isinstance(cc.private, list) assert "X-Custom-Header" in cc.private class TestBooleanDirectives: """Test boolean directives without values.""" def test_no_store(self): """no-store directive [RFC9111, Section 5.2.1.5, 5.2.2.5].""" cc = parse_cache_control("no-store") assert cc.no_store is True def test_no_transform(self): """no-transform directive [RFC9111, Section 5.2.1.6, 5.2.2.6].""" cc = parse_cache_control("no-transform") assert cc.no_transform is True def test_only_if_cached(self): """only-if-cached directive [RFC9111, Section 5.2.1.7].""" cc = parse_cache_control("only-if-cached") assert cc.only_if_cached is True def test_must_revalidate(self): """must-revalidate directive [RFC9111, Section 5.2.2.2].""" cc = parse_cache_control("must-revalidate") assert cc.must_revalidate is True def test_must_understand(self): """must-understand directive [RFC9111, Section 5.2.2.3].""" cc = parse_cache_control("must-understand") assert cc.must_understand is True def test_public(self): """public directive [RFC9111, Section 5.2.2.9].""" cc = parse_cache_control("public") assert cc.public is True def test_proxy_revalidate(self): """proxy-revalidate directive [RFC9111, Section 5.2.2.8].""" cc = parse_cache_control("proxy-revalidate") assert cc.proxy_revalidate is True def test_immutable(self): """immutable directive [RFC8246].""" cc = parse_cache_control("immutable") assert cc.immutable is True class TestExperimentalDirectives: """Test experimental directives from RFC5861.""" def test_stale_if_error(self): """stale-if-error directive [RFC5861, Section 4].""" cc = parse_cache_control("stale-if-error=86400") assert cc.stale_if_error == 86400 def test_stale_if_error_invalid(self): """Invalid stale-if-error value.""" cc = parse_cache_control("stale-if-error=bad") assert cc.stale_if_error is None def test_stale_while_revalidate(self): """stale-while-revalidate directive [RFC5861, Section 3].""" cc = parse_cache_control("stale-while-revalidate=120") assert cc.stale_while_revalidate == 120 def test_stale_while_revalidate_invalid(self): """Invalid stale-while-revalidate value.""" cc = parse_cache_control("stale-while-revalidate=xyz") assert cc.stale_while_revalidate is None class TestExtensions: """Test handling of unrecognized directives.""" def test_unknown_directive_without_value(self): """Unknown directive without value.""" cc = parse_cache_control("custom-directive") assert "custom-directive" in cc.extensions def test_unknown_directive_with_value(self): """Unknown directive with value.""" cc = parse_cache_control("custom-directive=value") assert "custom-directive=value" in cc.extensions def test_multiple_extensions(self): """Multiple unknown directives.""" cc = parse_cache_control("ext1=val1, ext2, ext3=val3") assert "ext1=val1" in cc.extensions assert "ext2" in cc.extensions assert "ext3=val3" in cc.extensions def test_extensions_with_known_directives(self): """Mix of known and unknown directives.""" cc = parse_cache_control("max-age=3600, custom-ext=foo, no-cache") assert cc.max_age == 3600 assert cc.no_cache is True assert "custom-ext=foo" in cc.extensions class TestQuotedValues: """Test handling of quoted values.""" def test_quoted_field_names(self): """Quoted field names in no-cache.""" cc = parse_cache_control('no-cache="Set-Cookie"') assert isinstance(cc.no_cache, list) assert "Set-Cookie" in cc.no_cache def test_quoted_extension_value(self): """Quoted extension value.""" cc = parse_cache_control('custom="quoted value"') assert "custom=quoted value" in cc.extensions def test_escaped_quotes_in_value(self): """Escaped quotes within quoted value.""" cc = parse_cache_control('custom="value with \\"quotes\\""') assert any('value with "quotes"' in ext for ext in cc.extensions) class TestWhitespaceHandling: """Test handling of whitespace.""" def test_spaces_around_commas(self): """Spaces around commas.""" cc = parse_cache_control("max-age=3600 , no-cache , public") assert cc.max_age == 3600 assert cc.no_cache is True assert cc.public is True def test_spaces_around_equals(self): """Spaces around equals sign.""" cc = parse_cache_control("max-age = 3600") assert cc.max_age == 3600 def test_leading_trailing_spaces(self): """Leading and trailing spaces.""" cc = parse_cache_control(" max-age=3600, no-cache ") assert cc.max_age == 3600 assert cc.no_cache is True def test_multiple_spaces(self): """Multiple consecutive spaces.""" cc = parse_cache_control("max-age=3600, no-cache, public") assert cc.max_age == 3600 assert cc.no_cache is True assert cc.public is True class TestRequestDirectives: """Test typical request Cache-Control headers.""" def test_request_no_cache(self): """Request with no-cache.""" cc = parse_cache_control("no-cache") assert cc.no_cache is True def test_request_max_age_zero(self): """Request with max-age=0 (force revalidation).""" cc = parse_cache_control("max-age=0") assert cc.max_age == 0 def test_request_no_store(self): """Request with no-store.""" cc = parse_cache_control("no-store") assert cc.no_store is True def test_request_max_age_and_max_stale(self): """Request with max-age and max-stale.""" cc = parse_cache_control("max-age=600, max-stale=300") assert cc.max_age == 600 assert cc.max_stale == 300 def test_request_only_if_cached(self): """Request with only-if-cached.""" cc = parse_cache_control("only-if-cached") assert cc.only_if_cached is True def test_request_min_fresh(self): """Request with min-fresh.""" cc = parse_cache_control("min-fresh=300") assert cc.min_fresh == 300 class TestResponseDirectives: """Test typical response Cache-Control headers.""" def test_response_public_max_age(self): """Public cacheable response.""" cc = parse_cache_control("public, max-age=3600") assert cc.public is True assert cc.max_age == 3600 def test_response_private(self): """Private response.""" cc = parse_cache_control("private, max-age=0") assert cc.private is True assert cc.max_age == 0 def test_response_no_cache_must_revalidate(self): """Response requiring revalidation.""" cc = parse_cache_control("no-cache, must-revalidate") assert cc.no_cache is True assert cc.must_revalidate is True def test_response_immutable(self): """Immutable response.""" cc = parse_cache_control("public, max-age=31536000, immutable") assert cc.public is True assert cc.max_age == 31536000 assert cc.immutable is True def test_response_s_maxage(self): """Response with s-maxage for shared caches.""" cc = parse_cache_control("public, max-age=600, s-maxage=3600") assert cc.public is True assert cc.max_age == 600 assert cc.s_maxage == 3600 def test_response_proxy_revalidate(self): """Response with proxy-revalidate.""" cc = parse_cache_control("public, max-age=3600, proxy-revalidate") assert cc.public is True assert cc.max_age == 3600 assert cc.proxy_revalidate is True def test_response_must_understand(self): """Response with must-understand.""" cc = parse_cache_control("must-understand, no-store") assert cc.must_understand is True assert cc.no_store is True class TestRealWorldExamples: """Test real-world Cache-Control header examples.""" def test_cdn_static_asset(self): """Typical CDN static asset header.""" cc = parse_cache_control("public, max-age=31536000, immutable") assert cc.public is True assert cc.max_age == 31536000 assert cc.immutable is True def test_api_no_cache(self): """API endpoint that shouldn't be cached.""" cc = parse_cache_control("no-store, no-cache, must-revalidate, proxy-revalidate") assert cc.no_store is True assert cc.no_cache is True assert cc.must_revalidate is True assert cc.proxy_revalidate is True def test_private_user_data(self): """Private user data response.""" cc = parse_cache_control("private, max-age=0, no-cache") assert cc.private is True assert cc.max_age == 0 assert cc.no_cache is True def test_cdn_with_stale_while_revalidate(self): """CDN with stale-while-revalidate.""" cc = parse_cache_control("max-age=3600, stale-while-revalidate=86400") assert cc.max_age == 3600 assert cc.stale_while_revalidate == 86400 def test_cdn_with_stale_if_error(self): """CDN with stale-if-error fallback.""" cc = parse_cache_control("max-age=600, stale-if-error=86400") assert cc.max_age == 600 assert cc.stale_if_error == 86400 def test_aws_cloudfront_example(self): """AWS CloudFront example header.""" cc = parse_cache_control("public, max-age=0, s-maxage=2592000") assert cc.public is True assert cc.max_age == 0 assert cc.s_maxage == 2592000 def test_github_static_asset(self): """GitHub static asset header.""" cc = parse_cache_control("max-age=31536000, immutable") assert cc.max_age == 31536000 assert cc.immutable is True class TestEdgeCases: """Test edge cases and error conditions.""" def test_duplicate_directives(self): """Duplicate directives (last one wins for values).""" cc = parse_cache_control("max-age=100, max-age=200") assert cc.max_age == 200 def test_case_insensitive_directives(self): """Directives should be case-insensitive.""" cc = parse_cache_control("MAX-AGE=3600, NO-CACHE, PUBLIC") assert cc.max_age == 3600 assert cc.no_cache is True assert cc.public is True def test_empty_directive(self): """Empty directive between commas.""" cc = parse_cache_control("max-age=3600,, no-cache") assert cc.max_age == 3600 assert cc.no_cache is True def test_directive_with_empty_value(self): """Directive with empty value.""" cc = parse_cache_control("max-age=") assert cc.max_age is None def test_malformed_equals(self): """Multiple equals signs.""" cc = parse_cache_control("max-age==3600") # Should handle gracefully assert isinstance(cc, CacheControl) def test_only_commas(self): """String with only commas.""" cc = parse_cache_control(",,,") assert cc.max_age is None assert cc.no_cache is False def test_trailing_comma(self): """Trailing comma.""" cc = parse_cache_control("max-age=3600, no-cache,") assert cc.max_age == 3600 assert cc.no_cache is True def test_leading_comma(self): """Leading comma.""" cc = parse_cache_control(",max-age=3600, no-cache") assert cc.max_age == 3600 assert cc.no_cache is True class TestComplexCombinations: """Test complex combinations of directives.""" def test_all_request_directives(self): """Combine all request directives.""" cc = parse_cache_control( "max-age=100, max-stale=200, min-fresh=300, no-cache, no-store, no-transform, only-if-cached" ) assert cc.max_age == 100 assert cc.max_stale == 200 assert cc.min_fresh == 300 assert cc.no_cache is True assert cc.no_store is True assert cc.no_transform is True assert cc.only_if_cached is True def test_all_response_directives(self): """Combine multiple response directives.""" cc = parse_cache_control("public, max-age=3600, s-maxage=7200, must-revalidate, proxy-revalidate, immutable") assert cc.public is True assert cc.max_age == 3600 assert cc.s_maxage == 7200 assert cc.must_revalidate is True assert cc.proxy_revalidate is True assert cc.immutable is True def test_mixed_with_extensions(self): """Mix standard directives with extensions.""" cc = parse_cache_control("max-age=3600, custom-ext=foo, no-cache, another-ext, stale-while-revalidate=120") assert cc.max_age == 3600 assert cc.no_cache is True assert cc.stale_while_revalidate == 120 assert "custom-ext=foo" in cc.extensions assert "another-ext" in cc.extensions class TestDefaultValues: """Test that unset directives have correct default values.""" def test_all_defaults(self): """Check all default values for empty header.""" cc = parse_cache_control("") # Time-based directives should be None assert cc.max_age is None assert cc.max_stale is None assert cc.min_fresh is None assert cc.s_maxage is None assert cc.stale_if_error is None assert cc.stale_while_revalidate is None # Boolean directives should be False assert cc.no_store is False assert cc.no_transform is False assert cc.only_if_cached is False assert cc.must_revalidate is False assert cc.must_understand is False assert cc.public is False assert cc.proxy_revalidate is False assert cc.immutable is False # Special directives should be False assert cc.no_cache is False assert cc.private is False # Extensions should be empty list assert cc.extensions == [] karpetrosyan-hishel-fd309b1/tests/conftest.py000066400000000000000000000132171514113157400214670ustar00rootroot00000000000000import json import os import sqlite3 from datetime import date from typing import Any import anysqlite import pytest def format_value(value: Any, col_name: str, col_type: str) -> str: """Format a value for display based on its type and column name.""" if value is None: return "NULL" # Handle BLOB columns if col_type.upper() == "BLOB": if isinstance(value, bytes): # Try to decode as UTF-8 string first try: decoded = value.decode("utf-8") # Check if it looks like JSON if decoded.strip().startswith("{") or decoded.strip().startswith("["): try: parsed = json.loads(decoded) return f"(JSON) {json.dumps(parsed, indent=2)}" except json.JSONDecodeError: pass # Show string if it's printable if all(32 <= ord(c) <= 126 or c in "\n\r\t" for c in decoded): return f"(str) '{decoded}'" except UnicodeDecodeError: pass # Show hex representation for binary data hex_str = value.hex() if len(hex_str) > 64: return f"(bytes) 0x{hex_str[:60]}... ({len(value)} bytes)" return f"(bytes) 0x{hex_str} ({len(value)} bytes)" return repr(value) # Handle timestamps - ONLY show date, not the raw timestamp if col_name.endswith("_at") and isinstance(value, (int, float)): try: dt = date.fromtimestamp(value) return dt.isoformat() # Changed: removed the timestamp prefix except (ValueError, OSError): return str(value) # Handle TEXT columns if col_type.upper() == "TEXT": return f"'{value}'" # Handle other types return str(value) def print_sqlite_state(conn: sqlite3.Connection) -> str: """ Print all tables and their rows in a pretty format suitable for inline snapshots. Args: conn: SQLite database connection Returns: Formatted string representation of the database state """ cursor = conn.cursor() # Get all table names cursor.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name") tables = [row[0] for row in cursor.fetchall()] output_lines = [] output_lines.append("=" * 80) output_lines.append("DATABASE SNAPSHOT") output_lines.append("=" * 80) for table_name in tables: # Get column information cursor.execute(f"PRAGMA table_info({table_name})") columns = cursor.fetchall() column_names = [col[1] for col in columns] column_types = {col[1]: col[2] for col in columns} # Get all rows cursor.execute(f"SELECT * FROM {table_name}") rows = cursor.fetchall() output_lines.append("") output_lines.append(f"TABLE: {table_name}") output_lines.append("-" * 80) output_lines.append(f"Rows: {len(rows)}") output_lines.append("") if not rows: output_lines.append(" (empty)") continue # Format each row for idx, row in enumerate(rows, 1): output_lines.append(f" Row {idx}:") for col_name, value in zip(column_names, row): col_type = column_types[col_name] formatted_value = format_value(value, col_name, col_type) output_lines.append(f" {col_name:15} = {formatted_value}") if idx < len(rows): output_lines.append("") output_lines.append("") output_lines.append("=" * 80) result = "\n".join(output_lines) return result async def aprint_sqlite_state(conn: anysqlite.Connection) -> str: """ Print all tables and their rows in a pretty format suitable for inline snapshots. Args: conn: SQLite database connection Returns: Formatted string representation of the database state """ cursor = await conn.cursor() # Get all table names await cursor.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name") tables = [row[0] for row in await cursor.fetchall()] output_lines = [] output_lines.append("=" * 80) output_lines.append("DATABASE SNAPSHOT") output_lines.append("=" * 80) for table_name in tables: # Get column information await cursor.execute(f"PRAGMA table_info({table_name})") columns = await cursor.fetchall() column_names = [col[1] for col in columns] column_types = {col[1]: col[2] for col in columns} # Get all rows await cursor.execute(f"SELECT * FROM {table_name}") rows = await cursor.fetchall() output_lines.append("") output_lines.append(f"TABLE: {table_name}") output_lines.append("-" * 80) output_lines.append(f"Rows: {len(rows)}") output_lines.append("") if not rows: output_lines.append(" (empty)") continue # Format each row for idx, row in enumerate(rows, 1): output_lines.append(f" Row {idx}:") for col_name, value in zip(column_names, row): col_type = column_types[col_name] formatted_value = format_value(value, col_name, col_type) output_lines.append(f" {col_name:15} = {formatted_value}") if idx < len(rows): output_lines.append("") output_lines.append("") output_lines.append("=" * 80) result = "\n".join(output_lines) return result @pytest.fixture() def use_temp_dir(tmpdir): cur_dir = os.getcwd() os.chdir(tmpdir) yield os.chdir(cur_dir) karpetrosyan-hishel-fd309b1/tests/test_asgi.py000066400000000000000000000532641514113157400216320ustar00rootroot00000000000000from __future__ import annotations import gzip from datetime import datetime from typing import Any from zoneinfo import ZoneInfo import anysqlite import pytest from inline_snapshot import snapshot from time_machine import travel from hishel import AsyncSqliteStorage, CacheOptions from hishel._policies import FilterPolicy, SpecificationPolicy from hishel.asgi import ASGICacheMiddleware, _ASGIScope # Mock ASGI application that returns a simple response async def simple_asgi_app(scope: _ASGIScope, receive: Any, send: Any) -> None: """Simple ASGI app that returns a 200 OK response with cache headers.""" if scope["type"] != "http": return await send( { "type": "http.response.start", "status": 200, "headers": [ (b"content-type", b"text/plain"), (b"cache-control", b"public, max-age=3600"), (b"content-length", b"13"), ], } ) await send( { "type": "http.response.body", "body": b"Hello, World!", "more_body": False, } ) # Mock ASGI application that returns no-cache response async def no_cache_asgi_app(scope: _ASGIScope, receive: Any, send: Any) -> None: """ASGI app that returns a response without cache headers.""" if scope["type"] != "http": return await send( { "type": "http.response.start", "status": 200, "headers": [ (b"content-type", b"text/plain"), (b"cache-control", b"no-cache"), (b"content-length", b"13"), ], } ) await send( { "type": "http.response.body", "body": b"Hello, World!", "more_body": False, } ) # Mock ASGI application that returns gzipped content async def gzipped_asgi_app(scope: _ASGIScope, receive: Any, send: Any) -> None: """ASGI app that returns gzip-encoded content.""" if scope["type"] != "http": return data = gzip.compress(b"a" * 1000) await send( { "type": "http.response.start", "status": 200, "headers": [ (b"content-type", b"text/plain"), (b"content-encoding", b"gzip"), (b"content-length", str(len(data)).encode()), (b"cache-control", b"public, max-age=3600"), ], } ) await send( { "type": "http.response.body", "body": data, "more_body": False, } ) # Mock ASGI application that streams response in chunks async def streaming_asgi_app(scope: _ASGIScope, receive: Any, send: Any) -> None: """ASGI app that streams response in multiple chunks.""" if scope["type"] != "http": return await send( { "type": "http.response.start", "status": 200, "headers": [ (b"content-type", b"text/plain"), (b"cache-control", b"public, max-age=3600"), ], } ) # Send multiple chunks for i in range(5): await send( { "type": "http.response.body", "body": f"Chunk {i}\n".encode(), "more_body": True, } ) # Final chunk await send( { "type": "http.response.body", "body": b"", "more_body": False, } ) # Mock ASGI application that echoes the request method async def echo_method_asgi_app(scope: _ASGIScope, receive: Any, send: Any) -> None: """ASGI app that echoes the HTTP method in the response.""" if scope["type"] != "http": return method = scope.get("method", "UNKNOWN") body = f"Method: {method}".encode() await send( { "type": "http.response.start", "status": 200, "headers": [ (b"content-type", b"text/plain"), (b"cache-control", b"public, max-age=3600"), (b"content-length", str(len(body)).encode()), ], } ) await send( { "type": "http.response.body", "body": body, "more_body": False, } ) # Helper function to create ASGI scope def create_asgi_scope( method: str = "GET", path: str = "/", query_string: bytes = b"", headers: list[tuple[bytes, bytes]] | None = None, ) -> _ASGIScope: """Create a basic ASGI HTTP scope dictionary.""" return { "type": "http", "asgi": {"version": "3.0"}, "http_version": "1.1", "method": method, "scheme": "https", "path": path, "query_string": query_string, "root_path": "", "headers": headers or [], "server": ("testserver", 80), "client": ("127.0.0.1", 8000), "state": {}, "extensions": {}, } # Helper function to create a simple receive callable async def simple_receive() -> dict[str, Any]: """Simple receive callable that returns http.disconnect.""" return {"type": "http.disconnect"} # Helper class to collect ASGI responses class ResponseCollector: """Collect response data from ASGI send calls.""" def __init__(self) -> None: self.status: int = 0 self.headers: list[tuple[bytes, bytes]] = [] self.body_chunks: list[bytes] = [] async def send(self, message: dict[str, Any]) -> None: """Collect response data from send calls.""" if message["type"] == "http.response.start": self.status = message["status"] self.headers = message.get("headers", []) elif message["type"] == "http.response.body": body = message.get("body", b"") if body: self.body_chunks.append(body) def get_body(self) -> bytes: """Get the complete response body.""" return b"".join(self.body_chunks) def get_header(self, name: bytes) -> bytes | None: """Get a response header by name.""" for key, value in self.headers: if key.lower() == name.lower(): return value return None @pytest.mark.anyio @travel(datetime(2024, 1, 1, 0, 0, 0, tzinfo=ZoneInfo("UTC")), tick=False) async def test_simple_caching(caplog: pytest.LogCaptureFixture) -> None: """Test that basic caching works with ASGI middleware.""" storage = AsyncSqliteStorage(connection=await anysqlite.connect(":memory:")) middleware = ASGICacheMiddleware(app=simple_asgi_app, storage=storage) # First request - cache miss scope = create_asgi_scope() collector1 = ResponseCollector() with caplog.at_level("DEBUG", logger="hishel"): await middleware(scope, simple_receive, collector1.send) assert collector1.status == 200 assert collector1.get_body() == b"Hello, World!" # Check that cache miss occurred assert any("CacheMiss" in msg for msg in caplog.messages) collector2 = ResponseCollector() with caplog.at_level("DEBUG", logger="hishel"): await middleware(scope, simple_receive, collector2.send) assert caplog.messages == snapshot( [ "Incoming HTTP request: method=GET path=/", "Building internal request: method=GET url=https://testserver:80/ headers_count=0", "Converted ASGI request to internal format: url=https://testserver:80/", "Handling request through cache proxy", "Handling state: IdleClient", "Handling state: CacheMiss", "Sending request to wrapped application: url=https://testserver:80/", "Application response started: status=200", "Received response body chunk: size=13 bytes", "Application response complete: status=200 total_bytes=13 chunks=1", "Added Date header to response: Mon, 01 Jan 2024 00:00:00 GMT", "Storing response in cache", "Handling state: StoreAndUse", "Request processed: method=GET path=/ status=200", "Sending response to client: status=200 headers_count=4", "Response headers sent", "Sent response chunk: size=13 bytes", "Response fully sent: status=200 total_bytes=13 chunks=1", "Response sent successfully", "Incoming HTTP request: method=GET path=/", "Building internal request: method=GET url=https://testserver:80/ headers_count=0", "Converted ASGI request to internal format: url=https://testserver:80/", "Handling request through cache proxy", "Handling state: IdleClient", "Handling state: FromCache", "Request processed: method=GET path=/ status=200", "Sending response to client: status=200 headers_count=5", "Response headers sent", "Sent response chunk: size=13 bytes", "Response fully sent: status=200 total_bytes=13 chunks=1", "Response sent successfully", ] ) assert collector2.status == 200 assert collector2.get_body() == b"Hello, World!" await middleware.aclose() @pytest.mark.anyio @travel(datetime(2024, 1, 1, 0, 0, 0, tzinfo=ZoneInfo("UTC")), tick=False) async def test_simple_caching_ignoring_spec(caplog: pytest.LogCaptureFixture) -> None: storage = AsyncSqliteStorage(connection=await anysqlite.connect(":memory:")) middleware = ASGICacheMiddleware(app=no_cache_asgi_app, storage=storage, policy=FilterPolicy()) # First request scope = create_asgi_scope() collector1 = ResponseCollector() with caplog.at_level("DEBUG", logger="hishel"): await middleware(scope, simple_receive, collector1.send) assert collector1.status == 200 assert collector1.get_body() == b"Hello, World!" # Should cache despite no-cache header assert any("ignoring specification" in msg.lower() for msg in caplog.messages) collector2 = ResponseCollector() with caplog.at_level("DEBUG", logger="hishel"): await middleware(scope, simple_receive, collector2.send) assert caplog.messages == snapshot( [ "Incoming HTTP request: method=GET path=/", "Building internal request: method=GET url=https://testserver:80/ headers_count=0", "Converted ASGI request to internal format: url=https://testserver:80/", "Handling request through cache proxy", "Trying to get cached response ignoring specification", "Found 0 cached entries for the request", "Sending request to wrapped application: url=https://testserver:80/", "Application response started: status=200", "Received response body chunk: size=13 bytes", "Application response complete: status=200 total_bytes=13 chunks=1", "Added Date header to response: Mon, 01 Jan 2024 00:00:00 GMT", "Storing response in cache ignoring specification", "Request processed: method=GET path=/ status=200", "Sending response to client: status=200 headers_count=4", "Response headers sent", "Sent response chunk: size=13 bytes", "Response fully sent: status=200 total_bytes=13 chunks=1", "Response sent successfully", "Incoming HTTP request: method=GET path=/", "Building internal request: method=GET url=https://testserver:80/ headers_count=0", "Converted ASGI request to internal format: url=https://testserver:80/", "Handling request through cache proxy", "Trying to get cached response ignoring specification", "Found 1 cached entries for the request", "Found matching cached response for the request", "Request processed: method=GET path=/ status=200", "Sending response to client: status=200 headers_count=4", "Response headers sent", "Sent response chunk: size=13 bytes", "Response fully sent: status=200 total_bytes=13 chunks=1", "Response sent successfully", ] ) assert collector2.status == 200 assert collector2.get_body() == b"Hello, World!" await middleware.aclose() @pytest.mark.anyio async def test_encoded_content_caching() -> None: """Test that gzip-encoded content is cached correctly.""" storage = AsyncSqliteStorage(connection=await anysqlite.connect(":memory:")) middleware = ASGICacheMiddleware( app=gzipped_asgi_app, storage=storage, policy=FilterPolicy(), ) # First request - cache miss scope = create_asgi_scope() collector1 = ResponseCollector() await middleware(scope, simple_receive, collector1.send) assert collector1.status == 200 data = collector1.get_body() assert collector1.get_header(b"content-encoding") == b"gzip" assert len(data) > 0 # Verify it's actually gzipped decompressed = gzip.decompress(data) assert decompressed == b"a" * 1000 # Second request - cache hit collector2 = ResponseCollector() await middleware(scope, simple_receive, collector2.send) assert collector2.status == 200 cached_data = collector2.get_body() assert cached_data == data assert collector2.get_header(b"content-encoding") == b"gzip" await middleware.aclose() @pytest.mark.anyio async def test_streaming_response_caching() -> None: """Test that streaming responses are cached correctly.""" storage = AsyncSqliteStorage(connection=await anysqlite.connect(":memory:")) middleware = ASGICacheMiddleware( app=streaming_asgi_app, storage=storage, policy=FilterPolicy(), ) # First request - cache miss scope = create_asgi_scope() collector1 = ResponseCollector() await middleware(scope, simple_receive, collector1.send) assert collector1.status == 200 body1 = collector1.get_body() expected = b"".join(f"Chunk {i}\n".encode() for i in range(5)) assert body1 == expected # Second request - cache hit collector2 = ResponseCollector() await middleware(scope, simple_receive, collector2.send) assert collector2.status == 200 body2 = collector2.get_body() assert body2 == body1 await middleware.aclose() @pytest.mark.anyio async def test_different_methods() -> None: """Test that different HTTP methods are cached separately.""" storage = AsyncSqliteStorage(connection=await anysqlite.connect(":memory:")) middleware = ASGICacheMiddleware( app=echo_method_asgi_app, storage=storage, policy=FilterPolicy(), ) # GET request scope_get = create_asgi_scope(method="GET") collector_get = ResponseCollector() await middleware(scope_get, simple_receive, collector_get.send) assert collector_get.status == 200 assert collector_get.get_body() == b"Method: GET" # POST request scope_post = create_asgi_scope(method="POST") collector_post = ResponseCollector() await middleware(scope_post, simple_receive, collector_post.send) assert collector_post.status == 200 assert collector_post.get_body() == b"Method: POST" # Second GET request - should hit cache collector_get2 = ResponseCollector() await middleware(scope_get, simple_receive, collector_get2.send) assert collector_get2.status == 200 assert collector_get2.get_body() == b"Method: GET" await middleware.aclose() @pytest.mark.anyio async def test_different_paths() -> None: """Test that different paths are cached separately.""" storage = AsyncSqliteStorage(connection=await anysqlite.connect(":memory:")) middleware = ASGICacheMiddleware( app=simple_asgi_app, storage=storage, policy=FilterPolicy(), ) # Request to /path1 scope1 = create_asgi_scope(path="/path1") collector1 = ResponseCollector() await middleware(scope1, simple_receive, collector1.send) assert collector1.status == 200 # Request to /path2 scope2 = create_asgi_scope(path="/path2") collector2 = ResponseCollector() await middleware(scope2, simple_receive, collector2.send) assert collector2.status == 200 # Second request to /path1 - should hit cache collector1_cached = ResponseCollector() await middleware(scope1, simple_receive, collector1_cached.send) assert collector1_cached.status == 200 await middleware.aclose() @pytest.mark.anyio async def test_query_strings() -> None: """Test that query strings are included in cache keys.""" storage = AsyncSqliteStorage(connection=await anysqlite.connect(":memory:")) middleware = ASGICacheMiddleware( app=simple_asgi_app, storage=storage, policy=FilterPolicy(), ) # Request with query string 1 scope1 = create_asgi_scope(query_string=b"param=value1") collector1 = ResponseCollector() await middleware(scope1, simple_receive, collector1.send) assert collector1.status == 200 # Request with query string 2 scope2 = create_asgi_scope(query_string=b"param=value2") collector2 = ResponseCollector() await middleware(scope2, simple_receive, collector2.send) assert collector2.status == 200 # Second request with query string 1 - should hit cache collector1_cached = ResponseCollector() await middleware(scope1, simple_receive, collector1_cached.send) assert collector1_cached.status == 200 await middleware.aclose() @pytest.mark.anyio async def test_non_http_scope() -> None: """Test that non-HTTP scopes are passed through without caching.""" storage = AsyncSqliteStorage(connection=await anysqlite.connect(":memory:")) # Track if the app was called app_called = False async def websocket_app(scope: _ASGIScope, receive: Any, send: Any) -> None: nonlocal app_called app_called = True middleware = ASGICacheMiddleware(app=websocket_app, storage=storage) # WebSocket scope scope = { "type": "websocket", "path": "/ws", } await middleware(scope, simple_receive, lambda msg: None) # type: ignore # App should have been called directly assert app_called await middleware.aclose() @pytest.mark.anyio async def test_custom_cache_options() -> None: """Test that custom cache options are respected.""" storage = AsyncSqliteStorage(connection=await anysqlite.connect(":memory:")) cache_options = CacheOptions() middleware = ASGICacheMiddleware( app=simple_asgi_app, storage=storage, policy=SpecificationPolicy( cache_options=cache_options, ), ) scope = create_asgi_scope() collector = ResponseCollector() await middleware(scope, simple_receive, collector.send) assert collector.status == 200 assert collector.get_body() == b"Hello, World!" await middleware.aclose() @pytest.mark.anyio async def test_request_with_body() -> None: """Test that requests with bodies are handled correctly.""" storage = AsyncSqliteStorage(connection=await anysqlite.connect(":memory:")) # Track if body was received received_body = [] async def body_echo_app(scope: _ASGIScope, receive: Any, send: Any) -> None: """ASGI app that collects the request body.""" if scope["type"] != "http": return # Read request body while True: message = await receive() if message["type"] == "http.request": body = message.get("body", b"") if body: received_body.append(body) if not message.get("more_body", False): break elif message["type"] == "http.disconnect": break # Send response await send( { "type": "http.response.start", "status": 200, "headers": [ (b"content-type", b"text/plain"), (b"cache-control", b"public, max-age=3600"), ], } ) await send( { "type": "http.response.body", "body": b"Body received", "more_body": False, } ) middleware = ASGICacheMiddleware(app=body_echo_app, storage=storage) # Create receive callable that sends body body_sent = False async def receive_with_body() -> dict[str, Any]: nonlocal body_sent if not body_sent: body_sent = True return { "type": "http.request", "body": b"Test body content", "more_body": False, } return {"type": "http.disconnect"} scope = create_asgi_scope(method="POST") collector = ResponseCollector() await middleware(scope, receive_with_body, collector.send) assert collector.status == 200 assert collector.get_body() == b"Body received" assert b"Test body content" in received_body await middleware.aclose() @pytest.mark.anyio async def test_headers_are_preserved() -> None: """Test that response headers are preserved in cached responses.""" storage = AsyncSqliteStorage(connection=await anysqlite.connect(":memory:")) middleware = ASGICacheMiddleware( app=simple_asgi_app, storage=storage, policy=FilterPolicy(), ) # First request scope = create_asgi_scope() collector1 = ResponseCollector() await middleware(scope, simple_receive, collector1.send) assert collector1.get_header(b"content-type") == b"text/plain" assert collector1.get_header(b"cache-control") is not None # Second request - from cache collector2 = ResponseCollector() await middleware(scope, simple_receive, collector2.send) # Headers should be preserved assert collector2.get_header(b"content-type") == b"text/plain" assert collector2.get_header(b"cache-control") is not None await middleware.aclose() karpetrosyan-hishel-fd309b1/tests/test_async_httpx.py000066400000000000000000000105461514113157400232470ustar00rootroot00000000000000import gzip from datetime import datetime from zoneinfo import ZoneInfo import anysqlite import httpx import pytest from httpx import ByteStream, MockTransport from inline_snapshot import snapshot from time_machine import travel from hishel import AsyncSqliteStorage from hishel._policies import FilterPolicy from hishel.httpx import AsyncCacheClient, AsyncCacheTransport @pytest.mark.anyio @travel(datetime(2024, 1, 1, 0, 0, 0, tzinfo=ZoneInfo("UTC")), tick=False) async def test_simple_caching(caplog: pytest.LogCaptureFixture) -> None: client = AsyncCacheClient( storage=AsyncSqliteStorage(connection=await anysqlite.connect(":memory:")), ) with caplog.at_level("DEBUG", logger="hishel"): await client.get("https://hishel.com") response = await client.get("https://hishel.com") assert caplog.messages == snapshot( [ "Handling state: IdleClient", "Handling state: CacheMiss", "Storing response in cache", "Handling state: StoreAndUse", "Handling state: IdleClient", "Handling state: FromCache", ] ) assert response.extensions == snapshot( { "hishel_from_cache": True, "hishel_created_at": 1704067200.0, "hishel_revalidated": False, "hishel_stored": False, } ) @pytest.mark.anyio @travel(datetime(2024, 1, 1, 0, 0, 0, tzinfo=ZoneInfo("UTC")), tick=False) async def test_simple_caching_ignoring_spec(caplog: pytest.LogCaptureFixture) -> None: client = AsyncCacheClient( storage=AsyncSqliteStorage(connection=await anysqlite.connect(":memory:")), policy=FilterPolicy(), ) with caplog.at_level("DEBUG", logger="hishel"): await client.get("https://hishel.com", extensions={"hishel_spec_ignore": True}) response = await client.get("https://hishel.com", extensions={"hishel_spec_ignore": True}) assert caplog.messages == snapshot( [ "Trying to get cached response ignoring specification", "Found 0 cached entries for the request", "Storing response in cache ignoring specification", "Trying to get cached response ignoring specification", "Found 1 cached entries for the request", "Found matching cached response for the request", ] ) assert response.extensions == snapshot( { "hishel_from_cache": True, "hishel_created_at": 1704067200.0, "hishel_revalidated": False, "hishel_stored": False, } ) @pytest.mark.anyio async def test_encoded_content_caching() -> None: data = gzip.compress(b"a" * 1000) compressed_data = ByteStream(data) mocked_responses = [ httpx.Response( 200, stream=compressed_data, headers={ "Content-Encoding": "gzip", "Content-Type": "text/plain", "Content-Length": str(len(data)), }, ) ] async def handler(request: httpx.Request) -> httpx.Response: if not mocked_responses: raise RuntimeError("No more mocked responses available") return mocked_responses.pop(0) storage = AsyncSqliteStorage(connection=await anysqlite.connect(":memory:")) client = AsyncCacheClient( transport=AsyncCacheTransport( next_transport=MockTransport(handler=handler), storage=storage, policy=FilterPolicy() ), ) # First request - should fetch from the mocked transport and store in cache async with client.stream("get", "https://localhost", extensions={"hishel_spec_ignore": True}) as response: response_data = b"".join([chunk async for chunk in response.aiter_raw()]) assert data == response_data assert response.headers.get("Content-Length") == str(len(data)) == str(len(response_data)) assert response.headers.get("Content-Encoding") == "gzip" # Second request - should fetch from cache async with client.stream("get", "https://localhost", extensions={"hishel_spec_ignore": True}) as response: response_data = b"".join([chunk async for chunk in response.aiter_raw()]) assert data == response_data assert response.headers.get("Content-Length") == str(len(data)) == str(len(response_data)) assert response.headers.get("Content-Encoding") == "gzip" karpetrosyan-hishel-fd309b1/tests/test_requests.py000066400000000000000000000061221514113157400225510ustar00rootroot00000000000000from typing import Any import pytest from inline_snapshot import snapshot from requests import Session from hishel._utils import filter_mapping from hishel.requests import CacheAdapter def test_simple_caching(use_temp_dir: Any, caplog: pytest.LogCaptureFixture) -> None: session = Session() adapter = CacheAdapter() session.mount("http://", adapter) session.mount("https://", adapter) with caplog.at_level("DEBUG", logger="hishel"): session.get("https://hishel.com") response = session.get("https://hishel.com") assert caplog.messages == snapshot( [ "Handling state: IdleClient", "Handling state: CacheMiss", "Storing response in cache", "Handling state: StoreAndUse", "Handling state: IdleClient", "Handling state: FromCache", ] ) assert filter_mapping( {k: v for k, v in response.headers.items() if k.lower().startswith("x-hishel")}, ["x-hishel-created-at"] ) == snapshot( { "X-Hishel-From-Cache": "True", "X-Hishel-Revalidated": "False", "X-Hishel-Stored": "False", } ) def test_simple_caching_ignoring_spec(use_temp_dir: Any, caplog: pytest.LogCaptureFixture) -> None: session = Session() adapter = CacheAdapter() session.mount("http://", adapter) session.mount("https://", adapter) with caplog.at_level("DEBUG", logger="hishel"): session.get("https://hishel.com", headers={"x-hishel-spec-ignore": "True"}) response = session.get("https://hishel.com", headers={"x-hishel-spec-ignore": "True"}) assert caplog.messages == snapshot( [ "Handling state: IdleClient", "Handling state: CacheMiss", "Storing response in cache", "Handling state: StoreAndUse", "Handling state: IdleClient", "Handling state: FromCache", ] ) assert filter_mapping( {k: v for k, v in response.headers.items() if k.lower().startswith("x-hishel")}, ["x-hishel-created-at"] ) == snapshot( { "X-Hishel-From-Cache": "True", "X-Hishel-Revalidated": "False", "X-Hishel-Stored": "False", } ) def test_encoded_content_caching(use_temp_dir: Any) -> None: """Test that gzip-encoded content is properly cached and retrieved.""" session = Session() adapter = CacheAdapter() session.mount("https://", adapter) # First request - should fetch from mock and store in cache response1 = session.get("https://httpbingo.org/gzip", headers={"x-hishel-spec-ignore": "True"}) # just check that we can read the content and decode it properly response1.json() assert response1.headers.get("Content-Encoding") == "gzip" # Second request - should fetch from cache (no additional mock call) response2 = session.get("https://httpbingo.org/gzip", headers={"x-hishel-spec-ignore": "True"}) # just check that we can read the content and decode it properly response2.json() assert response2.headers.get("Content-Encoding") == "gzip" karpetrosyan-hishel-fd309b1/tests/test_sync_httpx.py000066400000000000000000000103021514113157400230740ustar00rootroot00000000000000import gzip from datetime import datetime from zoneinfo import ZoneInfo import sqlite3 import httpx import pytest from httpx import ByteStream, MockTransport from inline_snapshot import snapshot from time_machine import travel from hishel import SyncSqliteStorage from hishel._policies import FilterPolicy from hishel.httpx import SyncCacheClient, SyncCacheTransport @travel(datetime(2024, 1, 1, 0, 0, 0, tzinfo=ZoneInfo("UTC")), tick=False) def test_simple_caching(caplog: pytest.LogCaptureFixture) -> None: client = SyncCacheClient( storage=SyncSqliteStorage(connection=sqlite3.connect(":memory:")), ) with caplog.at_level("DEBUG", logger="hishel"): client.get("https://hishel.com") response = client.get("https://hishel.com") assert caplog.messages == snapshot( [ "Handling state: IdleClient", "Handling state: CacheMiss", "Storing response in cache", "Handling state: StoreAndUse", "Handling state: IdleClient", "Handling state: FromCache", ] ) assert response.extensions == snapshot( { "hishel_from_cache": True, "hishel_created_at": 1704067200.0, "hishel_revalidated": False, "hishel_stored": False, } ) @travel(datetime(2024, 1, 1, 0, 0, 0, tzinfo=ZoneInfo("UTC")), tick=False) def test_simple_caching_ignoring_spec(caplog: pytest.LogCaptureFixture) -> None: client = SyncCacheClient( storage=SyncSqliteStorage(connection=sqlite3.connect(":memory:")), policy=FilterPolicy(), ) with caplog.at_level("DEBUG", logger="hishel"): client.get("https://hishel.com", extensions={"hishel_spec_ignore": True}) response = client.get("https://hishel.com", extensions={"hishel_spec_ignore": True}) assert caplog.messages == snapshot( [ "Trying to get cached response ignoring specification", "Found 0 cached entries for the request", "Storing response in cache ignoring specification", "Trying to get cached response ignoring specification", "Found 1 cached entries for the request", "Found matching cached response for the request", ] ) assert response.extensions == snapshot( { "hishel_from_cache": True, "hishel_created_at": 1704067200.0, "hishel_revalidated": False, "hishel_stored": False, } ) def test_encoded_content_caching() -> None: data = gzip.compress(b"a" * 1000) compressed_data = ByteStream(data) mocked_responses = [ httpx.Response( 200, stream=compressed_data, headers={ "Content-Encoding": "gzip", "Content-Type": "text/plain", "Content-Length": str(len(data)), }, ) ] def handler(request: httpx.Request) -> httpx.Response: if not mocked_responses: raise RuntimeError("No more mocked responses available") return mocked_responses.pop(0) storage = SyncSqliteStorage(connection=sqlite3.connect(":memory:")) client = SyncCacheClient( transport=SyncCacheTransport( next_transport=MockTransport(handler=handler), storage=storage, policy=FilterPolicy() ), ) # First request - should fetch from the mocked transport and store in cache with client.stream("get", "https://localhost", extensions={"hishel_spec_ignore": True}) as response: response_data = b"".join([chunk for chunk in response.iter_raw()]) assert data == response_data assert response.headers.get("Content-Length") == str(len(data)) == str(len(response_data)) assert response.headers.get("Content-Encoding") == "gzip" # Second request - should fetch from cache with client.stream("get", "https://localhost", extensions={"hishel_spec_ignore": True}) as response: response_data = b"".join([chunk for chunk in response.iter_raw()]) assert data == response_data assert response.headers.get("Content-Length") == str(len(data)) == str(len(response_data)) assert response.headers.get("Content-Encoding") == "gzip" karpetrosyan-hishel-fd309b1/uv.lock000066400000000000000000021103011514113157400174240ustar00rootroot00000000000000version = 1 revision = 3 requires-python = ">=3.10" [options] resolution-mode = "lowest-direct" [[package]] name = "annotated-types" version = "0.7.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] [[package]] name = "anyio" version = "4.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "idna" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/16/ce/8a777047513153587e5434fd752e89334ac33e379aa3497db860eeb60377/anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0", size = 228266, upload-time = "2025-11-28T23:37:38.911Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362, upload-time = "2025-11-28T23:36:57.897Z" }, ] [[package]] name = "anysqlite" version = "0.0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0f/4b/cd5d66b9f87e773bc71344a368b9472987e33514e6627e28342b9c3e7c43/anysqlite-0.0.5.tar.gz", hash = "sha256:9dfcf87baf6b93426ad1d9118088c41dbf24ef01b445eea4a5d486bac2755cce", size = 3432, upload-time = "2023-10-02T13:49:25.135Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0b/31/349eae2bc9d9331dd8951684cf94528d91efaa71129dc30822ac111dfc66/anysqlite-0.0.5-py3-none-any.whl", hash = "sha256:cb345dc4f76f6b37f768d7a0b3e9cf5c700dfcb7a6356af8ab46a11f666edbe7", size = 3907, upload-time = "2023-10-02T13:49:26.943Z" }, ] [[package]] name = "asttokens" version = "3.0.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, ] [[package]] name = "attrs" version = "25.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, ] [[package]] name = "babel" version = "2.18.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, ] [[package]] name = "backports-tarfile" version = "1.2.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406, upload-time = "2024-05-28T17:01:54.731Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181, upload-time = "2024-05-28T17:01:53.112Z" }, ] [[package]] name = "backrefs" version = "6.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/86/e3/bb3a439d5cb255c4774724810ad8073830fac9c9dee123555820c1bcc806/backrefs-6.1.tar.gz", hash = "sha256:3bba1749aafe1db9b915f00e0dd166cba613b6f788ffd63060ac3485dc9be231", size = 7011962, upload-time = "2025-11-15T14:52:08.323Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3b/ee/c216d52f58ea75b5e1841022bbae24438b19834a29b163cb32aa3a2a7c6e/backrefs-6.1-py310-none-any.whl", hash = "sha256:2a2ccb96302337ce61ee4717ceacfbf26ba4efb1d55af86564b8bbaeda39cac1", size = 381059, upload-time = "2025-11-15T14:51:59.758Z" }, { url = "https://files.pythonhosted.org/packages/e6/9a/8da246d988ded941da96c7ed945d63e94a445637eaad985a0ed88787cb89/backrefs-6.1-py311-none-any.whl", hash = "sha256:e82bba3875ee4430f4de4b6db19429a27275d95a5f3773c57e9e18abc23fd2b7", size = 392854, upload-time = "2025-11-15T14:52:01.194Z" }, { url = "https://files.pythonhosted.org/packages/37/c9/fd117a6f9300c62bbc33bc337fd2b3c6bfe28b6e9701de336b52d7a797ad/backrefs-6.1-py312-none-any.whl", hash = "sha256:c64698c8d2269343d88947c0735cb4b78745bd3ba590e10313fbf3f78c34da5a", size = 398770, upload-time = "2025-11-15T14:52:02.584Z" }, { url = "https://files.pythonhosted.org/packages/eb/95/7118e935b0b0bd3f94dfec2d852fd4e4f4f9757bdb49850519acd245cd3a/backrefs-6.1-py313-none-any.whl", hash = "sha256:4c9d3dc1e2e558965202c012304f33d4e0e477e1c103663fd2c3cc9bb18b0d05", size = 400726, upload-time = "2025-11-15T14:52:04.093Z" }, { url = "https://files.pythonhosted.org/packages/1d/72/6296bad135bfafd3254ae3648cd152980a424bd6fed64a101af00cc7ba31/backrefs-6.1-py314-none-any.whl", hash = "sha256:13eafbc9ccd5222e9c1f0bec563e6d2a6d21514962f11e7fc79872fd56cbc853", size = 412584, upload-time = "2025-11-15T14:52:05.233Z" }, { url = "https://files.pythonhosted.org/packages/02/e3/a4fa1946722c4c7b063cc25043a12d9ce9b4323777f89643be74cef2993c/backrefs-6.1-py39-none-any.whl", hash = "sha256:a9e99b8a4867852cad177a6430e31b0f6e495d65f8c6c134b68c14c3c95bf4b0", size = 381058, upload-time = "2025-11-15T14:52:06.698Z" }, ] [[package]] name = "botocore-stubs" version = "1.42.41" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "types-awscrt" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0c/a8/a26608ff39e3a5866c6c79eda10133490205cbddd45074190becece3ff2a/botocore_stubs-1.42.41.tar.gz", hash = "sha256:dbeac2f744df6b814ce83ec3f3777b299a015cbea57a2efc41c33b8c38265825", size = 42411, upload-time = "2026-02-03T20:46:14.479Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/32/76/cab7af7f16c0b09347f2ebe7ffda7101132f786acb767666dce43055faab/botocore_stubs-1.42.41-py3-none-any.whl", hash = "sha256:9423110fb0e391834bd2ed44ae5f879d8cb370a444703d966d30842ce2bcb5f0", size = 66759, upload-time = "2026-02-03T20:46:13.02Z" }, ] [[package]] name = "certifi" version = "2026.1.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, ] [[package]] name = "cffi" version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycparser", marker = "implementation_name != 'PyPy'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, ] [[package]] name = "charset-normalizer" version = "3.4.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, ] [[package]] name = "click" version = "8.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] [[package]] name = "coverage" version = "7.10.7" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/51/26/d22c300112504f5f9a9fd2297ce33c35f3d353e4aeb987c8419453b2a7c2/coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239", size = 827704, upload-time = "2025-09-21T20:03:56.815Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e5/6c/3a3f7a46888e69d18abe3ccc6fe4cb16cccb1e6a2f99698931dafca489e6/coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a", size = 217987, upload-time = "2025-09-21T20:00:57.218Z" }, { url = "https://files.pythonhosted.org/packages/03/94/952d30f180b1a916c11a56f5c22d3535e943aa22430e9e3322447e520e1c/coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5", size = 218388, upload-time = "2025-09-21T20:01:00.081Z" }, { url = "https://files.pythonhosted.org/packages/50/2b/9e0cf8ded1e114bcd8b2fd42792b57f1c4e9e4ea1824cde2af93a67305be/coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17", size = 245148, upload-time = "2025-09-21T20:01:01.768Z" }, { url = "https://files.pythonhosted.org/packages/19/20/d0384ac06a6f908783d9b6aa6135e41b093971499ec488e47279f5b846e6/coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b", size = 246958, upload-time = "2025-09-21T20:01:03.355Z" }, { url = "https://files.pythonhosted.org/packages/60/83/5c283cff3d41285f8eab897651585db908a909c572bdc014bcfaf8a8b6ae/coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87", size = 248819, upload-time = "2025-09-21T20:01:04.968Z" }, { url = "https://files.pythonhosted.org/packages/60/22/02eb98fdc5ff79f423e990d877693e5310ae1eab6cb20ae0b0b9ac45b23b/coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e", size = 245754, upload-time = "2025-09-21T20:01:06.321Z" }, { url = "https://files.pythonhosted.org/packages/b4/bc/25c83bcf3ad141b32cd7dc45485ef3c01a776ca3aa8ef0a93e77e8b5bc43/coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e", size = 246860, upload-time = "2025-09-21T20:01:07.605Z" }, { url = "https://files.pythonhosted.org/packages/3c/b7/95574702888b58c0928a6e982038c596f9c34d52c5e5107f1eef729399b5/coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df", size = 244877, upload-time = "2025-09-21T20:01:08.829Z" }, { url = "https://files.pythonhosted.org/packages/47/b6/40095c185f235e085df0e0b158f6bd68cc6e1d80ba6c7721dc81d97ec318/coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0", size = 245108, upload-time = "2025-09-21T20:01:10.527Z" }, { url = "https://files.pythonhosted.org/packages/c8/50/4aea0556da7a4b93ec9168420d170b55e2eb50ae21b25062513d020c6861/coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13", size = 245752, upload-time = "2025-09-21T20:01:11.857Z" }, { url = "https://files.pythonhosted.org/packages/6a/28/ea1a84a60828177ae3b100cb6723838523369a44ec5742313ed7db3da160/coverage-7.10.7-cp310-cp310-win32.whl", hash = "sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b", size = 220497, upload-time = "2025-09-21T20:01:13.459Z" }, { url = "https://files.pythonhosted.org/packages/fc/1a/a81d46bbeb3c3fd97b9602ebaa411e076219a150489bcc2c025f151bd52d/coverage-7.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807", size = 221392, upload-time = "2025-09-21T20:01:14.722Z" }, { url = "https://files.pythonhosted.org/packages/d2/5d/c1a17867b0456f2e9ce2d8d4708a4c3a089947d0bec9c66cdf60c9e7739f/coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59", size = 218102, upload-time = "2025-09-21T20:01:16.089Z" }, { url = "https://files.pythonhosted.org/packages/54/f0/514dcf4b4e3698b9a9077f084429681bf3aad2b4a72578f89d7f643eb506/coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a", size = 218505, upload-time = "2025-09-21T20:01:17.788Z" }, { url = "https://files.pythonhosted.org/packages/20/f6/9626b81d17e2a4b25c63ac1b425ff307ecdeef03d67c9a147673ae40dc36/coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699", size = 248898, upload-time = "2025-09-21T20:01:19.488Z" }, { url = "https://files.pythonhosted.org/packages/b0/ef/bd8e719c2f7417ba03239052e099b76ea1130ac0cbb183ee1fcaa58aaff3/coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d", size = 250831, upload-time = "2025-09-21T20:01:20.817Z" }, { url = "https://files.pythonhosted.org/packages/a5/b6/bf054de41ec948b151ae2b79a55c107f5760979538f5fb80c195f2517718/coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e", size = 252937, upload-time = "2025-09-21T20:01:22.171Z" }, { url = "https://files.pythonhosted.org/packages/0f/e5/3860756aa6f9318227443c6ce4ed7bf9e70bb7f1447a0353f45ac5c7974b/coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23", size = 249021, upload-time = "2025-09-21T20:01:23.907Z" }, { url = "https://files.pythonhosted.org/packages/26/0f/bd08bd042854f7fd07b45808927ebcce99a7ed0f2f412d11629883517ac2/coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab", size = 250626, upload-time = "2025-09-21T20:01:25.721Z" }, { url = "https://files.pythonhosted.org/packages/8e/a7/4777b14de4abcc2e80c6b1d430f5d51eb18ed1d75fca56cbce5f2db9b36e/coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82", size = 248682, upload-time = "2025-09-21T20:01:27.105Z" }, { url = "https://files.pythonhosted.org/packages/34/72/17d082b00b53cd45679bad682fac058b87f011fd8b9fe31d77f5f8d3a4e4/coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2", size = 248402, upload-time = "2025-09-21T20:01:28.629Z" }, { url = "https://files.pythonhosted.org/packages/81/7a/92367572eb5bdd6a84bfa278cc7e97db192f9f45b28c94a9ca1a921c3577/coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61", size = 249320, upload-time = "2025-09-21T20:01:30.004Z" }, { url = "https://files.pythonhosted.org/packages/2f/88/a23cc185f6a805dfc4fdf14a94016835eeb85e22ac3a0e66d5e89acd6462/coverage-7.10.7-cp311-cp311-win32.whl", hash = "sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14", size = 220536, upload-time = "2025-09-21T20:01:32.184Z" }, { url = "https://files.pythonhosted.org/packages/fe/ef/0b510a399dfca17cec7bc2f05ad8bd78cf55f15c8bc9a73ab20c5c913c2e/coverage-7.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2", size = 221425, upload-time = "2025-09-21T20:01:33.557Z" }, { url = "https://files.pythonhosted.org/packages/51/7f/023657f301a276e4ba1850f82749bc136f5a7e8768060c2e5d9744a22951/coverage-7.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a", size = 220103, upload-time = "2025-09-21T20:01:34.929Z" }, { url = "https://files.pythonhosted.org/packages/13/e4/eb12450f71b542a53972d19117ea5a5cea1cab3ac9e31b0b5d498df1bd5a/coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417", size = 218290, upload-time = "2025-09-21T20:01:36.455Z" }, { url = "https://files.pythonhosted.org/packages/37/66/593f9be12fc19fb36711f19a5371af79a718537204d16ea1d36f16bd78d2/coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973", size = 218515, upload-time = "2025-09-21T20:01:37.982Z" }, { url = "https://files.pythonhosted.org/packages/66/80/4c49f7ae09cafdacc73fbc30949ffe77359635c168f4e9ff33c9ebb07838/coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c", size = 250020, upload-time = "2025-09-21T20:01:39.617Z" }, { url = "https://files.pythonhosted.org/packages/a6/90/a64aaacab3b37a17aaedd83e8000142561a29eb262cede42d94a67f7556b/coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7", size = 252769, upload-time = "2025-09-21T20:01:41.341Z" }, { url = "https://files.pythonhosted.org/packages/98/2e/2dda59afd6103b342e096f246ebc5f87a3363b5412609946c120f4e7750d/coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6", size = 253901, upload-time = "2025-09-21T20:01:43.042Z" }, { url = "https://files.pythonhosted.org/packages/53/dc/8d8119c9051d50f3119bb4a75f29f1e4a6ab9415cd1fa8bf22fcc3fb3b5f/coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59", size = 250413, upload-time = "2025-09-21T20:01:44.469Z" }, { url = "https://files.pythonhosted.org/packages/98/b3/edaff9c5d79ee4d4b6d3fe046f2b1d799850425695b789d491a64225d493/coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b", size = 251820, upload-time = "2025-09-21T20:01:45.915Z" }, { url = "https://files.pythonhosted.org/packages/11/25/9a0728564bb05863f7e513e5a594fe5ffef091b325437f5430e8cfb0d530/coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a", size = 249941, upload-time = "2025-09-21T20:01:47.296Z" }, { url = "https://files.pythonhosted.org/packages/e0/fd/ca2650443bfbef5b0e74373aac4df67b08180d2f184b482c41499668e258/coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb", size = 249519, upload-time = "2025-09-21T20:01:48.73Z" }, { url = "https://files.pythonhosted.org/packages/24/79/f692f125fb4299b6f963b0745124998ebb8e73ecdfce4ceceb06a8c6bec5/coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1", size = 251375, upload-time = "2025-09-21T20:01:50.529Z" }, { url = "https://files.pythonhosted.org/packages/5e/75/61b9bbd6c7d24d896bfeec57acba78e0f8deac68e6baf2d4804f7aae1f88/coverage-7.10.7-cp312-cp312-win32.whl", hash = "sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256", size = 220699, upload-time = "2025-09-21T20:01:51.941Z" }, { url = "https://files.pythonhosted.org/packages/ca/f3/3bf7905288b45b075918d372498f1cf845b5b579b723c8fd17168018d5f5/coverage-7.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba", size = 221512, upload-time = "2025-09-21T20:01:53.481Z" }, { url = "https://files.pythonhosted.org/packages/5c/44/3e32dbe933979d05cf2dac5e697c8599cfe038aaf51223ab901e208d5a62/coverage-7.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf", size = 220147, upload-time = "2025-09-21T20:01:55.2Z" }, { url = "https://files.pythonhosted.org/packages/9a/94/b765c1abcb613d103b64fcf10395f54d69b0ef8be6a0dd9c524384892cc7/coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d", size = 218320, upload-time = "2025-09-21T20:01:56.629Z" }, { url = "https://files.pythonhosted.org/packages/72/4f/732fff31c119bb73b35236dd333030f32c4bfe909f445b423e6c7594f9a2/coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b", size = 218575, upload-time = "2025-09-21T20:01:58.203Z" }, { url = "https://files.pythonhosted.org/packages/87/02/ae7e0af4b674be47566707777db1aa375474f02a1d64b9323e5813a6cdd5/coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e", size = 249568, upload-time = "2025-09-21T20:01:59.748Z" }, { url = "https://files.pythonhosted.org/packages/a2/77/8c6d22bf61921a59bce5471c2f1f7ac30cd4ac50aadde72b8c48d5727902/coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b", size = 252174, upload-time = "2025-09-21T20:02:01.192Z" }, { url = "https://files.pythonhosted.org/packages/b1/20/b6ea4f69bbb52dac0aebd62157ba6a9dddbfe664f5af8122dac296c3ee15/coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49", size = 253447, upload-time = "2025-09-21T20:02:02.701Z" }, { url = "https://files.pythonhosted.org/packages/f9/28/4831523ba483a7f90f7b259d2018fef02cb4d5b90bc7c1505d6e5a84883c/coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911", size = 249779, upload-time = "2025-09-21T20:02:04.185Z" }, { url = "https://files.pythonhosted.org/packages/a7/9f/4331142bc98c10ca6436d2d620c3e165f31e6c58d43479985afce6f3191c/coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0", size = 251604, upload-time = "2025-09-21T20:02:06.034Z" }, { url = "https://files.pythonhosted.org/packages/ce/60/bda83b96602036b77ecf34e6393a3836365481b69f7ed7079ab85048202b/coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f", size = 249497, upload-time = "2025-09-21T20:02:07.619Z" }, { url = "https://files.pythonhosted.org/packages/5f/af/152633ff35b2af63977edd835d8e6430f0caef27d171edf2fc76c270ef31/coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c", size = 249350, upload-time = "2025-09-21T20:02:10.34Z" }, { url = "https://files.pythonhosted.org/packages/9d/71/d92105d122bd21cebba877228990e1646d862e34a98bb3374d3fece5a794/coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f", size = 251111, upload-time = "2025-09-21T20:02:12.122Z" }, { url = "https://files.pythonhosted.org/packages/a2/9e/9fdb08f4bf476c912f0c3ca292e019aab6712c93c9344a1653986c3fd305/coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698", size = 220746, upload-time = "2025-09-21T20:02:13.919Z" }, { url = "https://files.pythonhosted.org/packages/b1/b1/a75fd25df44eab52d1931e89980d1ada46824c7a3210be0d3c88a44aaa99/coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843", size = 221541, upload-time = "2025-09-21T20:02:15.57Z" }, { url = "https://files.pythonhosted.org/packages/14/3a/d720d7c989562a6e9a14b2c9f5f2876bdb38e9367126d118495b89c99c37/coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546", size = 220170, upload-time = "2025-09-21T20:02:17.395Z" }, { url = "https://files.pythonhosted.org/packages/bb/22/e04514bf2a735d8b0add31d2b4ab636fc02370730787c576bb995390d2d5/coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c", size = 219029, upload-time = "2025-09-21T20:02:18.936Z" }, { url = "https://files.pythonhosted.org/packages/11/0b/91128e099035ece15da3445d9015e4b4153a6059403452d324cbb0a575fa/coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15", size = 219259, upload-time = "2025-09-21T20:02:20.44Z" }, { url = "https://files.pythonhosted.org/packages/8b/51/66420081e72801536a091a0c8f8c1f88a5c4bf7b9b1bdc6222c7afe6dc9b/coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4", size = 260592, upload-time = "2025-09-21T20:02:22.313Z" }, { url = "https://files.pythonhosted.org/packages/5d/22/9b8d458c2881b22df3db5bb3e7369e63d527d986decb6c11a591ba2364f7/coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0", size = 262768, upload-time = "2025-09-21T20:02:24.287Z" }, { url = "https://files.pythonhosted.org/packages/f7/08/16bee2c433e60913c610ea200b276e8eeef084b0d200bdcff69920bd5828/coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0", size = 264995, upload-time = "2025-09-21T20:02:26.133Z" }, { url = "https://files.pythonhosted.org/packages/20/9d/e53eb9771d154859b084b90201e5221bca7674ba449a17c101a5031d4054/coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65", size = 259546, upload-time = "2025-09-21T20:02:27.716Z" }, { url = "https://files.pythonhosted.org/packages/ad/b0/69bc7050f8d4e56a89fb550a1577d5d0d1db2278106f6f626464067b3817/coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541", size = 262544, upload-time = "2025-09-21T20:02:29.216Z" }, { url = "https://files.pythonhosted.org/packages/ef/4b/2514b060dbd1bc0aaf23b852c14bb5818f244c664cb16517feff6bb3a5ab/coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6", size = 260308, upload-time = "2025-09-21T20:02:31.226Z" }, { url = "https://files.pythonhosted.org/packages/54/78/7ba2175007c246d75e496f64c06e94122bdb914790a1285d627a918bd271/coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999", size = 258920, upload-time = "2025-09-21T20:02:32.823Z" }, { url = "https://files.pythonhosted.org/packages/c0/b3/fac9f7abbc841409b9a410309d73bfa6cfb2e51c3fada738cb607ce174f8/coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2", size = 261434, upload-time = "2025-09-21T20:02:34.86Z" }, { url = "https://files.pythonhosted.org/packages/ee/51/a03bec00d37faaa891b3ff7387192cef20f01604e5283a5fabc95346befa/coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a", size = 221403, upload-time = "2025-09-21T20:02:37.034Z" }, { url = "https://files.pythonhosted.org/packages/53/22/3cf25d614e64bf6d8e59c7c669b20d6d940bb337bdee5900b9ca41c820bb/coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb", size = 222469, upload-time = "2025-09-21T20:02:39.011Z" }, { url = "https://files.pythonhosted.org/packages/49/a1/00164f6d30d8a01c3c9c48418a7a5be394de5349b421b9ee019f380df2a0/coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb", size = 220731, upload-time = "2025-09-21T20:02:40.939Z" }, { url = "https://files.pythonhosted.org/packages/23/9c/5844ab4ca6a4dd97a1850e030a15ec7d292b5c5cb93082979225126e35dd/coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520", size = 218302, upload-time = "2025-09-21T20:02:42.527Z" }, { url = "https://files.pythonhosted.org/packages/f0/89/673f6514b0961d1f0e20ddc242e9342f6da21eaba3489901b565c0689f34/coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32", size = 218578, upload-time = "2025-09-21T20:02:44.468Z" }, { url = "https://files.pythonhosted.org/packages/05/e8/261cae479e85232828fb17ad536765c88dd818c8470aca690b0ac6feeaa3/coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f", size = 249629, upload-time = "2025-09-21T20:02:46.503Z" }, { url = "https://files.pythonhosted.org/packages/82/62/14ed6546d0207e6eda876434e3e8475a3e9adbe32110ce896c9e0c06bb9a/coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a", size = 252162, upload-time = "2025-09-21T20:02:48.689Z" }, { url = "https://files.pythonhosted.org/packages/ff/49/07f00db9ac6478e4358165a08fb41b469a1b053212e8a00cb02f0d27a05f/coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360", size = 253517, upload-time = "2025-09-21T20:02:50.31Z" }, { url = "https://files.pythonhosted.org/packages/a2/59/c5201c62dbf165dfbc91460f6dbbaa85a8b82cfa6131ac45d6c1bfb52deb/coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69", size = 249632, upload-time = "2025-09-21T20:02:51.971Z" }, { url = "https://files.pythonhosted.org/packages/07/ae/5920097195291a51fb00b3a70b9bbd2edbfe3c84876a1762bd1ef1565ebc/coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14", size = 251520, upload-time = "2025-09-21T20:02:53.858Z" }, { url = "https://files.pythonhosted.org/packages/b9/3c/a815dde77a2981f5743a60b63df31cb322c944843e57dbd579326625a413/coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe", size = 249455, upload-time = "2025-09-21T20:02:55.807Z" }, { url = "https://files.pythonhosted.org/packages/aa/99/f5cdd8421ea656abefb6c0ce92556709db2265c41e8f9fc6c8ae0f7824c9/coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e", size = 249287, upload-time = "2025-09-21T20:02:57.784Z" }, { url = "https://files.pythonhosted.org/packages/c3/7a/e9a2da6a1fc5d007dd51fca083a663ab930a8c4d149c087732a5dbaa0029/coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd", size = 250946, upload-time = "2025-09-21T20:02:59.431Z" }, { url = "https://files.pythonhosted.org/packages/ef/5b/0b5799aa30380a949005a353715095d6d1da81927d6dbed5def2200a4e25/coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2", size = 221009, upload-time = "2025-09-21T20:03:01.324Z" }, { url = "https://files.pythonhosted.org/packages/da/b0/e802fbb6eb746de006490abc9bb554b708918b6774b722bb3a0e6aa1b7de/coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681", size = 221804, upload-time = "2025-09-21T20:03:03.4Z" }, { url = "https://files.pythonhosted.org/packages/9e/e8/71d0c8e374e31f39e3389bb0bd19e527d46f00ea8571ec7ec8fd261d8b44/coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880", size = 220384, upload-time = "2025-09-21T20:03:05.111Z" }, { url = "https://files.pythonhosted.org/packages/62/09/9a5608d319fa3eba7a2019addeacb8c746fb50872b57a724c9f79f146969/coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63", size = 219047, upload-time = "2025-09-21T20:03:06.795Z" }, { url = "https://files.pythonhosted.org/packages/f5/6f/f58d46f33db9f2e3647b2d0764704548c184e6f5e014bef528b7f979ef84/coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2", size = 219266, upload-time = "2025-09-21T20:03:08.495Z" }, { url = "https://files.pythonhosted.org/packages/74/5c/183ffc817ba68e0b443b8c934c8795553eb0c14573813415bd59941ee165/coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d", size = 260767, upload-time = "2025-09-21T20:03:10.172Z" }, { url = "https://files.pythonhosted.org/packages/0f/48/71a8abe9c1ad7e97548835e3cc1adbf361e743e9d60310c5f75c9e7bf847/coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0", size = 262931, upload-time = "2025-09-21T20:03:11.861Z" }, { url = "https://files.pythonhosted.org/packages/84/fd/193a8fb132acfc0a901f72020e54be5e48021e1575bb327d8ee1097a28fd/coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699", size = 265186, upload-time = "2025-09-21T20:03:13.539Z" }, { url = "https://files.pythonhosted.org/packages/b1/8f/74ecc30607dd95ad50e3034221113ccb1c6d4e8085cc761134782995daae/coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9", size = 259470, upload-time = "2025-09-21T20:03:15.584Z" }, { url = "https://files.pythonhosted.org/packages/0f/55/79ff53a769f20d71b07023ea115c9167c0bb56f281320520cf64c5298a96/coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f", size = 262626, upload-time = "2025-09-21T20:03:17.673Z" }, { url = "https://files.pythonhosted.org/packages/88/e2/dac66c140009b61ac3fc13af673a574b00c16efdf04f9b5c740703e953c0/coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1", size = 260386, upload-time = "2025-09-21T20:03:19.36Z" }, { url = "https://files.pythonhosted.org/packages/a2/f1/f48f645e3f33bb9ca8a496bc4a9671b52f2f353146233ebd7c1df6160440/coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0", size = 258852, upload-time = "2025-09-21T20:03:21.007Z" }, { url = "https://files.pythonhosted.org/packages/bb/3b/8442618972c51a7affeead957995cfa8323c0c9bcf8fa5a027421f720ff4/coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399", size = 261534, upload-time = "2025-09-21T20:03:23.12Z" }, { url = "https://files.pythonhosted.org/packages/b2/dc/101f3fa3a45146db0cb03f5b4376e24c0aac818309da23e2de0c75295a91/coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235", size = 221784, upload-time = "2025-09-21T20:03:24.769Z" }, { url = "https://files.pythonhosted.org/packages/4c/a1/74c51803fc70a8a40d7346660379e144be772bab4ac7bb6e6b905152345c/coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d", size = 222905, upload-time = "2025-09-21T20:03:26.93Z" }, { url = "https://files.pythonhosted.org/packages/12/65/f116a6d2127df30bcafbceef0302d8a64ba87488bf6f73a6d8eebf060873/coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a", size = 220922, upload-time = "2025-09-21T20:03:28.672Z" }, { url = "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260", size = 209952, upload-time = "2025-09-21T20:03:53.918Z" }, ] [package.optional-dependencies] toml = [ { name = "tomli", marker = "python_full_version <= '3.11'" }, ] [[package]] name = "cryptography" version = "46.0.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/78/19/f748958276519adf6a0c1e79e7b8860b4830dda55ccdf29f2719b5fc499c/cryptography-46.0.4.tar.gz", hash = "sha256:bfd019f60f8abc2ed1b9be4ddc21cfef059c841d86d710bb69909a688cbb8f59", size = 749301, upload-time = "2026-01-28T00:24:37.379Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8d/99/157aae7949a5f30d51fcb1a9851e8ebd5c74bf99b5285d8bb4b8b9ee641e/cryptography-46.0.4-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:281526e865ed4166009e235afadf3a4c4cba6056f99336a99efba65336fd5485", size = 7173686, upload-time = "2026-01-28T00:23:07.515Z" }, { url = "https://files.pythonhosted.org/packages/87/91/874b8910903159043b5c6a123b7e79c4559ddd1896e38967567942635778/cryptography-46.0.4-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5f14fba5bf6f4390d7ff8f086c566454bff0411f6d8aa7af79c88b6f9267aecc", size = 4275871, upload-time = "2026-01-28T00:23:09.439Z" }, { url = "https://files.pythonhosted.org/packages/c0/35/690e809be77896111f5b195ede56e4b4ed0435b428c2f2b6d35046fbb5e8/cryptography-46.0.4-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47bcd19517e6389132f76e2d5303ded6cf3f78903da2158a671be8de024f4cd0", size = 4423124, upload-time = "2026-01-28T00:23:11.529Z" }, { url = "https://files.pythonhosted.org/packages/1a/5b/a26407d4f79d61ca4bebaa9213feafdd8806dc69d3d290ce24996d3cfe43/cryptography-46.0.4-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:01df4f50f314fbe7009f54046e908d1754f19d0c6d3070df1e6268c5a4af09fa", size = 4277090, upload-time = "2026-01-28T00:23:13.123Z" }, { url = "https://files.pythonhosted.org/packages/0c/d8/4bb7aec442a9049827aa34cee1aa83803e528fa55da9a9d45d01d1bb933e/cryptography-46.0.4-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5aa3e463596b0087b3da0dbe2b2487e9fc261d25da85754e30e3b40637d61f81", size = 4947652, upload-time = "2026-01-28T00:23:14.554Z" }, { url = "https://files.pythonhosted.org/packages/2b/08/f83e2e0814248b844265802d081f2fac2f1cbe6cd258e72ba14ff006823a/cryptography-46.0.4-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0a9ad24359fee86f131836a9ac3bffc9329e956624a2d379b613f8f8abaf5255", size = 4455157, upload-time = "2026-01-28T00:23:16.443Z" }, { url = "https://files.pythonhosted.org/packages/0a/05/19d849cf4096448779d2dcc9bb27d097457dac36f7273ffa875a93b5884c/cryptography-46.0.4-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:dc1272e25ef673efe72f2096e92ae39dea1a1a450dd44918b15351f72c5a168e", size = 3981078, upload-time = "2026-01-28T00:23:17.838Z" }, { url = "https://files.pythonhosted.org/packages/e6/89/f7bac81d66ba7cde867a743ea5b37537b32b5c633c473002b26a226f703f/cryptography-46.0.4-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:de0f5f4ec8711ebc555f54735d4c673fc34b65c44283895f1a08c2b49d2fd99c", size = 4276213, upload-time = "2026-01-28T00:23:19.257Z" }, { url = "https://files.pythonhosted.org/packages/da/9f/7133e41f24edd827020ad21b068736e792bc68eecf66d93c924ad4719fb3/cryptography-46.0.4-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:eeeb2e33d8dbcccc34d64651f00a98cb41b2dc69cef866771a5717e6734dfa32", size = 4912190, upload-time = "2026-01-28T00:23:21.244Z" }, { url = "https://files.pythonhosted.org/packages/a6/f7/6d43cbaddf6f65b24816e4af187d211f0bc536a29961f69faedc48501d8e/cryptography-46.0.4-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3d425eacbc9aceafd2cb429e42f4e5d5633c6f873f5e567077043ef1b9bbf616", size = 4454641, upload-time = "2026-01-28T00:23:22.866Z" }, { url = "https://files.pythonhosted.org/packages/9e/4f/ebd0473ad656a0ac912a16bd07db0f5d85184924e14fc88feecae2492834/cryptography-46.0.4-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91627ebf691d1ea3976a031b61fb7bac1ccd745afa03602275dda443e11c8de0", size = 4405159, upload-time = "2026-01-28T00:23:25.278Z" }, { url = "https://files.pythonhosted.org/packages/d1/f7/7923886f32dc47e27adeff8246e976d77258fd2aa3efdd1754e4e323bf49/cryptography-46.0.4-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2d08bc22efd73e8854b0b7caff402d735b354862f1145d7be3b9c0f740fef6a0", size = 4666059, upload-time = "2026-01-28T00:23:26.766Z" }, { url = "https://files.pythonhosted.org/packages/eb/a7/0fca0fd3591dffc297278a61813d7f661a14243dd60f499a7a5b48acb52a/cryptography-46.0.4-cp311-abi3-win32.whl", hash = "sha256:82a62483daf20b8134f6e92898da70d04d0ef9a75829d732ea1018678185f4f5", size = 3026378, upload-time = "2026-01-28T00:23:28.317Z" }, { url = "https://files.pythonhosted.org/packages/2d/12/652c84b6f9873f0909374864a57b003686c642ea48c84d6c7e2c515e6da5/cryptography-46.0.4-cp311-abi3-win_amd64.whl", hash = "sha256:6225d3ebe26a55dbc8ead5ad1265c0403552a63336499564675b29eb3184c09b", size = 3478614, upload-time = "2026-01-28T00:23:30.275Z" }, { url = "https://files.pythonhosted.org/packages/b9/27/542b029f293a5cce59349d799d4d8484b3b1654a7b9a0585c266e974a488/cryptography-46.0.4-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:485e2b65d25ec0d901bca7bcae0f53b00133bf3173916d8e421f6fddde103908", size = 7116417, upload-time = "2026-01-28T00:23:31.958Z" }, { url = "https://files.pythonhosted.org/packages/f8/f5/559c25b77f40b6bf828eabaf988efb8b0e17b573545edb503368ca0a2a03/cryptography-46.0.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:078e5f06bd2fa5aea5a324f2a09f914b1484f1d0c2a4d6a8a28c74e72f65f2da", size = 4264508, upload-time = "2026-01-28T00:23:34.264Z" }, { url = "https://files.pythonhosted.org/packages/49/a1/551fa162d33074b660dc35c9bc3616fefa21a0e8c1edd27b92559902e408/cryptography-46.0.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dce1e4f068f03008da7fa51cc7abc6ddc5e5de3e3d1550334eaf8393982a5829", size = 4409080, upload-time = "2026-01-28T00:23:35.793Z" }, { url = "https://files.pythonhosted.org/packages/b0/6a/4d8d129a755f5d6df1bbee69ea2f35ebfa954fa1847690d1db2e8bca46a5/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:2067461c80271f422ee7bdbe79b9b4be54a5162e90345f86a23445a0cf3fd8a2", size = 4270039, upload-time = "2026-01-28T00:23:37.263Z" }, { url = "https://files.pythonhosted.org/packages/4c/f5/ed3fcddd0a5e39321e595e144615399e47e7c153a1fb8c4862aec3151ff9/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:c92010b58a51196a5f41c3795190203ac52edfd5dc3ff99149b4659eba9d2085", size = 4926748, upload-time = "2026-01-28T00:23:38.884Z" }, { url = "https://files.pythonhosted.org/packages/43/ae/9f03d5f0c0c00e85ecb34f06d3b79599f20630e4db91b8a6e56e8f83d410/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:829c2b12bbc5428ab02d6b7f7e9bbfd53e33efd6672d21341f2177470171ad8b", size = 4442307, upload-time = "2026-01-28T00:23:40.56Z" }, { url = "https://files.pythonhosted.org/packages/8b/22/e0f9f2dae8040695103369cf2283ef9ac8abe4d51f68710bec2afd232609/cryptography-46.0.4-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:62217ba44bf81b30abaeda1488686a04a702a261e26f87db51ff61d9d3510abd", size = 3959253, upload-time = "2026-01-28T00:23:42.827Z" }, { url = "https://files.pythonhosted.org/packages/01/5b/6a43fcccc51dae4d101ac7d378a8724d1ba3de628a24e11bf2f4f43cba4d/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:9c2da296c8d3415b93e6053f5a728649a87a48ce084a9aaf51d6e46c87c7f2d2", size = 4269372, upload-time = "2026-01-28T00:23:44.655Z" }, { url = "https://files.pythonhosted.org/packages/17/b7/0f6b8c1dd0779df2b526e78978ff00462355e31c0a6f6cff8a3e99889c90/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9b34d8ba84454641a6bf4d6762d15847ecbd85c1316c0a7984e6e4e9f748ec2e", size = 4891908, upload-time = "2026-01-28T00:23:46.48Z" }, { url = "https://files.pythonhosted.org/packages/83/17/259409b8349aa10535358807a472c6a695cf84f106022268d31cea2b6c97/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:df4a817fa7138dd0c96c8c8c20f04b8aaa1fac3bbf610913dcad8ea82e1bfd3f", size = 4441254, upload-time = "2026-01-28T00:23:48.403Z" }, { url = "https://files.pythonhosted.org/packages/9c/fe/e4a1b0c989b00cee5ffa0764401767e2d1cf59f45530963b894129fd5dce/cryptography-46.0.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b1de0ebf7587f28f9190b9cb526e901bf448c9e6a99655d2b07fff60e8212a82", size = 4396520, upload-time = "2026-01-28T00:23:50.26Z" }, { url = "https://files.pythonhosted.org/packages/b3/81/ba8fd9657d27076eb40d6a2f941b23429a3c3d2f56f5a921d6b936a27bc9/cryptography-46.0.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9b4d17bc7bd7cdd98e3af40b441feaea4c68225e2eb2341026c84511ad246c0c", size = 4651479, upload-time = "2026-01-28T00:23:51.674Z" }, { url = "https://files.pythonhosted.org/packages/00/03/0de4ed43c71c31e4fe954edd50b9d28d658fef56555eba7641696370a8e2/cryptography-46.0.4-cp314-cp314t-win32.whl", hash = "sha256:c411f16275b0dea722d76544a61d6421e2cc829ad76eec79280dbdc9ddf50061", size = 3001986, upload-time = "2026-01-28T00:23:53.485Z" }, { url = "https://files.pythonhosted.org/packages/5c/70/81830b59df7682917d7a10f833c4dab2a5574cd664e86d18139f2b421329/cryptography-46.0.4-cp314-cp314t-win_amd64.whl", hash = "sha256:728fedc529efc1439eb6107b677f7f7558adab4553ef8669f0d02d42d7b959a7", size = 3468288, upload-time = "2026-01-28T00:23:55.09Z" }, { url = "https://files.pythonhosted.org/packages/56/f7/f648fdbb61d0d45902d3f374217451385edc7e7768d1b03ff1d0e5ffc17b/cryptography-46.0.4-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a9556ba711f7c23f77b151d5798f3ac44a13455cc68db7697a1096e6d0563cab", size = 7169583, upload-time = "2026-01-28T00:23:56.558Z" }, { url = "https://files.pythonhosted.org/packages/d8/cc/8f3224cbb2a928de7298d6ed4790f5ebc48114e02bdc9559196bfb12435d/cryptography-46.0.4-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8bf75b0259e87fa70bddc0b8b4078b76e7fd512fd9afae6c1193bcf440a4dbef", size = 4275419, upload-time = "2026-01-28T00:23:58.364Z" }, { url = "https://files.pythonhosted.org/packages/17/43/4a18faa7a872d00e4264855134ba82d23546c850a70ff209e04ee200e76f/cryptography-46.0.4-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3c268a3490df22270955966ba236d6bc4a8f9b6e4ffddb78aac535f1a5ea471d", size = 4419058, upload-time = "2026-01-28T00:23:59.867Z" }, { url = "https://files.pythonhosted.org/packages/ee/64/6651969409821d791ba12346a124f55e1b76f66a819254ae840a965d4b9c/cryptography-46.0.4-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:812815182f6a0c1d49a37893a303b44eaac827d7f0d582cecfc81b6427f22973", size = 4278151, upload-time = "2026-01-28T00:24:01.731Z" }, { url = "https://files.pythonhosted.org/packages/20/0b/a7fce65ee08c3c02f7a8310cc090a732344066b990ac63a9dfd0a655d321/cryptography-46.0.4-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:a90e43e3ef65e6dcf969dfe3bb40cbf5aef0d523dff95bfa24256be172a845f4", size = 4939441, upload-time = "2026-01-28T00:24:03.175Z" }, { url = "https://files.pythonhosted.org/packages/db/a7/20c5701e2cd3e1dfd7a19d2290c522a5f435dd30957d431dcb531d0f1413/cryptography-46.0.4-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a05177ff6296644ef2876fce50518dffb5bcdf903c85250974fc8bc85d54c0af", size = 4451617, upload-time = "2026-01-28T00:24:05.403Z" }, { url = "https://files.pythonhosted.org/packages/00/dc/3e16030ea9aa47b63af6524c354933b4fb0e352257c792c4deeb0edae367/cryptography-46.0.4-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:daa392191f626d50f1b136c9b4cf08af69ca8279d110ea24f5c2700054d2e263", size = 3977774, upload-time = "2026-01-28T00:24:06.851Z" }, { url = "https://files.pythonhosted.org/packages/42/c8/ad93f14118252717b465880368721c963975ac4b941b7ef88f3c56bf2897/cryptography-46.0.4-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e07ea39c5b048e085f15923511d8121e4a9dc45cee4e3b970ca4f0d338f23095", size = 4277008, upload-time = "2026-01-28T00:24:08.926Z" }, { url = "https://files.pythonhosted.org/packages/00/cf/89c99698151c00a4631fbfcfcf459d308213ac29e321b0ff44ceeeac82f1/cryptography-46.0.4-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d5a45ddc256f492ce42a4e35879c5e5528c09cd9ad12420828c972951d8e016b", size = 4903339, upload-time = "2026-01-28T00:24:12.009Z" }, { url = "https://files.pythonhosted.org/packages/03/c3/c90a2cb358de4ac9309b26acf49b2a100957e1ff5cc1e98e6c4996576710/cryptography-46.0.4-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:6bb5157bf6a350e5b28aee23beb2d84ae6f5be390b2f8ee7ea179cda077e1019", size = 4451216, upload-time = "2026-01-28T00:24:13.975Z" }, { url = "https://files.pythonhosted.org/packages/96/2c/8d7f4171388a10208671e181ca43cdc0e596d8259ebacbbcfbd16de593da/cryptography-46.0.4-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dd5aba870a2c40f87a3af043e0dee7d9eb02d4aff88a797b48f2b43eff8c3ab4", size = 4404299, upload-time = "2026-01-28T00:24:16.169Z" }, { url = "https://files.pythonhosted.org/packages/e9/23/cbb2036e450980f65c6e0a173b73a56ff3bccd8998965dea5cc9ddd424a5/cryptography-46.0.4-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:93d8291da8d71024379ab2cb0b5c57915300155ad42e07f76bea6ad838d7e59b", size = 4664837, upload-time = "2026-01-28T00:24:17.629Z" }, { url = "https://files.pythonhosted.org/packages/0a/21/f7433d18fe6d5845329cbdc597e30caf983229c7a245bcf54afecc555938/cryptography-46.0.4-cp38-abi3-win32.whl", hash = "sha256:0563655cb3c6d05fb2afe693340bc050c30f9f34e15763361cf08e94749401fc", size = 3009779, upload-time = "2026-01-28T00:24:20.198Z" }, { url = "https://files.pythonhosted.org/packages/3a/6a/bd2e7caa2facffedf172a45c1a02e551e6d7d4828658c9a245516a598d94/cryptography-46.0.4-cp38-abi3-win_amd64.whl", hash = "sha256:fa0900b9ef9c49728887d1576fd8d9e7e3ea872fa9b25ef9b64888adc434e976", size = 3466633, upload-time = "2026-01-28T00:24:21.851Z" }, { url = "https://files.pythonhosted.org/packages/59/e0/f9c6c53e1f2a1c2507f00f2faba00f01d2f334b35b0fbfe5286715da2184/cryptography-46.0.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:766330cce7416c92b5e90c3bb71b1b79521760cdcfc3a6a1a182d4c9fab23d2b", size = 3476316, upload-time = "2026-01-28T00:24:24.144Z" }, { url = "https://files.pythonhosted.org/packages/27/7a/f8d2d13227a9a1a9fe9c7442b057efecffa41f1e3c51d8622f26b9edbe8f/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c236a44acfb610e70f6b3e1c3ca20ff24459659231ef2f8c48e879e2d32b73da", size = 4216693, upload-time = "2026-01-28T00:24:25.758Z" }, { url = "https://files.pythonhosted.org/packages/c5/de/3787054e8f7972658370198753835d9d680f6cd4a39df9f877b57f0dd69c/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8a15fb869670efa8f83cbffbc8753c1abf236883225aed74cd179b720ac9ec80", size = 4382765, upload-time = "2026-01-28T00:24:27.577Z" }, { url = "https://files.pythonhosted.org/packages/8a/5f/60e0afb019973ba6a0b322e86b3d61edf487a4f5597618a430a2a15f2d22/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:fdc3daab53b212472f1524d070735b2f0c214239df131903bae1d598016fa822", size = 4216066, upload-time = "2026-01-28T00:24:29.056Z" }, { url = "https://files.pythonhosted.org/packages/81/8e/bf4a0de294f147fee66f879d9bae6f8e8d61515558e3d12785dd90eca0be/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:44cc0675b27cadb71bdbb96099cca1fa051cd11d2ade09e5cd3a2edb929ed947", size = 4382025, upload-time = "2026-01-28T00:24:30.681Z" }, { url = "https://files.pythonhosted.org/packages/79/f4/9ceb90cfd6a3847069b0b0b353fd3075dc69b49defc70182d8af0c4ca390/cryptography-46.0.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be8c01a7d5a55f9a47d1888162b76c8f49d62b234d88f0ff91a9fbebe32ffbc3", size = 3406043, upload-time = "2026-01-28T00:24:32.236Z" }, ] [[package]] name = "distlib" version = "0.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, ] [[package]] name = "dnspython" version = "2.8.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, ] [[package]] name = "email-validator" version = "2.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "dnspython" }, { name = "idna" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, ] [[package]] name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, ] [[package]] name = "executing" version = "2.2.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, ] [[package]] name = "fastapi" version = "0.119.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "starlette" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a6/f4/152127681182e6413e7a89684c434e19e7414ed7ac0c632999c3c6980640/fastapi-0.119.1.tar.gz", hash = "sha256:a5e3426edce3fe221af4e1992c6d79011b247e3b03cc57999d697fe76cbf8ae0", size = 338616, upload-time = "2025-10-20T11:30:27.734Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b1/26/e6d959b4ac959fdb3e9c4154656fc160794db6af8e64673d52759456bf07/fastapi-0.119.1-py3-none-any.whl", hash = "sha256:0b8c2a2cce853216e150e9bd4faaed88227f8eb37de21cb200771f491586a27f", size = 108123, upload-time = "2025-10-20T11:30:26.185Z" }, ] [package.optional-dependencies] standard = [ { name = "email-validator" }, { name = "fastapi-cli", extra = ["standard"] }, { name = "httpx" }, { name = "jinja2" }, { name = "python-multipart" }, { name = "uvicorn", extra = ["standard"] }, ] [[package]] name = "fastapi-cli" version = "0.0.20" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "rich-toolkit" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typer" }, { name = "uvicorn", extra = ["standard"] }, ] sdist = { url = "https://files.pythonhosted.org/packages/d3/ca/d90fb3bfbcbd6e56c77afd9d114dd6ce8955d8bb90094399d1c70e659e40/fastapi_cli-0.0.20.tar.gz", hash = "sha256:d17c2634f7b96b6b560bc16b0035ed047d523c912011395f49f00a421692bc3a", size = 19786, upload-time = "2025-12-22T17:13:33.794Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/08/89/5c4eef60524d0fd704eb0706885b82cd5623a43396b94e4a5b17d3a3f516/fastapi_cli-0.0.20-py3-none-any.whl", hash = "sha256:e58b6a0038c0b1532b7a0af690656093dee666201b6b19d3c87175b358e9f783", size = 12390, upload-time = "2025-12-22T17:13:31.708Z" }, ] [package.optional-dependencies] standard = [ { name = "fastapi-cloud-cli" }, { name = "uvicorn", extra = ["standard"] }, ] [[package]] name = "fastapi-cloud-cli" version = "0.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "fastar" }, { name = "httpx" }, { name = "pydantic", extra = ["email"] }, { name = "rich-toolkit" }, { name = "rignore" }, { name = "sentry-sdk" }, { name = "typer" }, { name = "uvicorn", extra = ["standard"] }, ] sdist = { url = "https://files.pythonhosted.org/packages/11/15/6c3d85d63964340fde6f36cc80f3f365d35f371e6a918d68ff3a3d588ef2/fastapi_cloud_cli-0.11.0.tar.gz", hash = "sha256:ecc83a5db106be35af528eccb01aa9bced1d29783efd48c8c1c831cf111eea99", size = 36170, upload-time = "2026-01-15T09:51:33.681Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1a/07/60f79270a3320780be7e2ae8a1740cb98a692920b569ba420b97bcc6e175/fastapi_cloud_cli-0.11.0-py3-none-any.whl", hash = "sha256:76857b0f09d918acfcb50ade34682ba3b2079ca0c43fda10215de301f185a7f8", size = 26884, upload-time = "2026-01-15T09:51:34.471Z" }, ] [[package]] name = "fastar" version = "0.8.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/69/e7/f89d54fb04104114dd0552836dc2b47914f416cc0e200b409dd04a33de5e/fastar-0.8.0.tar.gz", hash = "sha256:f4d4d68dbf1c4c2808f0e730fac5843493fc849f70fe3ad3af60dfbaf68b9a12", size = 68524, upload-time = "2025-11-26T02:36:00.72Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c3/e2/51d9ee443aabcd5aa581d45b18b6198ced364b5cd97e5504c5d782ceb82c/fastar-0.8.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c9f930cff014cf79d396d0541bd9f3a3f170c9b5e45d10d634d98f9ed08788c3", size = 708536, upload-time = "2025-11-26T02:34:35.236Z" }, { url = "https://files.pythonhosted.org/packages/07/2a/edfc6274768b8a3859a5ca4f8c29cb7f614d7f27d2378e2c88aa91cda54e/fastar-0.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07b70f712d20622346531a4b46bb332569bea621f61314c0b7e80903a16d14cf", size = 632235, upload-time = "2025-11-26T02:34:19.367Z" }, { url = "https://files.pythonhosted.org/packages/ef/1e/3cfbaaec464caef196700ee2ffae1c03f94f7c5e2a85d0ec0ea9cdd1da81/fastar-0.8.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:330639db3bfba4c6d132421a2a4aeb81e7bea8ce9159cdb6e247fbc5fae97686", size = 871386, upload-time = "2025-11-26T02:33:47.613Z" }, { url = "https://files.pythonhosted.org/packages/82/50/224a674ad541054179e4e6e0b54bb6e162f04f698a2512b42a8085fc6b6f/fastar-0.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98ea7ceb6231e48d7bb0d7dc13e946baa29c7f6873eaf4afb69725d6da349033", size = 764955, upload-time = "2025-11-26T02:32:44.279Z" }, { url = "https://files.pythonhosted.org/packages/4d/5e/4608184aa57cb6a54f62c1eb3e5133ba8d461fc7f13193c0255effbec12a/fastar-0.8.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a90695a601a78bbca910fdf2efcdf3103c55d0de5a5c6e93556d707bf886250b", size = 765987, upload-time = "2025-11-26T02:32:59.701Z" }, { url = "https://files.pythonhosted.org/packages/e0/53/6afd2b680dddfa10df9a16bbcf6cabfee0d92435d5c7e3f4cfe3b1712662/fastar-0.8.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d0bf655ff4c9320b0ca8a5b128063d5093c0c8c1645a2b5f7167143fd8531aa", size = 930900, upload-time = "2025-11-26T02:33:16.059Z" }, { url = "https://files.pythonhosted.org/packages/ef/1e/b7a304bfcc1d06845cbfa4b464516f6fff9c8c6692f6ef80a3a86b04e199/fastar-0.8.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d8df22cdd8d58e7689aa89b2e4a07e8e5fa4f88d2d9c2621f0e88a49be97ccea", size = 821523, upload-time = "2025-11-26T02:33:30.897Z" }, { url = "https://files.pythonhosted.org/packages/1d/da/9ef8605c6d233cd6ca3a95f7f518ac22aa064903afe6afa57733bfb7c31b/fastar-0.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8a5e6ad722685128521c8fb44cf25bd38669650ba3a4b466b8903e5aa28e1a0", size = 821268, upload-time = "2025-11-26T02:34:04.003Z" }, { url = "https://files.pythonhosted.org/packages/7e/22/ed37c78a6b4420de1677d82e79742787975c34847229c33dc376334c7283/fastar-0.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:31cd541231a2456e32104da891cf9962c3b40234d0465cbf9322a6bc8a1b05d5", size = 986286, upload-time = "2025-11-26T02:34:50.279Z" }, { url = "https://files.pythonhosted.org/packages/ca/a6/366b15f432d85d4089e6e4b52a09cc2a2bcf4d7a1f0771e3d3194deccb1e/fastar-0.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:175db2a98d67ced106468e8987975484f8bbbd5ad99201da823b38bafb565ed5", size = 1041921, upload-time = "2025-11-26T02:35:07.292Z" }, { url = "https://files.pythonhosted.org/packages/f4/45/45f8e6991e3ce9f8aeefdc8d4c200daada41097a36808643d1703464c3e2/fastar-0.8.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ada877ab1c65197d772ce1b1c2e244d4799680d8b3f136a4308360f3d8661b23", size = 1047302, upload-time = "2025-11-26T02:35:24.995Z" }, { url = "https://files.pythonhosted.org/packages/c2/e2/a587796111a3cd4b78cd61ec3fc1252d8517d81f763f4164ed5680f84810/fastar-0.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:01084cb75f13ca6a8e80bd41584322523189f8e81b472053743d6e6c3062b5a6", size = 995141, upload-time = "2025-11-26T02:35:42.449Z" }, { url = "https://files.pythonhosted.org/packages/89/c0/7a8ec86695b0b77168e220cf2af1aa30592f5ecdbd0ce6d641d29c4a8bae/fastar-0.8.0-cp310-cp310-win32.whl", hash = "sha256:ca639b9909805e44364ea13cca2682b487e74826e4ad75957115ec693228d6b6", size = 456544, upload-time = "2025-11-26T02:36:23.801Z" }, { url = "https://files.pythonhosted.org/packages/be/a9/8da4deb840121c59deabd939ce2dca3d6beec85576f3743d1144441938b5/fastar-0.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:fbc0f2ed0f4add7fb58034c576584d44d7eaaf93dee721dfb26dbed6e222dbac", size = 490701, upload-time = "2025-11-26T02:36:09.625Z" }, { url = "https://files.pythonhosted.org/packages/cd/15/1c764530b81b266f6d27d78d49b6bef22a73b3300cd83a280bfd244908c5/fastar-0.8.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:cd9c0d3ebf7a0a6f642f771cf41b79f7c98d40a3072a8abe1174fbd9bd615bd3", size = 708427, upload-time = "2025-11-26T02:34:36.502Z" }, { url = "https://files.pythonhosted.org/packages/41/fc/75d42c008516543219e4293e4d8ac55da57a5c63147484f10468bd1bc24e/fastar-0.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2875a077340fe4f8099bd3ed8fa90d9595e1ac3cd62ae19ab690d5bf550eeb35", size = 631740, upload-time = "2025-11-26T02:34:20.718Z" }, { url = "https://files.pythonhosted.org/packages/50/8d/9632984f7824ed2210157dcebd8e9821ef6d4f2b28510d0516db6625ff9b/fastar-0.8.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a999263d9f87184bf2801833b2ecf105e03c0dd91cac78685673b70da564fd64", size = 871628, upload-time = "2025-11-26T02:33:49.279Z" }, { url = "https://files.pythonhosted.org/packages/05/97/3eb6ea71b7544d45cd29cacb764ca23cde8ce0aed1a6a02251caa4c0a818/fastar-0.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c41111da56430f638cbfc498ebdcc7d30f63416e904b27b7695c29bd4889cb8", size = 765005, upload-time = "2025-11-26T02:32:45.833Z" }, { url = "https://files.pythonhosted.org/packages/d6/45/3eb0ee945a0b5d5f9df7e7c25c037ce7fa441cd0b4d44f76d286e2f4396a/fastar-0.8.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3719541a12bb09ab1eae91d2c987a9b2b7d7149c52e7109ba6e15b74aabc49b1", size = 765587, upload-time = "2025-11-26T02:33:01.174Z" }, { url = "https://files.pythonhosted.org/packages/51/bb/7defd6ec0d9570b1987d8ebde52d07d97f3f26e10b592fb3e12738eba39a/fastar-0.8.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7a9b0fff8079b18acdface7ef1b7f522fd9a589f65ca4a1a0dd7c92a0886c2a2", size = 931150, upload-time = "2025-11-26T02:33:17.374Z" }, { url = "https://files.pythonhosted.org/packages/28/54/62e51e684dab347c61878afbf09e177029c1a91eb1e39ef244e6b3ef9efa/fastar-0.8.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ac073576c1931959191cb20df38bab21dd152f66c940aa3ca8b22e39f753b2f3", size = 821354, upload-time = "2025-11-26T02:33:32.083Z" }, { url = "https://files.pythonhosted.org/packages/53/a8/12708ea4d21e3cf9f485b2a67d44ce84d949a6eddcc9aa5b3d324585ab43/fastar-0.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:003b59a7c3e405b6a7bff8fab17d31e0ccbc7f06730a8f8ca1694eeea75f3c76", size = 821626, upload-time = "2025-11-26T02:34:05.685Z" }, { url = "https://files.pythonhosted.org/packages/e7/c4/1b4d3347c7a759853f963410bf6baf42fe014d587c50c39c8e145f4bf1a0/fastar-0.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a7b96748425efd9fc155cd920d65088a1b0d754421962418ea73413d02ff515a", size = 986187, upload-time = "2025-11-26T02:34:52.047Z" }, { url = "https://files.pythonhosted.org/packages/dc/59/2dbe0dc2570764475e60030403738faa261a9d3bff16b08629c378ab939a/fastar-0.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:90957a30e64418b02df5b4d525bea50403d98a4b1f29143ce5914ddfa7e54ee4", size = 1041536, upload-time = "2025-11-26T02:35:08.926Z" }, { url = "https://files.pythonhosted.org/packages/d9/0f/639b295669c7ca6fbc2b4be2a7832aaeac1a5e06923f15a8a6d6daecbc7d/fastar-0.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f6e784a8015623fbb7ccca1af372fd82cb511b408ddd2348dc929fc6e415df73", size = 1047149, upload-time = "2025-11-26T02:35:26.597Z" }, { url = "https://files.pythonhosted.org/packages/cb/e7/23e3a19e06d261d1894f98eca9458f98c090c505a0c712dafc0ff1fc2965/fastar-0.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a03eaf287bbc93064688a1220580ce261e7557c8898f687f4d0b281c85b28d3c", size = 994992, upload-time = "2025-11-26T02:35:44.009Z" }, { url = "https://files.pythonhosted.org/packages/f2/7a/3ea4726bae3ac9358d02107ae48f3e10ee186dbed554af79e00b7b498c44/fastar-0.8.0-cp311-cp311-win32.whl", hash = "sha256:661a47ed90762f419406c47e802f46af63a08254ba96abd1c8191e4ce967b665", size = 456449, upload-time = "2025-11-26T02:36:25.291Z" }, { url = "https://files.pythonhosted.org/packages/cb/3c/0142bee993c431ee91cf5535e6e4b079ad491f620c215fcd79b7e5ffeb2b/fastar-0.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:b48abd6056fef7bc3d414aafb453c5b07fdf06d2df5a2841d650288a3aa1e9d3", size = 490863, upload-time = "2025-11-26T02:36:11.114Z" }, { url = "https://files.pythonhosted.org/packages/3b/18/d119944f6bdbf6e722e204e36db86390ea45684a1bf6be6e3aa42abd471f/fastar-0.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:50c18788b3c6ffb85e176dcb8548bb8e54616a0519dcdbbfba66f6bbc4316933", size = 462230, upload-time = "2025-11-26T02:36:01.917Z" }, { url = "https://files.pythonhosted.org/packages/58/f1/5b2ff898abac7f1a418284aad285e3a4f68d189c572ab2db0f6c9079dd16/fastar-0.8.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f10d2adfe40f47ff228f4efaa32d409d732ded98580e03ed37c9535b5fc923d", size = 706369, upload-time = "2025-11-26T02:34:37.783Z" }, { url = "https://files.pythonhosted.org/packages/23/60/8046a386dca39154f80c927cbbeeb4b1c1267a3271bffe61552eb9995757/fastar-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b930da9d598e3bc69513d131f397e6d6be4643926ef3de5d33d1e826631eb036", size = 629097, upload-time = "2025-11-26T02:34:21.888Z" }, { url = "https://files.pythonhosted.org/packages/22/7e/1ae005addc789924a9268da2394d3bb5c6f96836f7e37b7e3d23c2362675/fastar-0.8.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9d210da2de733ca801de83e931012349d209f38b92d9630ccaa94bd445bdc9b8", size = 868938, upload-time = "2025-11-26T02:33:51.119Z" }, { url = "https://files.pythonhosted.org/packages/a6/77/290a892b073b84bf82e6b2259708dfe79c54f356e252c2dd40180b16fe07/fastar-0.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa02270721517078a5bd61a38719070ac2537a4aa6b6c48cf369cf2abc59174a", size = 765204, upload-time = "2025-11-26T02:32:47.02Z" }, { url = "https://files.pythonhosted.org/packages/d0/00/c3155171b976003af3281f5258189f1935b15d1221bfc7467b478c631216/fastar-0.8.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:83c391e5b789a720e4d0029b9559f5d6dee3226693c5b39c0eab8eaece997e0f", size = 764717, upload-time = "2025-11-26T02:33:02.453Z" }, { url = "https://files.pythonhosted.org/packages/b7/43/405b7ad76207b2c11b7b59335b70eac19e4a2653977f5588a1ac8fed54f4/fastar-0.8.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3258d7a78a72793cdd081545da61cabe85b1f37634a1d0b97ffee0ff11d105ef", size = 931502, upload-time = "2025-11-26T02:33:18.619Z" }, { url = "https://files.pythonhosted.org/packages/da/8a/a3dde6d37cc3da4453f2845cdf16675b5686b73b164f37e2cc579b057c2c/fastar-0.8.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e6eab95dd985cdb6a50666cbeb9e4814676e59cfe52039c880b69d67cfd44767", size = 821454, upload-time = "2025-11-26T02:33:33.427Z" }, { url = "https://files.pythonhosted.org/packages/da/c1/904fe2468609c8990dce9fe654df3fbc7324a8d8e80d8240ae2c89757064/fastar-0.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:829b1854166141860887273c116c94e31357213fa8e9fe8baeb18bd6c38aa8d9", size = 821647, upload-time = "2025-11-26T02:34:07Z" }, { url = "https://files.pythonhosted.org/packages/c8/73/a0642ab7a400bc07528091785e868ace598fde06fcd139b8f865ec1b6f3c/fastar-0.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b1667eae13f9457a3c737f4376d68e8c3e548353538b28f7e4273a30cb3965cd", size = 986342, upload-time = "2025-11-26T02:34:53.371Z" }, { url = "https://files.pythonhosted.org/packages/af/af/60c1bfa6edab72366461a95f053d0f5f7ab1825fe65ca2ca367432cd8629/fastar-0.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b864a95229a7db0814cd9ef7987cb713fd43dce1b0d809dd17d9cd6f02fdde3e", size = 1040207, upload-time = "2025-11-26T02:35:10.65Z" }, { url = "https://files.pythonhosted.org/packages/f6/a0/0d624290dec622e7fa084b6881f456809f68777d54a314f5dde932714506/fastar-0.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c05fbc5618ce17675a42576fa49858d79734627f0a0c74c0875ab45ee8de340c", size = 1045031, upload-time = "2025-11-26T02:35:28.108Z" }, { url = "https://files.pythonhosted.org/packages/a7/74/cf663af53c4706ba88e6b4af44a6b0c3bd7d7ca09f079dc40647a8f06585/fastar-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7f41c51ee96f338662ee3c3df4840511ba3f9969606840f1b10b7cb633a3c716", size = 994877, upload-time = "2025-11-26T02:35:45.797Z" }, { url = "https://files.pythonhosted.org/packages/52/17/444c8be6e77206050e350da7c338102b6cab384be937fa0b1d6d1f9ede73/fastar-0.8.0-cp312-cp312-win32.whl", hash = "sha256:d949a1a2ea7968b734632c009df0571c94636a5e1622c87a6e2bf712a7334f47", size = 455996, upload-time = "2025-11-26T02:36:26.938Z" }, { url = "https://files.pythonhosted.org/packages/dc/34/fc3b5e56d71a17b1904800003d9251716e8fd65f662e1b10a26881698a74/fastar-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:fc645994d5b927d769121094e8a649b09923b3c13a8b0b98696d8f853f23c532", size = 490429, upload-time = "2025-11-26T02:36:12.707Z" }, { url = "https://files.pythonhosted.org/packages/35/a8/5608cc837417107c594e2e7be850b9365bcb05e99645966a5d6a156285fe/fastar-0.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:d81ee82e8dc78a0adb81728383bd39611177d642a8fa2d601d4ad5ad59e5f3bd", size = 461297, upload-time = "2025-11-26T02:36:03.546Z" }, { url = "https://files.pythonhosted.org/packages/d1/a5/79ecba3646e22d03eef1a66fb7fc156567213e2e4ab9faab3bbd4489e483/fastar-0.8.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:a3253a06845462ca2196024c7a18f5c0ba4de1532ab1c4bad23a40b332a06a6a", size = 706112, upload-time = "2025-11-26T02:34:39.237Z" }, { url = "https://files.pythonhosted.org/packages/0a/03/4f883bce878218a8676c2d7ca09b50c856a5470bb3b7f63baf9521ea6995/fastar-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5cbeb3ebfa0980c68ff8b126295cc6b208ccd81b638aebc5a723d810a7a0e5d2", size = 628954, upload-time = "2025-11-26T02:34:23.705Z" }, { url = "https://files.pythonhosted.org/packages/4f/f1/892e471f156b03d10ba48ace9384f5a896702a54506137462545f38e40b8/fastar-0.8.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1c0d5956b917daac77d333d48b3f0f3ff927b8039d5b32d8125462782369f761", size = 868685, upload-time = "2025-11-26T02:33:53.077Z" }, { url = "https://files.pythonhosted.org/packages/39/ba/e24915045852e30014ec6840446975c03f4234d1c9270394b51d3ad18394/fastar-0.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27b404db2b786b65912927ce7f3790964a4bcbde42cdd13091b82a89cd655e1c", size = 765044, upload-time = "2025-11-26T02:32:48.187Z" }, { url = "https://files.pythonhosted.org/packages/14/2c/1aa11ac21a99984864c2fca4994e094319ff3a2046e7a0343c39317bd5b9/fastar-0.8.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0902fc89dcf1e7f07b8563032a4159fe2b835e4c16942c76fd63451d0e5f76a3", size = 764322, upload-time = "2025-11-26T02:33:03.859Z" }, { url = "https://files.pythonhosted.org/packages/ba/f0/4b91902af39fe2d3bae7c85c6d789586b9fbcf618d7fdb3d37323915906d/fastar-0.8.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:069347e2f0f7a8b99bbac8cd1bc0e06c7b4a31dc964fc60d84b95eab3d869dc1", size = 931016, upload-time = "2025-11-26T02:33:19.902Z" }, { url = "https://files.pythonhosted.org/packages/c9/97/8fc43a5a9c0a2dc195730f6f7a0f367d171282cd8be2511d0e87c6d2dad0/fastar-0.8.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7fd135306f6bfe9a835918280e0eb440b70ab303e0187d90ab51ca86e143f70d", size = 821308, upload-time = "2025-11-26T02:33:34.664Z" }, { url = "https://files.pythonhosted.org/packages/0c/e9/058615b63a7fd27965e8c5966f393ed0c169f7ff5012e1674f21684de3ba/fastar-0.8.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d06d6897f43c27154b5f2d0eb930a43a81b7eec73f6f0b0114814d4a10ab38", size = 821171, upload-time = "2025-11-26T02:34:08.498Z" }, { url = "https://files.pythonhosted.org/packages/ca/cf/69e16a17961570a755c37ffb5b5aa7610d2e77807625f537989da66f2a9d/fastar-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a922f8439231fa0c32b15e8d70ff6d415619b9d40492029dabbc14a0c53b5f18", size = 986227, upload-time = "2025-11-26T02:34:55.06Z" }, { url = "https://files.pythonhosted.org/packages/fb/83/2100192372e59b56f4ace37d7d9cabda511afd71b5febad1643d1c334271/fastar-0.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a739abd51eb766384b4caff83050888e80cd75bbcfec61e6d1e64875f94e4a40", size = 1039395, upload-time = "2025-11-26T02:35:12.166Z" }, { url = "https://files.pythonhosted.org/packages/75/15/cdd03aca972f55872efbb7cf7540c3fa7b97a75d626303a3ea46932163dc/fastar-0.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5a65f419d808b23ac89d5cd1b13a2f340f15bc5d1d9af79f39fdb77bba48ff1b", size = 1044766, upload-time = "2025-11-26T02:35:29.62Z" }, { url = "https://files.pythonhosted.org/packages/3d/29/945e69e4e2652329ace545999334ec31f1431fbae3abb0105587e11af2ae/fastar-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7bb2ae6c0cce58f0db1c9f20495e7557cca2c1ee9c69bbd90eafd54f139171c5", size = 994740, upload-time = "2025-11-26T02:35:47.887Z" }, { url = "https://files.pythonhosted.org/packages/4b/5d/dbfe28f8cd1eb484bba0c62e5259b2cf6fea229d6ef43e05c06b5a78c034/fastar-0.8.0-cp313-cp313-win32.whl", hash = "sha256:b28753e0d18a643272597cb16d39f1053842aa43131ad3e260c03a2417d38401", size = 455990, upload-time = "2025-11-26T02:36:28.502Z" }, { url = "https://files.pythonhosted.org/packages/e1/01/e965740bd36e60ef4c5aa2cbe42b6c4eb1dc3551009238a97c2e5e96bd23/fastar-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:620e5d737dce8321d49a5ebb7997f1fd0047cde3512082c27dc66d6ac8c1927a", size = 490227, upload-time = "2025-11-26T02:36:14.363Z" }, { url = "https://files.pythonhosted.org/packages/dd/10/c99202719b83e5249f26902ae53a05aea67d840eeb242019322f20fc171c/fastar-0.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:c4c4bd08df563120cd33e854fe0a93b81579e8571b11f9b7da9e84c37da2d6b6", size = 461078, upload-time = "2025-11-26T02:36:04.94Z" }, { url = "https://files.pythonhosted.org/packages/96/4a/9573b87a0ef07580ed111e7230259aec31bb33ca3667963ebee77022ec61/fastar-0.8.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:50b36ce654ba44b0e13fae607ae17ee6e1597b69f71df1bee64bb8328d881dfc", size = 706041, upload-time = "2025-11-26T02:34:40.638Z" }, { url = "https://files.pythonhosted.org/packages/4a/19/f95444a1d4f375333af49300aa75ee93afa3335c0e40fda528e460ed859c/fastar-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:63a892762683d7ab00df0227d5ea9677c62ff2cde9b875e666c0be569ed940f3", size = 628617, upload-time = "2025-11-26T02:34:24.893Z" }, { url = "https://files.pythonhosted.org/packages/b3/c9/b51481b38b7e3f16ef2b9e233b1a3623386c939d745d6e41bbd389eaae30/fastar-0.8.0-cp314-cp314-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4ae6a145c1bff592644bde13f2115e0239f4b7babaf506d14e7d208483cf01a5", size = 869299, upload-time = "2025-11-26T02:33:54.274Z" }, { url = "https://files.pythonhosted.org/packages/bf/02/3ba1267ee5ba7314e29c431cf82eaa68586f2c40cdfa08be3632b7d07619/fastar-0.8.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ae0ff7c0a1c7e1428404b81faee8aebef466bfd0be25bfe4dabf5d535c68741", size = 764667, upload-time = "2025-11-26T02:32:49.606Z" }, { url = "https://files.pythonhosted.org/packages/1b/84/bf33530fd015b5d7c2cc69e0bce4a38d736754a6955487005aab1af6adcd/fastar-0.8.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dbfd87dbd217b45c898b2dbcd0169aae534b2c1c5cbe3119510881f6a5ac8ef5", size = 763993, upload-time = "2025-11-26T02:33:05.782Z" }, { url = "https://files.pythonhosted.org/packages/da/e0/9564d24e7cea6321a8d921c6d2a457044a476ef197aa4708e179d3d97f0d/fastar-0.8.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a5abd99fcba83ef28c8fe6ae2927edc79053db43a0457a962ed85c9bf150d37", size = 930153, upload-time = "2025-11-26T02:33:21.53Z" }, { url = "https://files.pythonhosted.org/packages/35/b1/6f57fcd8d6e192cfebf97e58eb27751640ad93784c857b79039e84387b51/fastar-0.8.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:91d4c685620c3a9d6b5ae091dbabab4f98b20049b7ecc7976e19cc9016c0d5d6", size = 821177, upload-time = "2025-11-26T02:33:35.839Z" }, { url = "https://files.pythonhosted.org/packages/b3/78/9e004ea9f3aa7466f5ddb6f9518780e1d2f0ed3ca55f093632982598bace/fastar-0.8.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f77c2f2cad76e9dc7b6701297adb1eba87d0485944b416fc2ccf5516c01219a3", size = 820652, upload-time = "2025-11-26T02:34:09.776Z" }, { url = "https://files.pythonhosted.org/packages/42/95/b604ed536544005c9f1aee7c4c74b00150db3d8d535cd8232dc20f947063/fastar-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e7f07c4a3dada7757a8fc430a5b4a29e6ef696d2212747213f57086ffd970316", size = 985961, upload-time = "2025-11-26T02:34:56.401Z" }, { url = "https://files.pythonhosted.org/packages/f2/7b/fa9d4d96a5d494bdb8699363bb9de8178c0c21a02e1d89cd6f913d127018/fastar-0.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:90c0c3fe55105c0aed8a83135dbdeb31e683455dbd326a1c48fa44c378b85616", size = 1039316, upload-time = "2025-11-26T02:35:13.807Z" }, { url = "https://files.pythonhosted.org/packages/4e/f9/8462789243bc3f33e8401378ec6d54de4e20cfa60c96a0e15e3e9d1389bb/fastar-0.8.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fb9ee51e5bffe0dab3d3126d3a4fac8d8f7235cedcb4b8e74936087ce1c157f3", size = 1045028, upload-time = "2025-11-26T02:35:31.079Z" }, { url = "https://files.pythonhosted.org/packages/a5/71/9abb128777e616127194b509e98fcda3db797d76288c1a8c23dd22afc14f/fastar-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e380b1e8d30317f52406c43b11e98d11e1d68723bbd031e18049ea3497b59a6d", size = 994677, upload-time = "2025-11-26T02:35:49.391Z" }, { url = "https://files.pythonhosted.org/packages/de/c1/b81b3f194853d7ad232a67a1d768f5f51a016f165cfb56cb31b31bbc6177/fastar-0.8.0-cp314-cp314-win32.whl", hash = "sha256:1c4ffc06e9c4a8ca498c07e094670d8d8c0d25b17ca6465b9774da44ea997ab1", size = 456687, upload-time = "2025-11-26T02:36:30.205Z" }, { url = "https://files.pythonhosted.org/packages/cb/87/9e0cd4768a98181d56f0cdbab2363404cc15deb93f4aad3b99cd2761bbaa/fastar-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:5517a8ad4726267c57a3e0e2a44430b782e00b230bf51c55b5728e758bb3a692", size = 490578, upload-time = "2025-11-26T02:36:16.218Z" }, { url = "https://files.pythonhosted.org/packages/aa/1e/580a76cf91847654f2ad6520e956e93218f778540975bc4190d363f709e2/fastar-0.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:58030551046ff4a8616931e52a36c83545ff05996db5beb6e0cd2b7e748aa309", size = 461473, upload-time = "2025-11-26T02:36:06.373Z" }, { url = "https://files.pythonhosted.org/packages/58/4c/bdb5c6efe934f68708529c8c9d4055ebef5c4be370621966438f658b29bd/fastar-0.8.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:1e7d29b6bfecb29db126a08baf3c04a5ab667f6cea2b7067d3e623a67729c4a6", size = 705570, upload-time = "2025-11-26T02:34:42.01Z" }, { url = "https://files.pythonhosted.org/packages/6d/78/f01ac7e71d5a37621bd13598a26e948a12b85ca8042f7ee1a0a8c9f59cda/fastar-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:05eb7b96940f9526b485f1d0b02393839f0f61cac4b1f60024984f8b326d2640", size = 627761, upload-time = "2025-11-26T02:34:26.152Z" }, { url = "https://files.pythonhosted.org/packages/06/45/6df0ecda86ea9d2e95053c1a655d153dee55fc121b6e13ea6d1e246a50b6/fastar-0.8.0-cp314-cp314t-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:619352d8ac011794e2345c462189dc02ba634750d23cd9d86a9267dd71b1f278", size = 869414, upload-time = "2025-11-26T02:33:55.618Z" }, { url = "https://files.pythonhosted.org/packages/b2/72/486421f5a8c0c377cc82e7a50c8a8ea899a6ec2aa72bde8f09fb667a2dc8/fastar-0.8.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74ebfecef3fe6d7a90355fac1402fd30636988332a1d33f3e80019a10782bb24", size = 763863, upload-time = "2025-11-26T02:32:51.051Z" }, { url = "https://files.pythonhosted.org/packages/d4/64/39f654dbb41a3867fb1f2c8081c014d8f1d32ea10585d84cacbef0b32995/fastar-0.8.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2975aca5a639e26a3ab0d23b4b0628d6dd6d521146c3c11486d782be621a35aa", size = 763065, upload-time = "2025-11-26T02:33:07.274Z" }, { url = "https://files.pythonhosted.org/packages/4e/bd/c011a34fb3534c4c3301f7c87c4ffd7e47f6113c904c092ddc8a59a303ea/fastar-0.8.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afc438eaed8ff0dcdd9308268be5cb38c1db7e94c3ccca7c498ca13a4a4535a3", size = 930530, upload-time = "2025-11-26T02:33:23.117Z" }, { url = "https://files.pythonhosted.org/packages/55/9d/aa6e887a7033c571b1064429222bbe09adc9a3c1e04f3d1788ba5838ebd5/fastar-0.8.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6ced0a5399cc0a84a858ef0a31ca2d0c24d3bbec4bcda506a9192d8119f3590a", size = 820572, upload-time = "2025-11-26T02:33:37.542Z" }, { url = "https://files.pythonhosted.org/packages/ad/9c/7a3a2278a1052e1a5d98646de7c095a00cffd2492b3b84ce730e2f1cd93a/fastar-0.8.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec9b23da8c4c039da3fe2e358973c66976a0c8508aa06d6626b4403cb5666c19", size = 820649, upload-time = "2025-11-26T02:34:11.108Z" }, { url = "https://files.pythonhosted.org/packages/02/9e/d38edc1f4438cd047e56137c26d94783ffade42e1b3bde620ccf17b771ef/fastar-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:dfba078fcd53478032fd0ceed56960ec6b7ff0511cfc013a8a3a4307e3a7bac4", size = 985653, upload-time = "2025-11-26T02:34:57.884Z" }, { url = "https://files.pythonhosted.org/packages/69/d9/2147d0c19757e165cd62d41cec3f7b38fad2ad68ab784978b5f81716c7ea/fastar-0.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ade56c94c14be356d295fecb47a3fcd473dd43a8803ead2e2b5b9e58feb6dcfa", size = 1038140, upload-time = "2025-11-26T02:35:15.778Z" }, { url = "https://files.pythonhosted.org/packages/7f/1d/ec4c717ffb8a308871e9602ec3197d957e238dc0227127ac573ec9bca952/fastar-0.8.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e48d938f9366db5e59441728f70b7f6c1ccfab7eff84f96f9b7e689b07786c52", size = 1045195, upload-time = "2025-11-26T02:35:32.865Z" }, { url = "https://files.pythonhosted.org/packages/6a/9f/637334dc8c8f3bb391388b064ae13f0ad9402bc5a6c3e77b8887d0c31921/fastar-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:79c441dc1482ff51a54fb3f57ae6f7bb3d2cff88fa2cc5d196c519f8aab64a56", size = 994686, upload-time = "2025-11-26T02:35:51.392Z" }, { url = "https://files.pythonhosted.org/packages/c9/e2/dfa19a4b260b8ab3581b7484dcb80c09b25324f4daa6b6ae1c7640d1607a/fastar-0.8.0-cp314-cp314t-win32.whl", hash = "sha256:187f61dc739afe45ac8e47ed7fd1adc45d52eac110cf27d579155720507d6fbe", size = 455767, upload-time = "2025-11-26T02:36:34.758Z" }, { url = "https://files.pythonhosted.org/packages/51/47/df65c72afc1297797b255f90c4778b5d6f1f0f80282a134d5ab610310ed9/fastar-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:40e9d763cf8bf85ce2fa256e010aa795c0fe3d3bd1326d5c3084e6ce7857127e", size = 489971, upload-time = "2025-11-26T02:36:22.081Z" }, { url = "https://files.pythonhosted.org/packages/85/11/0aa8455af26f0ae89e42be67f3a874255ee5d7f0f026fc86e8d56f76b428/fastar-0.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:e59673307b6a08210987059a2bdea2614fe26e3335d0e5d1a3d95f49a05b1418", size = 460467, upload-time = "2025-11-26T02:36:07.978Z" }, { url = "https://files.pythonhosted.org/packages/25/9f/6eaa810c240236eff2edf736cd50a17c97dbab1693cda4f7bcea09d13418/fastar-0.8.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2127cf2e80ffd49744a160201e0e2f55198af6c028a7b3f750026e0b1f1caa4e", size = 710544, upload-time = "2025-11-26T02:34:46.195Z" }, { url = "https://files.pythonhosted.org/packages/1d/a5/58ff9e49a1cd5fbfc8f1238226cbf83b905376a391a6622cdd396b2cfa29/fastar-0.8.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:ff85094f10003801339ac4fa9b20a3410c2d8f284d4cba2dc99de6e98c877812", size = 634020, upload-time = "2025-11-26T02:34:31.085Z" }, { url = "https://files.pythonhosted.org/packages/80/94/f839257c6600a83fbdb5a7fcc06319599086137b25ba38ca3d2c0fe14562/fastar-0.8.0-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:3dbca235f0bd804cca6602fe055d3892bebf95fb802e6c6c7d872fb10f7abc6c", size = 871735, upload-time = "2025-11-26T02:34:00.088Z" }, { url = "https://files.pythonhosted.org/packages/eb/79/4124c54260f7ee5cb7034bfe499eff2f8512b052d54be4671e59d4f25a4f/fastar-0.8.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:722e54bfdee6c81a0005e147319e93d8797f442308032c92fa28d03ef8fda076", size = 766779, upload-time = "2025-11-26T02:32:55.109Z" }, { url = "https://files.pythonhosted.org/packages/36/b6/043b263c4126bf6557c942d099503989af9c5c7ee5cca9a04e00f754816f/fastar-0.8.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a78e5221b94a80800930b7fd0d0e797ae73aadf7044c05ed46cb9bdf870f022", size = 766755, upload-time = "2025-11-26T02:33:11.595Z" }, { url = "https://files.pythonhosted.org/packages/57/ff/29a5dc06f2940439ebf98661ecc98d48d3f22fed8d6a2d5dc985d1e8da24/fastar-0.8.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:997092d31ff451de8d0568f6773f3517cb87dcd0bc76184edb65d7154390a6f8", size = 932732, upload-time = "2025-11-26T02:33:27.122Z" }, { url = "https://files.pythonhosted.org/packages/eb/e8/2218830f422b37aad52c24b53cb84b5d88bd6fd6ad411bd6689b1a32500d/fastar-0.8.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:558e8fcf8fe574541df5db14a46cd98bfbed14a811b7014a54f2b714c0cfac42", size = 822571, upload-time = "2025-11-26T02:33:42.986Z" }, { url = "https://files.pythonhosted.org/packages/6e/fd/ba6dfeff77cddfe58d85c490b1735c002b81c0d6f826916a8b6c4f8818bc/fastar-0.8.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1d2a54f87e2908cc19e1a6ee249620174fbefc54a219aba1eaa6f31657683c3", size = 822440, upload-time = "2025-11-26T02:34:15.439Z" }, { url = "https://files.pythonhosted.org/packages/a7/57/54d5740c84b35de0eb12975397ecc16785b5ad8bed2dbac38b8c8a7c1edd/fastar-0.8.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:ef94901537be277f9ec59db939eb817960496c6351afede5b102699b5098604d", size = 987424, upload-time = "2025-11-26T02:35:02.742Z" }, { url = "https://files.pythonhosted.org/packages/ee/c7/18115927f16deb1ddffdbd4ae992e7e33064bc6defa2b92a147948f8bc0c/fastar-0.8.0-pp310-pypy310_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:0afbb92f78bf29d5e9db76fb46cbabc429e49015cddf72ab9e761afbe88ac100", size = 1042675, upload-time = "2025-11-26T02:35:20.252Z" }, { url = "https://files.pythonhosted.org/packages/d7/1a/ca884fc7973ec6d765e87af23a4dd25784fb0a36ac2df825f18c3630bbab/fastar-0.8.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:fb59c7925e7710ad178d9e1a3e65edf295d9a042a0cdcb673b4040949eb8ad0a", size = 1047098, upload-time = "2025-11-26T02:35:37.643Z" }, { url = "https://files.pythonhosted.org/packages/44/ee/25cd645db749b206bb95e1512e57e75d56ccbbb8ec3536f52a7979deab6b/fastar-0.8.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:e6c4d6329da568ec36b1347b0c09c4d27f9dfdeddf9f438ddb16799ecf170098", size = 997397, upload-time = "2025-11-26T02:35:56.215Z" }, { url = "https://files.pythonhosted.org/packages/98/6e/6c46aa7f8c8734e7f96ee5141acd3877667ce66f34eea10703aa7571d191/fastar-0.8.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:998e3fa4b555b63eb134e6758437ed739ad1652fdd2a61dfe1dacbfddc35fe66", size = 710662, upload-time = "2025-11-26T02:34:47.593Z" }, { url = "https://files.pythonhosted.org/packages/70/27/fd622442f2fbd4ff5459677987481ef1c60e077cb4e63a2ed4d8dce6f869/fastar-0.8.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5f83e60d845091f3a12bc37f412774264d161576eaf810ed8b43567eb934b7e5", size = 634049, upload-time = "2025-11-26T02:34:32.365Z" }, { url = "https://files.pythonhosted.org/packages/8f/ee/aa4d08aea25b5419a7277132e738ab1cd775f26aebddce11413b07e2fdff/fastar-0.8.0-pp311-pypy311_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:299672e1c74d8b73c61684fac9159cfc063d35f4b165996a88facb0e26862cb5", size = 872055, upload-time = "2025-11-26T02:34:01.377Z" }, { url = "https://files.pythonhosted.org/packages/92/9a/2bf2f77aade575e67997e0c759fd55cb1c66b7a5b437b1cd0e97d8b241bc/fastar-0.8.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3d3a27066b84d015deab5faee78565509bb33b137896443e4144cb1be1a5f90", size = 766787, upload-time = "2025-11-26T02:32:57.161Z" }, { url = "https://files.pythonhosted.org/packages/0b/90/23a3f6c252f11b10c70f854bce09abc61f71b5a0e6a4b0eac2bcb9a2c583/fastar-0.8.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ef0bcf4385bbdd3c1acecce2d9ea7dab7cc9b8ee0581bbccb7ab11908a7ce288", size = 766861, upload-time = "2025-11-26T02:33:12.824Z" }, { url = "https://files.pythonhosted.org/packages/76/bb/beeb9078380acd4484db5c957d066171695d9340e3526398eb230127b0c2/fastar-0.8.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f10ef62b6eda6cb6fd9ba8e1fe08a07d7b2bdcc8eaa00eb91566143b92ed7eee", size = 932667, upload-time = "2025-11-26T02:33:28.405Z" }, { url = "https://files.pythonhosted.org/packages/f4/6d/b034cc637bd0ee638d5a85d08e941b0b8ffd44cf391fb751ba98233734f7/fastar-0.8.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c4f6c82a8ee98c17aa48585ee73b51c89c1b010e5c951af83e07c3436180e3fc", size = 822712, upload-time = "2025-11-26T02:33:44.27Z" }, { url = "https://files.pythonhosted.org/packages/e2/2b/7d183c63f59227c4689792042d6647f2586a5e7273b55e81745063088d81/fastar-0.8.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6129067fcb86276635b5857010f4e9b9c7d5d15dd571bb03c6c1ed73c40fd92", size = 822659, upload-time = "2025-11-26T02:34:16.815Z" }, { url = "https://files.pythonhosted.org/packages/3e/f9/716e0cd9de2427fdf766bc68176f76226cd01fffef3a56c5046fa863f5f0/fastar-0.8.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:4cc9e77019e489f1ddac446b6a5b9dfb5c3d9abd142652c22a1d9415dbcc0e47", size = 987412, upload-time = "2025-11-26T02:35:04.259Z" }, { url = "https://files.pythonhosted.org/packages/a4/b9/9a8c3fd59958c1c8027bc075af11722cdc62c4968bb277e841d131232289/fastar-0.8.0-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:382bfe82c026086487cb17fee12f4c1e2b4e67ce230f2e04487d3e7ddfd69031", size = 1042911, upload-time = "2025-11-26T02:35:21.857Z" }, { url = "https://files.pythonhosted.org/packages/e2/2f/c3f30963b47022134b8a231c12845f4d7cfba520f59bbc1a82468aea77c7/fastar-0.8.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:908d2b9a1ff3d549cc304b32f95706a536da8f0bcb0bc0f9e4c1cce39b80e218", size = 1047464, upload-time = "2025-11-26T02:35:39.376Z" }, { url = "https://files.pythonhosted.org/packages/9e/8a/218ab6d9a2bab3b07718e6cd8405529600edc1e9c266320e8524c8f63251/fastar-0.8.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:1aa7dbde2d2d73eb5b6203d0f74875cb66350f0f1b4325b4839fc8fbbf5d074e", size = 997309, upload-time = "2025-11-26T02:35:57.722Z" }, ] [[package]] name = "filelock" version = "3.20.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, ] [[package]] name = "ghp-import" version = "2.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "python-dateutil" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, ] [[package]] name = "gitdb" version = "4.0.12" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "smmap" }, ] sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, ] [[package]] name = "gitpython" version = "3.1.46" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "gitdb" }, ] sdist = { url = "https://files.pythonhosted.org/packages/df/b5/59d16470a1f0dfe8c793f9ef56fd3826093fc52b3bd96d6b9d6c26c7e27b/gitpython-3.1.46.tar.gz", hash = "sha256:400124c7d0ef4ea03f7310ac2fbf7151e09ff97f2a3288d64a440c584a29c37f", size = 215371, upload-time = "2026-01-01T15:37:32.073Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/6a/09/e21df6aef1e1ffc0c816f0522ddc3f6dcded766c3261813131c78a704470/gitpython-3.1.46-py3-none-any.whl", hash = "sha256:79812ed143d9d25b6d176a10bb511de0f9c67b1fa641d82097b0ab90398a2058", size = 208620, upload-time = "2026-01-01T15:37:30.574Z" }, ] [[package]] name = "h11" version = "0.16.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] [[package]] name = "hatch" version = "1.15.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "hatchling" }, { name = "httpx" }, { name = "hyperlink" }, { name = "keyring" }, { name = "packaging" }, { name = "pexpect" }, { name = "platformdirs" }, { name = "rich" }, { name = "shellingham" }, { name = "tomli-w" }, { name = "tomlkit" }, { name = "userpath" }, { name = "uv" }, { name = "virtualenv" }, { name = "zstandard" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f8/30/a7f19d337df93fb15dec6892e9ae678acd4ae10ce03d02722f17c7fe513b/hatch-1.15.1.tar.gz", hash = "sha256:444a78123c9837e8c9f5adfbf2b8b0a72139587eb49d6b368038b0521136fc43", size = 5189156, upload-time = "2025-10-16T20:35:54.616Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/69/01/316ef533114e0de0649e2e925ae2f97dfe26fbe5358f678e84b2a5fa1407/hatch-1.15.1-py3-none-any.whl", hash = "sha256:99dccb26b00226056142f89d6e286be61e2d7b5b5b4e6178ebbe9298c1bc45d9", size = 126295, upload-time = "2025-10-16T20:35:52.354Z" }, ] [[package]] name = "hatchling" version = "1.28.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, { name = "pathspec" }, { name = "pluggy" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "trove-classifiers" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0b/8e/e480359492affde4119a131da729dd26da742c2c9b604dff74836e47eef9/hatchling-1.28.0.tar.gz", hash = "sha256:4d50b02aece6892b8cd0b3ce6c82cb218594d3ec5836dbde75bf41a21ab004c8", size = 55365, upload-time = "2025-11-27T00:31:13.766Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0d/a5/48cb7efb8b4718b1a4c0c331e3364a3a33f614ff0d6afd2b93ee883d3c47/hatchling-1.28.0-py3-none-any.whl", hash = "sha256:dc48722b68b3f4bbfa3ff618ca07cdea6750e7d03481289ffa8be1521d18a961", size = 76075, upload-time = "2025-11-27T00:31:12.544Z" }, ] [[package]] name = "hishel" version = "1.1.8" source = { editable = "." } dependencies = [ { name = "msgpack" }, { name = "typing-extensions" }, ] [package.optional-dependencies] async = [ { name = "anyio" }, { name = "anysqlite" }, ] fastapi = [ { name = "fastapi" }, ] httpx = [ { name = "anyio" }, { name = "anysqlite" }, { name = "httpx" }, ] requests = [ { name = "requests" }, ] [package.dev-dependencies] dev = [ { name = "anyio" }, { name = "anysqlite" }, { name = "coverage" }, { name = "fastapi", extra = ["standard"] }, { name = "hatch" }, { name = "inline-snapshot" }, { name = "mike" }, { name = "mkdocs" }, { name = "mkdocs-git-committers-plugin" }, { name = "mkdocs-git-revision-date-localized-plugin" }, { name = "mkdocs-material" }, { name = "mypy" }, { name = "pyright" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "pytest-icdiff" }, { name = "ruff" }, { name = "time-machine" }, { name = "trio" }, { name = "types-boto3" }, { name = "types-pyyaml" }, { name = "types-requests" }, { name = "zipp" }, ] [package.metadata] requires-dist = [ { name = "anyio", marker = "extra == 'async'", specifier = ">=4.9.0" }, { name = "anyio", marker = "extra == 'httpx'", specifier = ">=4.9.0" }, { name = "anysqlite", marker = "extra == 'async'", specifier = ">=0.0.5" }, { name = "anysqlite", marker = "extra == 'httpx'", specifier = ">=0.0.5" }, { name = "fastapi", marker = "extra == 'fastapi'", specifier = ">=0.119.1" }, { name = "httpx", marker = "extra == 'httpx'", specifier = ">=0.28.1" }, { name = "msgpack", specifier = ">=1.1.2" }, { name = "requests", marker = "extra == 'requests'", specifier = ">=2.32.5" }, { name = "typing-extensions", specifier = ">=4.14.1" }, ] provides-extras = ["async", "requests", "httpx", "fastapi"] [package.metadata.requires-dev] dev = [ { name = "anyio", specifier = "==4.12.0" }, { name = "anysqlite", specifier = ">=0.0.5" }, { name = "coverage", specifier = "==7.10.7" }, { name = "fastapi", extras = ["standard"], specifier = ">=0.119.1" }, { name = "hatch", specifier = "==1.15.1" }, { name = "inline-snapshot", specifier = ">=0.28.0" }, { name = "mike", specifier = ">=2.1.3" }, { name = "mkdocs", specifier = "==1.6.1" }, { name = "mkdocs-git-committers-plugin", specifier = ">=0.2.3" }, { name = "mkdocs-git-revision-date-localized-plugin", specifier = ">=1.4.7" }, { name = "mkdocs-material", specifier = "==9.7.0" }, { name = "mypy", specifier = "==1.19.0" }, { name = "pyright", specifier = ">=1.1.404" }, { name = "pytest", specifier = "==8.4.2" }, { name = "pytest-cov", specifier = ">=6.2.1" }, { name = "pytest-icdiff", specifier = ">=0.9" }, { name = "ruff", specifier = "==0.14.7" }, { name = "time-machine", specifier = ">=2.19.0" }, { name = "trio", specifier = "==0.31.0" }, { name = "types-boto3", specifier = "==1.41.5" }, { name = "types-pyyaml", specifier = "==6.0.12.20250915" }, { name = "types-requests", specifier = ">=2.31.0.6" }, { name = "zipp", specifier = ">=3.19.1" }, ] [[package]] name = "httpcore" version = "1.0.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "h11" }, ] sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, ] [[package]] name = "httptools" version = "0.7.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c7/e5/c07e0bcf4ec8db8164e9f6738c048b2e66aabf30e7506f440c4cc6953f60/httptools-0.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:11d01b0ff1fe02c4c32d60af61a4d613b74fad069e47e06e9067758c01e9ac78", size = 204531, upload-time = "2025-10-10T03:54:20.887Z" }, { url = "https://files.pythonhosted.org/packages/7e/4f/35e3a63f863a659f92ffd92bef131f3e81cf849af26e6435b49bd9f6f751/httptools-0.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84d86c1e5afdc479a6fdabf570be0d3eb791df0ae727e8dbc0259ed1249998d4", size = 109408, upload-time = "2025-10-10T03:54:22.455Z" }, { url = "https://files.pythonhosted.org/packages/f5/71/b0a9193641d9e2471ac541d3b1b869538a5fb6419d52fd2669fa9c79e4b8/httptools-0.7.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c8c751014e13d88d2be5f5f14fc8b89612fcfa92a9cc480f2bc1598357a23a05", size = 440889, upload-time = "2025-10-10T03:54:23.753Z" }, { url = "https://files.pythonhosted.org/packages/eb/d9/2e34811397b76718750fea44658cb0205b84566e895192115252e008b152/httptools-0.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:654968cb6b6c77e37b832a9be3d3ecabb243bbe7a0b8f65fbc5b6b04c8fcabed", size = 440460, upload-time = "2025-10-10T03:54:25.313Z" }, { url = "https://files.pythonhosted.org/packages/01/3f/a04626ebeacc489866bb4d82362c0657b2262bef381d68310134be7f40bb/httptools-0.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b580968316348b474b020edf3988eecd5d6eec4634ee6561e72ae3a2a0e00a8a", size = 425267, upload-time = "2025-10-10T03:54:26.81Z" }, { url = "https://files.pythonhosted.org/packages/a5/99/adcd4f66614db627b587627c8ad6f4c55f18881549bab10ecf180562e7b9/httptools-0.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d496e2f5245319da9d764296e86c5bb6fcf0cf7a8806d3d000717a889c8c0b7b", size = 424429, upload-time = "2025-10-10T03:54:28.174Z" }, { url = "https://files.pythonhosted.org/packages/d5/72/ec8fc904a8fd30ba022dfa85f3bbc64c3c7cd75b669e24242c0658e22f3c/httptools-0.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cbf8317bfccf0fed3b5680c559d3459cccf1abe9039bfa159e62e391c7270568", size = 86173, upload-time = "2025-10-10T03:54:29.5Z" }, { url = "https://files.pythonhosted.org/packages/9c/08/17e07e8d89ab8f343c134616d72eebfe03798835058e2ab579dcc8353c06/httptools-0.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657", size = 206521, upload-time = "2025-10-10T03:54:31.002Z" }, { url = "https://files.pythonhosted.org/packages/aa/06/c9c1b41ff52f16aee526fd10fbda99fa4787938aa776858ddc4a1ea825ec/httptools-0.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70", size = 110375, upload-time = "2025-10-10T03:54:31.941Z" }, { url = "https://files.pythonhosted.org/packages/cc/cc/10935db22fda0ee34c76f047590ca0a8bd9de531406a3ccb10a90e12ea21/httptools-0.7.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df", size = 456621, upload-time = "2025-10-10T03:54:33.176Z" }, { url = "https://files.pythonhosted.org/packages/0e/84/875382b10d271b0c11aa5d414b44f92f8dd53e9b658aec338a79164fa548/httptools-0.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e", size = 454954, upload-time = "2025-10-10T03:54:34.226Z" }, { url = "https://files.pythonhosted.org/packages/30/e1/44f89b280f7e46c0b1b2ccee5737d46b3bb13136383958f20b580a821ca0/httptools-0.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274", size = 440175, upload-time = "2025-10-10T03:54:35.942Z" }, { url = "https://files.pythonhosted.org/packages/6f/7e/b9287763159e700e335028bc1824359dc736fa9b829dacedace91a39b37e/httptools-0.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec", size = 440310, upload-time = "2025-10-10T03:54:37.1Z" }, { url = "https://files.pythonhosted.org/packages/b3/07/5b614f592868e07f5c94b1f301b5e14a21df4e8076215a3bccb830a687d8/httptools-0.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb", size = 86875, upload-time = "2025-10-10T03:54:38.421Z" }, { url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280, upload-time = "2025-10-10T03:54:39.274Z" }, { url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004, upload-time = "2025-10-10T03:54:40.403Z" }, { url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655, upload-time = "2025-10-10T03:54:41.347Z" }, { url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440, upload-time = "2025-10-10T03:54:42.452Z" }, { url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186, upload-time = "2025-10-10T03:54:43.937Z" }, { url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192, upload-time = "2025-10-10T03:54:45.003Z" }, { url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", size = 86694, upload-time = "2025-10-10T03:54:45.923Z" }, { url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889, upload-time = "2025-10-10T03:54:47.089Z" }, { url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180, upload-time = "2025-10-10T03:54:48.052Z" }, { url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596, upload-time = "2025-10-10T03:54:48.919Z" }, { url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268, upload-time = "2025-10-10T03:54:49.993Z" }, { url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517, upload-time = "2025-10-10T03:54:51.066Z" }, { url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337, upload-time = "2025-10-10T03:54:52.196Z" }, { url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743, upload-time = "2025-10-10T03:54:53.448Z" }, { url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619, upload-time = "2025-10-10T03:54:54.321Z" }, { url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714, upload-time = "2025-10-10T03:54:55.163Z" }, { url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909, upload-time = "2025-10-10T03:54:56.056Z" }, { url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831, upload-time = "2025-10-10T03:54:57.219Z" }, { url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631, upload-time = "2025-10-10T03:54:58.219Z" }, { url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910, upload-time = "2025-10-10T03:54:59.366Z" }, { url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205, upload-time = "2025-10-10T03:55:00.389Z" }, ] [[package]] name = "httpx" version = "0.28.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "certifi" }, { name = "httpcore" }, { name = "idna" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] [[package]] name = "hyperlink" version = "21.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3a/51/1947bd81d75af87e3bb9e34593a4cf118115a8feb451ce7a69044ef1412e/hyperlink-21.0.0.tar.gz", hash = "sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b", size = 140743, upload-time = "2021-01-08T05:51:20.972Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/6e/aa/8caf6a0a3e62863cbb9dab27135660acba46903b703e224f14f447e57934/hyperlink-21.0.0-py2.py3-none-any.whl", hash = "sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4", size = 74638, upload-time = "2021-01-08T05:51:22.906Z" }, ] [[package]] name = "icdiff" version = "2.0.7" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/fa/e4/43341832be5f2bcae71eb3ef08a07aaef9b74f74fe0b3675f62bd12057fe/icdiff-2.0.7.tar.gz", hash = "sha256:f79a318891adbf59a45e3a7694f5e1f18c5407065264637072ac8363b759866f", size = 16394, upload-time = "2023-08-21T15:00:55.742Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7c/2a/b3178baa75a3ec75a33588252296c82a1332d2b83cd01061539b74bde9dd/icdiff-2.0.7-py3-none-any.whl", hash = "sha256:f05d1b3623223dd1c70f7848da7d699de3d9a2550b902a8234d9026292fb5762", size = 17018, upload-time = "2023-08-21T15:00:54.634Z" }, ] [[package]] name = "idna" version = "3.11" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] [[package]] name = "importlib-metadata" version = "8.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "zipp" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, ] [[package]] name = "importlib-resources" version = "6.5.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/cf/8c/f834fbf984f691b4f7ff60f50b514cc3de5cc08abfc3295564dd89c5e2e7/importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c", size = 44693, upload-time = "2025-01-03T18:51:56.698Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461, upload-time = "2025-01-03T18:51:54.306Z" }, ] [[package]] name = "iniconfig" version = "2.3.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] [[package]] name = "inline-snapshot" version = "0.28.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asttokens" }, { name = "executing" }, { name = "pytest" }, { name = "rich" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e4/9e/83aaa750e9c8115d34b2d80646c1988941f2252c5548caf35aad5e529bad/inline_snapshot-0.28.0.tar.gz", hash = "sha256:6904bfc383240b6bea64de2f5d2992f04109b13def19395bdd13fb0ebcf5cf20", size = 348554, upload-time = "2025-08-24T21:48:04.056Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e6/04/190b336a006d4e1275c2dde1bf953336e818d18b779f24947579bb4ba48d/inline_snapshot-0.28.0-py3-none-any.whl", hash = "sha256:9988f82ee5e719445bbc437d0dc01e0a3c4c94f0ba910f8ad8b573cf15aa8348", size = 69026, upload-time = "2025-08-24T21:48:02.342Z" }, ] [[package]] name = "jaraco-classes" version = "3.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "more-itertools" }, ] sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" }, ] [[package]] name = "jaraco-context" version = "6.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "backports-tarfile", marker = "python_full_version < '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cb/9c/a788f5bb29c61e456b8ee52ce76dbdd32fd72cd73dd67bc95f42c7a8d13c/jaraco_context-6.1.0.tar.gz", hash = "sha256:129a341b0a85a7db7879e22acd66902fda67882db771754574338898b2d5d86f", size = 15850, upload-time = "2026-01-13T02:53:53.847Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8d/48/aa685dbf1024c7bd82bede569e3a85f82c32fd3d79ba5fea578f0159571a/jaraco_context-6.1.0-py3-none-any.whl", hash = "sha256:a43b5ed85815223d0d3cfdb6d7ca0d2bc8946f28f30b6f3216bda070f68badda", size = 7065, upload-time = "2026-01-13T02:53:53.031Z" }, ] [[package]] name = "jaraco-functools" version = "4.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "more-itertools" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0f/27/056e0638a86749374d6f57d0b0db39f29509cce9313cf91bdc0ac4d91084/jaraco_functools-4.4.0.tar.gz", hash = "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb", size = 19943, upload-time = "2025-12-21T09:29:43.6Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176", size = 10481, upload-time = "2025-12-21T09:29:42.27Z" }, ] [[package]] name = "jeepney" version = "0.9.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, ] [[package]] name = "jinja2" version = "3.1.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] [[package]] name = "keyring" version = "25.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, { name = "jaraco-classes" }, { name = "jaraco-context" }, { name = "jaraco-functools" }, { name = "jeepney", marker = "sys_platform == 'linux'" }, { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, { name = "secretstorage", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" }, ] [[package]] name = "librt" version = "0.7.8" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e7/24/5f3646ff414285e0f7708fa4e946b9bf538345a41d1c375c439467721a5e/librt-0.7.8.tar.gz", hash = "sha256:1a4ede613941d9c3470b0368be851df6bb78ab218635512d0370b27a277a0862", size = 148323, upload-time = "2026-01-14T12:56:16.876Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/44/13/57b06758a13550c5f09563893b004f98e9537ee6ec67b7df85c3571c8832/librt-0.7.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b45306a1fc5f53c9330fbee134d8b3227fe5da2ab09813b892790400aa49352d", size = 56521, upload-time = "2026-01-14T12:54:40.066Z" }, { url = "https://files.pythonhosted.org/packages/c2/24/bbea34d1452a10612fb45ac8356f95351ba40c2517e429602160a49d1fd0/librt-0.7.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:864c4b7083eeee250ed55135d2127b260d7eb4b5e953a9e5df09c852e327961b", size = 58456, upload-time = "2026-01-14T12:54:41.471Z" }, { url = "https://files.pythonhosted.org/packages/04/72/a168808f92253ec3a810beb1eceebc465701197dbc7e865a1c9ceb3c22c7/librt-0.7.8-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6938cc2de153bc927ed8d71c7d2f2ae01b4e96359126c602721340eb7ce1a92d", size = 164392, upload-time = "2026-01-14T12:54:42.843Z" }, { url = "https://files.pythonhosted.org/packages/14/5c/4c0d406f1b02735c2e7af8ff1ff03a6577b1369b91aa934a9fa2cc42c7ce/librt-0.7.8-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66daa6ac5de4288a5bbfbe55b4caa7bf0cd26b3269c7a476ffe8ce45f837f87d", size = 172959, upload-time = "2026-01-14T12:54:44.602Z" }, { url = "https://files.pythonhosted.org/packages/82/5f/3e85351c523f73ad8d938989e9a58c7f59fb9c17f761b9981b43f0025ce7/librt-0.7.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4864045f49dc9c974dadb942ac56a74cd0479a2aafa51ce272c490a82322ea3c", size = 186717, upload-time = "2026-01-14T12:54:45.986Z" }, { url = "https://files.pythonhosted.org/packages/08/f8/18bfe092e402d00fe00d33aa1e01dda1bd583ca100b393b4373847eade6d/librt-0.7.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a36515b1328dc5b3ffce79fe204985ca8572525452eacabee2166f44bb387b2c", size = 184585, upload-time = "2026-01-14T12:54:47.139Z" }, { url = "https://files.pythonhosted.org/packages/4e/fc/f43972ff56fd790a9fa55028a52ccea1875100edbb856b705bd393b601e3/librt-0.7.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b7e7f140c5169798f90b80d6e607ed2ba5059784968a004107c88ad61fb3641d", size = 180497, upload-time = "2026-01-14T12:54:48.946Z" }, { url = "https://files.pythonhosted.org/packages/e1/3a/25e36030315a410d3ad0b7d0f19f5f188e88d1613d7d3fd8150523ea1093/librt-0.7.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff71447cb778a4f772ddc4ce360e6ba9c95527ed84a52096bd1bbf9fee2ec7c0", size = 200052, upload-time = "2026-01-14T12:54:50.382Z" }, { url = "https://files.pythonhosted.org/packages/fc/b8/f3a5a1931ae2a6ad92bf6893b9ef44325b88641d58723529e2c2935e8abe/librt-0.7.8-cp310-cp310-win32.whl", hash = "sha256:047164e5f68b7a8ebdf9fae91a3c2161d3192418aadd61ddd3a86a56cbe3dc85", size = 43477, upload-time = "2026-01-14T12:54:51.815Z" }, { url = "https://files.pythonhosted.org/packages/fe/91/c4202779366bc19f871b4ad25db10fcfa1e313c7893feb942f32668e8597/librt-0.7.8-cp310-cp310-win_amd64.whl", hash = "sha256:d6f254d096d84156a46a84861183c183d30734e52383602443292644d895047c", size = 49806, upload-time = "2026-01-14T12:54:53.149Z" }, { url = "https://files.pythonhosted.org/packages/1b/a3/87ea9c1049f2c781177496ebee29430e4631f439b8553a4969c88747d5d8/librt-0.7.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3e9c11aa260c31493d4b3197d1e28dd07768594a4f92bec4506849d736248f", size = 56507, upload-time = "2026-01-14T12:54:54.156Z" }, { url = "https://files.pythonhosted.org/packages/5e/4a/23bcef149f37f771ad30203d561fcfd45b02bc54947b91f7a9ac34815747/librt-0.7.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ddb52499d0b3ed4aa88746aaf6f36a08314677d5c346234c3987ddc506404eac", size = 58455, upload-time = "2026-01-14T12:54:55.978Z" }, { url = "https://files.pythonhosted.org/packages/22/6e/46eb9b85c1b9761e0f42b6e6311e1cc544843ac897457062b9d5d0b21df4/librt-0.7.8-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e9c0afebbe6ce177ae8edba0c7c4d626f2a0fc12c33bb993d163817c41a7a05c", size = 164956, upload-time = "2026-01-14T12:54:57.311Z" }, { url = "https://files.pythonhosted.org/packages/7a/3f/aa7c7f6829fb83989feb7ba9aa11c662b34b4bd4bd5b262f2876ba3db58d/librt-0.7.8-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:631599598e2c76ded400c0a8722dec09217c89ff64dc54b060f598ed68e7d2a8", size = 174364, upload-time = "2026-01-14T12:54:59.089Z" }, { url = "https://files.pythonhosted.org/packages/3f/2d/d57d154b40b11f2cb851c4df0d4c4456bacd9b1ccc4ecb593ddec56c1a8b/librt-0.7.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c1ba843ae20db09b9d5c80475376168feb2640ce91cd9906414f23cc267a1ff", size = 188034, upload-time = "2026-01-14T12:55:00.141Z" }, { url = "https://files.pythonhosted.org/packages/59/f9/36c4dad00925c16cd69d744b87f7001792691857d3b79187e7a673e812fb/librt-0.7.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b5b007bb22ea4b255d3ee39dfd06d12534de2fcc3438567d9f48cdaf67ae1ae3", size = 186295, upload-time = "2026-01-14T12:55:01.303Z" }, { url = "https://files.pythonhosted.org/packages/23/9b/8a9889d3df5efb67695a67785028ccd58e661c3018237b73ad081691d0cb/librt-0.7.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dbd79caaf77a3f590cbe32dc2447f718772d6eea59656a7dcb9311161b10fa75", size = 181470, upload-time = "2026-01-14T12:55:02.492Z" }, { url = "https://files.pythonhosted.org/packages/43/64/54d6ef11afca01fef8af78c230726a9394759f2addfbf7afc5e3cc032a45/librt-0.7.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:87808a8d1e0bd62a01cafc41f0fd6818b5a5d0ca0d8a55326a81643cdda8f873", size = 201713, upload-time = "2026-01-14T12:55:03.919Z" }, { url = "https://files.pythonhosted.org/packages/2d/29/73e7ed2991330b28919387656f54109139b49e19cd72902f466bd44415fd/librt-0.7.8-cp311-cp311-win32.whl", hash = "sha256:31724b93baa91512bd0a376e7cf0b59d8b631ee17923b1218a65456fa9bda2e7", size = 43803, upload-time = "2026-01-14T12:55:04.996Z" }, { url = "https://files.pythonhosted.org/packages/3f/de/66766ff48ed02b4d78deea30392ae200bcbd99ae61ba2418b49fd50a4831/librt-0.7.8-cp311-cp311-win_amd64.whl", hash = "sha256:978e8b5f13e52cf23a9e80f3286d7546baa70bc4ef35b51d97a709d0b28e537c", size = 50080, upload-time = "2026-01-14T12:55:06.489Z" }, { url = "https://files.pythonhosted.org/packages/6f/e3/33450438ff3a8c581d4ed7f798a70b07c3206d298cf0b87d3806e72e3ed8/librt-0.7.8-cp311-cp311-win_arm64.whl", hash = "sha256:20e3946863d872f7cabf7f77c6c9d370b8b3d74333d3a32471c50d3a86c0a232", size = 43383, upload-time = "2026-01-14T12:55:07.49Z" }, { url = "https://files.pythonhosted.org/packages/56/04/79d8fcb43cae376c7adbab7b2b9f65e48432c9eced62ac96703bcc16e09b/librt-0.7.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b6943885b2d49c48d0cff23b16be830ba46b0152d98f62de49e735c6e655a63", size = 57472, upload-time = "2026-01-14T12:55:08.528Z" }, { url = "https://files.pythonhosted.org/packages/b4/ba/60b96e93043d3d659da91752689023a73981336446ae82078cddf706249e/librt-0.7.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46ef1f4b9b6cc364b11eea0ecc0897314447a66029ee1e55859acb3dd8757c93", size = 58986, upload-time = "2026-01-14T12:55:09.466Z" }, { url = "https://files.pythonhosted.org/packages/7c/26/5215e4cdcc26e7be7eee21955a7e13cbf1f6d7d7311461a6014544596fac/librt-0.7.8-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:907ad09cfab21e3c86e8f1f87858f7049d1097f77196959c033612f532b4e592", size = 168422, upload-time = "2026-01-14T12:55:10.499Z" }, { url = "https://files.pythonhosted.org/packages/0f/84/e8d1bc86fa0159bfc24f3d798d92cafd3897e84c7fea7fe61b3220915d76/librt-0.7.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2991b6c3775383752b3ca0204842743256f3ad3deeb1d0adc227d56b78a9a850", size = 177478, upload-time = "2026-01-14T12:55:11.577Z" }, { url = "https://files.pythonhosted.org/packages/57/11/d0268c4b94717a18aa91df1100e767b010f87b7ae444dafaa5a2d80f33a6/librt-0.7.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03679b9856932b8c8f674e87aa3c55ea11c9274301f76ae8dc4d281bda55cf62", size = 192439, upload-time = "2026-01-14T12:55:12.7Z" }, { url = "https://files.pythonhosted.org/packages/8d/56/1e8e833b95fe684f80f8894ae4d8b7d36acc9203e60478fcae599120a975/librt-0.7.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3968762fec1b2ad34ce57458b6de25dbb4142713e9ca6279a0d352fa4e9f452b", size = 191483, upload-time = "2026-01-14T12:55:13.838Z" }, { url = "https://files.pythonhosted.org/packages/17/48/f11cf28a2cb6c31f282009e2208312aa84a5ee2732859f7856ee306176d5/librt-0.7.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bb7a7807523a31f03061288cc4ffc065d684c39db7644c676b47d89553c0d714", size = 185376, upload-time = "2026-01-14T12:55:15.017Z" }, { url = "https://files.pythonhosted.org/packages/b8/6a/d7c116c6da561b9155b184354a60a3d5cdbf08fc7f3678d09c95679d13d9/librt-0.7.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad64a14b1e56e702e19b24aae108f18ad1bf7777f3af5fcd39f87d0c5a814449", size = 206234, upload-time = "2026-01-14T12:55:16.571Z" }, { url = "https://files.pythonhosted.org/packages/61/de/1975200bb0285fc921c5981d9978ce6ce11ae6d797df815add94a5a848a3/librt-0.7.8-cp312-cp312-win32.whl", hash = "sha256:0241a6ed65e6666236ea78203a73d800dbed896cf12ae25d026d75dc1fcd1dac", size = 44057, upload-time = "2026-01-14T12:55:18.077Z" }, { url = "https://files.pythonhosted.org/packages/8e/cd/724f2d0b3461426730d4877754b65d39f06a41ac9d0a92d5c6840f72b9ae/librt-0.7.8-cp312-cp312-win_amd64.whl", hash = "sha256:6db5faf064b5bab9675c32a873436b31e01d66ca6984c6f7f92621656033a708", size = 50293, upload-time = "2026-01-14T12:55:19.179Z" }, { url = "https://files.pythonhosted.org/packages/bd/cf/7e899acd9ee5727ad8160fdcc9994954e79fab371c66535c60e13b968ffc/librt-0.7.8-cp312-cp312-win_arm64.whl", hash = "sha256:57175aa93f804d2c08d2edb7213e09276bd49097611aefc37e3fa38d1fb99ad0", size = 43574, upload-time = "2026-01-14T12:55:20.185Z" }, { url = "https://files.pythonhosted.org/packages/a1/fe/b1f9de2829cf7fc7649c1dcd202cfd873837c5cc2fc9e526b0e7f716c3d2/librt-0.7.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4c3995abbbb60b3c129490fa985dfe6cac11d88fc3c36eeb4fb1449efbbb04fc", size = 57500, upload-time = "2026-01-14T12:55:21.219Z" }, { url = "https://files.pythonhosted.org/packages/eb/d4/4a60fbe2e53b825f5d9a77325071d61cd8af8506255067bf0c8527530745/librt-0.7.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44e0c2cbc9bebd074cf2cdbe472ca185e824be4e74b1c63a8e934cea674bebf2", size = 59019, upload-time = "2026-01-14T12:55:22.256Z" }, { url = "https://files.pythonhosted.org/packages/6a/37/61ff80341ba5159afa524445f2d984c30e2821f31f7c73cf166dcafa5564/librt-0.7.8-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d2f1e492cae964b3463a03dc77a7fe8742f7855d7258c7643f0ee32b6651dd3", size = 169015, upload-time = "2026-01-14T12:55:23.24Z" }, { url = "https://files.pythonhosted.org/packages/1c/86/13d4f2d6a93f181ebf2fc953868826653ede494559da8268023fe567fca3/librt-0.7.8-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:451e7ffcef8f785831fdb791bd69211f47e95dc4c6ddff68e589058806f044c6", size = 178161, upload-time = "2026-01-14T12:55:24.826Z" }, { url = "https://files.pythonhosted.org/packages/88/26/e24ef01305954fc4d771f1f09f3dd682f9eb610e1bec188ffb719374d26e/librt-0.7.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3469e1af9f1380e093ae06bedcbdd11e407ac0b303a56bbe9afb1d6824d4982d", size = 193015, upload-time = "2026-01-14T12:55:26.04Z" }, { url = "https://files.pythonhosted.org/packages/88/a0/92b6bd060e720d7a31ed474d046a69bd55334ec05e9c446d228c4b806ae3/librt-0.7.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f11b300027ce19a34f6d24ebb0a25fd0e24a9d53353225a5c1e6cadbf2916b2e", size = 192038, upload-time = "2026-01-14T12:55:27.208Z" }, { url = "https://files.pythonhosted.org/packages/06/bb/6f4c650253704279c3a214dad188101d1b5ea23be0606628bc6739456624/librt-0.7.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4adc73614f0d3c97874f02f2c7fd2a27854e7e24ad532ea6b965459c5b757eca", size = 186006, upload-time = "2026-01-14T12:55:28.594Z" }, { url = "https://files.pythonhosted.org/packages/dc/00/1c409618248d43240cadf45f3efb866837fa77e9a12a71481912135eb481/librt-0.7.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:60c299e555f87e4c01b2eca085dfccda1dde87f5a604bb45c2906b8305819a93", size = 206888, upload-time = "2026-01-14T12:55:30.214Z" }, { url = "https://files.pythonhosted.org/packages/d9/83/b2cfe8e76ff5c1c77f8a53da3d5de62d04b5ebf7cf913e37f8bca43b5d07/librt-0.7.8-cp313-cp313-win32.whl", hash = "sha256:b09c52ed43a461994716082ee7d87618096851319bf695d57ec123f2ab708951", size = 44126, upload-time = "2026-01-14T12:55:31.44Z" }, { url = "https://files.pythonhosted.org/packages/a9/0b/c59d45de56a51bd2d3a401fc63449c0ac163e4ef7f523ea8b0c0dee86ec5/librt-0.7.8-cp313-cp313-win_amd64.whl", hash = "sha256:f8f4a901a3fa28969d6e4519deceab56c55a09d691ea7b12ca830e2fa3461e34", size = 50262, upload-time = "2026-01-14T12:55:33.01Z" }, { url = "https://files.pythonhosted.org/packages/fc/b9/973455cec0a1ec592395250c474164c4a58ebf3e0651ee920fef1a2623f1/librt-0.7.8-cp313-cp313-win_arm64.whl", hash = "sha256:43d4e71b50763fcdcf64725ac680d8cfa1706c928b844794a7aa0fa9ac8e5f09", size = 43600, upload-time = "2026-01-14T12:55:34.054Z" }, { url = "https://files.pythonhosted.org/packages/1a/73/fa8814c6ce2d49c3827829cadaa1589b0bf4391660bd4510899393a23ebc/librt-0.7.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:be927c3c94c74b05128089a955fba86501c3b544d1d300282cc1b4bd370cb418", size = 57049, upload-time = "2026-01-14T12:55:35.056Z" }, { url = "https://files.pythonhosted.org/packages/53/fe/f6c70956da23ea235fd2e3cc16f4f0b4ebdfd72252b02d1164dd58b4e6c3/librt-0.7.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7b0803e9008c62a7ef79058233db7ff6f37a9933b8f2573c05b07ddafa226611", size = 58689, upload-time = "2026-01-14T12:55:36.078Z" }, { url = "https://files.pythonhosted.org/packages/1f/4d/7a2481444ac5fba63050d9abe823e6bc16896f575bfc9c1e5068d516cdce/librt-0.7.8-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:79feb4d00b2a4e0e05c9c56df707934f41fcb5fe53fd9efb7549068d0495b758", size = 166808, upload-time = "2026-01-14T12:55:37.595Z" }, { url = "https://files.pythonhosted.org/packages/ac/3c/10901d9e18639f8953f57c8986796cfbf4c1c514844a41c9197cf87cb707/librt-0.7.8-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9122094e3f24aa759c38f46bd8863433820654927370250f460ae75488b66ea", size = 175614, upload-time = "2026-01-14T12:55:38.756Z" }, { url = "https://files.pythonhosted.org/packages/db/01/5cbdde0951a5090a80e5ba44e6357d375048123c572a23eecfb9326993a7/librt-0.7.8-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e03bea66af33c95ce3addf87a9bf1fcad8d33e757bc479957ddbc0e4f7207ac", size = 189955, upload-time = "2026-01-14T12:55:39.939Z" }, { url = "https://files.pythonhosted.org/packages/6a/b4/e80528d2f4b7eaf1d437fcbd6fc6ba4cbeb3e2a0cb9ed5a79f47c7318706/librt-0.7.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1ade7f31675db00b514b98f9ab9a7698c7282dad4be7492589109471852d398", size = 189370, upload-time = "2026-01-14T12:55:41.057Z" }, { url = "https://files.pythonhosted.org/packages/c1/ab/938368f8ce31a9787ecd4becb1e795954782e4312095daf8fd22420227c8/librt-0.7.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a14229ac62adcf1b90a15992f1ab9c69ae8b99ffb23cb64a90878a6e8a2f5b81", size = 183224, upload-time = "2026-01-14T12:55:42.328Z" }, { url = "https://files.pythonhosted.org/packages/3c/10/559c310e7a6e4014ac44867d359ef8238465fb499e7eb31b6bfe3e3f86f5/librt-0.7.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5bcaaf624fd24e6a0cb14beac37677f90793a96864c67c064a91458611446e83", size = 203541, upload-time = "2026-01-14T12:55:43.501Z" }, { url = "https://files.pythonhosted.org/packages/f8/db/a0db7acdb6290c215f343835c6efda5b491bb05c3ddc675af558f50fdba3/librt-0.7.8-cp314-cp314-win32.whl", hash = "sha256:7aa7d5457b6c542ecaed79cec4ad98534373c9757383973e638ccced0f11f46d", size = 40657, upload-time = "2026-01-14T12:55:44.668Z" }, { url = "https://files.pythonhosted.org/packages/72/e0/4f9bdc2a98a798511e81edcd6b54fe82767a715e05d1921115ac70717f6f/librt-0.7.8-cp314-cp314-win_amd64.whl", hash = "sha256:3d1322800771bee4a91f3b4bd4e49abc7d35e65166821086e5afd1e6c0d9be44", size = 46835, upload-time = "2026-01-14T12:55:45.655Z" }, { url = "https://files.pythonhosted.org/packages/f9/3d/59c6402e3dec2719655a41ad027a7371f8e2334aa794ed11533ad5f34969/librt-0.7.8-cp314-cp314-win_arm64.whl", hash = "sha256:5363427bc6a8c3b1719f8f3845ea53553d301382928a86e8fab7984426949bce", size = 39885, upload-time = "2026-01-14T12:55:47.138Z" }, { url = "https://files.pythonhosted.org/packages/4e/9c/2481d80950b83085fb14ba3c595db56330d21bbc7d88a19f20165f3538db/librt-0.7.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ca916919793a77e4a98d4a1701e345d337ce53be4a16620f063191f7322ac80f", size = 59161, upload-time = "2026-01-14T12:55:48.45Z" }, { url = "https://files.pythonhosted.org/packages/96/79/108df2cfc4e672336765d54e3ff887294c1cc36ea4335c73588875775527/librt-0.7.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:54feb7b4f2f6706bb82325e836a01be805770443e2400f706e824e91f6441dde", size = 61008, upload-time = "2026-01-14T12:55:49.527Z" }, { url = "https://files.pythonhosted.org/packages/46/f2/30179898f9994a5637459d6e169b6abdc982012c0a4b2d4c26f50c06f911/librt-0.7.8-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:39a4c76fee41007070f872b648cc2f711f9abf9a13d0c7162478043377b52c8e", size = 187199, upload-time = "2026-01-14T12:55:50.587Z" }, { url = "https://files.pythonhosted.org/packages/b4/da/f7563db55cebdc884f518ba3791ad033becc25ff68eb70902b1747dc0d70/librt-0.7.8-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac9c8a458245c7de80bc1b9765b177055efff5803f08e548dd4bb9ab9a8d789b", size = 198317, upload-time = "2026-01-14T12:55:51.991Z" }, { url = "https://files.pythonhosted.org/packages/b3/6c/4289acf076ad371471fa86718c30ae353e690d3de6167f7db36f429272f1/librt-0.7.8-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b67aa7eff150f075fda09d11f6bfb26edffd300f6ab1666759547581e8f666", size = 210334, upload-time = "2026-01-14T12:55:53.682Z" }, { url = "https://files.pythonhosted.org/packages/4a/7f/377521ac25b78ac0a5ff44127a0360ee6d5ddd3ce7327949876a30533daa/librt-0.7.8-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:535929b6eff670c593c34ff435d5440c3096f20fa72d63444608a5aef64dd581", size = 211031, upload-time = "2026-01-14T12:55:54.827Z" }, { url = "https://files.pythonhosted.org/packages/c5/b1/e1e96c3e20b23d00cf90f4aad48f0deb4cdfec2f0ed8380d0d85acf98bbf/librt-0.7.8-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:63937bd0f4d1cb56653dc7ae900d6c52c41f0015e25aaf9902481ee79943b33a", size = 204581, upload-time = "2026-01-14T12:55:56.811Z" }, { url = "https://files.pythonhosted.org/packages/43/71/0f5d010e92ed9747e14bef35e91b6580533510f1e36a8a09eb79ee70b2f0/librt-0.7.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf243da9e42d914036fd362ac3fa77d80a41cadcd11ad789b1b5eec4daaf67ca", size = 224731, upload-time = "2026-01-14T12:55:58.175Z" }, { url = "https://files.pythonhosted.org/packages/22/f0/07fb6ab5c39a4ca9af3e37554f9d42f25c464829254d72e4ebbd81da351c/librt-0.7.8-cp314-cp314t-win32.whl", hash = "sha256:171ca3a0a06c643bd0a2f62a8944e1902c94aa8e5da4db1ea9a8daf872685365", size = 41173, upload-time = "2026-01-14T12:55:59.315Z" }, { url = "https://files.pythonhosted.org/packages/24/d4/7e4be20993dc6a782639625bd2f97f3c66125c7aa80c82426956811cfccf/librt-0.7.8-cp314-cp314t-win_amd64.whl", hash = "sha256:445b7304145e24c60288a2f172b5ce2ca35c0f81605f5299f3fa567e189d2e32", size = 47668, upload-time = "2026-01-14T12:56:00.261Z" }, { url = "https://files.pythonhosted.org/packages/fc/85/69f92b2a7b3c0f88ffe107c86b952b397004b5b8ea5a81da3d9c04c04422/librt-0.7.8-cp314-cp314t-win_arm64.whl", hash = "sha256:8766ece9de08527deabcd7cb1b4f1a967a385d26e33e536d6d8913db6ef74f06", size = 40550, upload-time = "2026-01-14T12:56:01.542Z" }, ] [[package]] name = "markdown" version = "3.10.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b7/b1/af95bcae8549f1f3fd70faacb29075826a0d689a27f232e8cee315efa053/markdown-3.10.1.tar.gz", hash = "sha256:1c19c10bd5c14ac948c53d0d762a04e2fa35a6d58a6b7b1e6bfcbe6fefc0001a", size = 365402, upload-time = "2026-01-21T18:09:28.206Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/59/1b/6ef961f543593969d25b2afe57a3564200280528caa9bd1082eecdd7b3bc/markdown-3.10.1-py3-none-any.whl", hash = "sha256:867d788939fe33e4b736426f5b9f651ad0c0ae0ecf89df0ca5d1176c70812fe3", size = 107684, upload-time = "2026-01-21T18:09:27.203Z" }, ] [[package]] name = "markdown-it-py" version = "4.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, ] [[package]] name = "markupsafe" version = "3.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] [[package]] name = "mdurl" version = "0.1.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] [[package]] name = "mergedeep" version = "1.3.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, ] [[package]] name = "mike" version = "2.1.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "importlib-metadata" }, { name = "importlib-resources" }, { name = "jinja2" }, { name = "mkdocs" }, { name = "pyparsing" }, { name = "pyyaml" }, { name = "pyyaml-env-tag" }, { name = "verspec" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ab/f7/2933f1a1fb0e0f077d5d6a92c6c7f8a54e6128241f116dff4df8b6050bbf/mike-2.1.3.tar.gz", hash = "sha256:abd79b8ea483fb0275b7972825d3082e5ae67a41820f8d8a0dc7a3f49944e810", size = 38119, upload-time = "2024-08-13T05:02:14.167Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/fd/1a/31b7cd6e4e7a02df4e076162e9783620777592bea9e4bb036389389af99d/mike-2.1.3-py3-none-any.whl", hash = "sha256:d90c64077e84f06272437b464735130d380703a76a5738b152932884c60c062a", size = 33754, upload-time = "2024-08-13T05:02:12.515Z" }, ] [[package]] name = "mkdocs" version = "1.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "ghp-import" }, { name = "jinja2" }, { name = "markdown" }, { name = "markupsafe" }, { name = "mergedeep" }, { name = "mkdocs-get-deps" }, { name = "packaging" }, { name = "pathspec" }, { name = "pyyaml" }, { name = "pyyaml-env-tag" }, { name = "watchdog" }, ] sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, ] [[package]] name = "mkdocs-get-deps" version = "0.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mergedeep" }, { name = "platformdirs" }, { name = "pyyaml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239, upload-time = "2023-11-20T17:51:09.981Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521, upload-time = "2023-11-20T17:51:08.587Z" }, ] [[package]] name = "mkdocs-git-committers-plugin" version = "0.2.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mkdocs" }, { name = "pygithub" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c6/88/e724563e9ff1b119869ef0704157caa8d7b9b9b4a8a8faf8120aa01620e2/mkdocs-git-committers-plugin-0.2.3.tar.gz", hash = "sha256:77188d8aacc11d5233d6949435670e3d6545ffb7a0e274d56f32ed3984353c61", size = 5379, upload-time = "2023-11-06T17:13:30.207Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/81/56/15fdbb6afa718a6c9de626bd11672040832ba547c03daf205e295be4e0d4/mkdocs_git_committers_plugin-0.2.3-py3-none-any.whl", hash = "sha256:4ca79efb7e61a72652d3512d61af5c40a4572e36667e1a00032aad524250780d", size = 4340, upload-time = "2023-11-06T17:13:28.979Z" }, ] [[package]] name = "mkdocs-git-revision-date-localized-plugin" version = "1.4.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "babel" }, { name = "gitpython" }, { name = "mkdocs" }, { name = "pytz" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5e/f8/a17ec39a4fc314d40cc96afdc1d401e393ebd4f42309d454cc940a2cf38a/mkdocs_git_revision_date_localized_plugin-1.4.7.tar.gz", hash = "sha256:10a49eff1e1c3cb766e054b9d8360c904ce4fe8c33ac3f6cc083ac6459c91953", size = 450473, upload-time = "2025-05-28T18:26:20.697Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/53/b6/106fcc15287e7228658fbd0ad9e8b0d775becced0a089cc39984641f4a0f/mkdocs_git_revision_date_localized_plugin-1.4.7-py3-none-any.whl", hash = "sha256:056c0a90242409148f1dc94d5c9d2c25b5b8ddd8de45489fa38f7fa7ccad2bc4", size = 25382, upload-time = "2025-05-28T18:26:18.907Z" }, ] [[package]] name = "mkdocs-material" version = "9.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "babel" }, { name = "backrefs" }, { name = "colorama" }, { name = "jinja2" }, { name = "markdown" }, { name = "mkdocs" }, { name = "mkdocs-material-extensions" }, { name = "paginate" }, { name = "pygments" }, { name = "pymdown-extensions" }, { name = "requests" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9c/3b/111b84cd6ff28d9e955b5f799ef217a17bc1684ac346af333e6100e413cb/mkdocs_material-9.7.0.tar.gz", hash = "sha256:602b359844e906ee402b7ed9640340cf8a474420d02d8891451733b6b02314ec", size = 4094546, upload-time = "2025-11-11T08:49:09.73Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/04/87/eefe8d5e764f4cf50ed91b943f8e8f96b5efd65489d8303b7a36e2e79834/mkdocs_material-9.7.0-py3-none-any.whl", hash = "sha256:da2866ea53601125ff5baa8aa06404c6e07af3c5ce3d5de95e3b52b80b442887", size = 9283770, upload-time = "2025-11-11T08:49:06.26Z" }, ] [[package]] name = "mkdocs-material-extensions" version = "1.3.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, ] [[package]] name = "more-itertools" version = "10.8.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, ] [[package]] name = "msgpack" version = "1.1.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", size = 173581, upload-time = "2025-10-08T09:15:56.596Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f5/a2/3b68a9e769db68668b25c6108444a35f9bd163bb848c0650d516761a59c0/msgpack-1.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0051fffef5a37ca2cd16978ae4f0aef92f164df86823871b5162812bebecd8e2", size = 81318, upload-time = "2025-10-08T09:14:38.722Z" }, { url = "https://files.pythonhosted.org/packages/5b/e1/2b720cc341325c00be44e1ed59e7cfeae2678329fbf5aa68f5bda57fe728/msgpack-1.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a605409040f2da88676e9c9e5853b3449ba8011973616189ea5ee55ddbc5bc87", size = 83786, upload-time = "2025-10-08T09:14:40.082Z" }, { url = "https://files.pythonhosted.org/packages/71/e5/c2241de64bfceac456b140737812a2ab310b10538a7b34a1d393b748e095/msgpack-1.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b696e83c9f1532b4af884045ba7f3aa741a63b2bc22617293a2c6a7c645f251", size = 398240, upload-time = "2025-10-08T09:14:41.151Z" }, { url = "https://files.pythonhosted.org/packages/b7/09/2a06956383c0fdebaef5aa9246e2356776f12ea6f2a44bd1368abf0e46c4/msgpack-1.1.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:365c0bbe981a27d8932da71af63ef86acc59ed5c01ad929e09a0b88c6294e28a", size = 406070, upload-time = "2025-10-08T09:14:42.821Z" }, { url = "https://files.pythonhosted.org/packages/0e/74/2957703f0e1ef20637d6aead4fbb314330c26f39aa046b348c7edcf6ca6b/msgpack-1.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:41d1a5d875680166d3ac5c38573896453bbbea7092936d2e107214daf43b1d4f", size = 393403, upload-time = "2025-10-08T09:14:44.38Z" }, { url = "https://files.pythonhosted.org/packages/a5/09/3bfc12aa90f77b37322fc33e7a8a7c29ba7c8edeadfa27664451801b9860/msgpack-1.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:354e81bcdebaab427c3df4281187edc765d5d76bfb3a7c125af9da7a27e8458f", size = 398947, upload-time = "2025-10-08T09:14:45.56Z" }, { url = "https://files.pythonhosted.org/packages/4b/4f/05fcebd3b4977cb3d840f7ef6b77c51f8582086de5e642f3fefee35c86fc/msgpack-1.1.2-cp310-cp310-win32.whl", hash = "sha256:e64c8d2f5e5d5fda7b842f55dec6133260ea8f53c4257d64494c534f306bf7a9", size = 64769, upload-time = "2025-10-08T09:14:47.334Z" }, { url = "https://files.pythonhosted.org/packages/d0/3e/b4547e3a34210956382eed1c85935fff7e0f9b98be3106b3745d7dec9c5e/msgpack-1.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:db6192777d943bdaaafb6ba66d44bf65aa0e9c5616fa1d2da9bb08828c6b39aa", size = 71293, upload-time = "2025-10-08T09:14:48.665Z" }, { url = "https://files.pythonhosted.org/packages/2c/97/560d11202bcd537abca693fd85d81cebe2107ba17301de42b01ac1677b69/msgpack-1.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2e86a607e558d22985d856948c12a3fa7b42efad264dca8a3ebbcfa2735d786c", size = 82271, upload-time = "2025-10-08T09:14:49.967Z" }, { url = "https://files.pythonhosted.org/packages/83/04/28a41024ccbd67467380b6fb440ae916c1e4f25e2cd4c63abe6835ac566e/msgpack-1.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:283ae72fc89da59aa004ba147e8fc2f766647b1251500182fac0350d8af299c0", size = 84914, upload-time = "2025-10-08T09:14:50.958Z" }, { url = "https://files.pythonhosted.org/packages/71/46/b817349db6886d79e57a966346cf0902a426375aadc1e8e7a86a75e22f19/msgpack-1.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61c8aa3bd513d87c72ed0b37b53dd5c5a0f58f2ff9f26e1555d3bd7948fb7296", size = 416962, upload-time = "2025-10-08T09:14:51.997Z" }, { url = "https://files.pythonhosted.org/packages/da/e0/6cc2e852837cd6086fe7d8406af4294e66827a60a4cf60b86575a4a65ca8/msgpack-1.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:454e29e186285d2ebe65be34629fa0e8605202c60fbc7c4c650ccd41870896ef", size = 426183, upload-time = "2025-10-08T09:14:53.477Z" }, { url = "https://files.pythonhosted.org/packages/25/98/6a19f030b3d2ea906696cedd1eb251708e50a5891d0978b012cb6107234c/msgpack-1.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7bc8813f88417599564fafa59fd6f95be417179f76b40325b500b3c98409757c", size = 411454, upload-time = "2025-10-08T09:14:54.648Z" }, { url = "https://files.pythonhosted.org/packages/b7/cd/9098fcb6adb32187a70b7ecaabf6339da50553351558f37600e53a4a2a23/msgpack-1.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bafca952dc13907bdfdedfc6a5f579bf4f292bdd506fadb38389afa3ac5b208e", size = 422341, upload-time = "2025-10-08T09:14:56.328Z" }, { url = "https://files.pythonhosted.org/packages/e6/ae/270cecbcf36c1dc85ec086b33a51a4d7d08fc4f404bdbc15b582255d05ff/msgpack-1.1.2-cp311-cp311-win32.whl", hash = "sha256:602b6740e95ffc55bfb078172d279de3773d7b7db1f703b2f1323566b878b90e", size = 64747, upload-time = "2025-10-08T09:14:57.882Z" }, { url = "https://files.pythonhosted.org/packages/2a/79/309d0e637f6f37e83c711f547308b91af02b72d2326ddd860b966080ef29/msgpack-1.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:d198d275222dc54244bf3327eb8cbe00307d220241d9cec4d306d49a44e85f68", size = 71633, upload-time = "2025-10-08T09:14:59.177Z" }, { url = "https://files.pythonhosted.org/packages/73/4d/7c4e2b3d9b1106cd0aa6cb56cc57c6267f59fa8bfab7d91df5adc802c847/msgpack-1.1.2-cp311-cp311-win_arm64.whl", hash = "sha256:86f8136dfa5c116365a8a651a7d7484b65b13339731dd6faebb9a0242151c406", size = 64755, upload-time = "2025-10-08T09:15:00.48Z" }, { url = "https://files.pythonhosted.org/packages/ad/bd/8b0d01c756203fbab65d265859749860682ccd2a59594609aeec3a144efa/msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa", size = 81939, upload-time = "2025-10-08T09:15:01.472Z" }, { url = "https://files.pythonhosted.org/packages/34/68/ba4f155f793a74c1483d4bdef136e1023f7bcba557f0db4ef3db3c665cf1/msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb", size = 85064, upload-time = "2025-10-08T09:15:03.764Z" }, { url = "https://files.pythonhosted.org/packages/f2/60/a064b0345fc36c4c3d2c743c82d9100c40388d77f0b48b2f04d6041dbec1/msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f", size = 417131, upload-time = "2025-10-08T09:15:05.136Z" }, { url = "https://files.pythonhosted.org/packages/65/92/a5100f7185a800a5d29f8d14041f61475b9de465ffcc0f3b9fba606e4505/msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42", size = 427556, upload-time = "2025-10-08T09:15:06.837Z" }, { url = "https://files.pythonhosted.org/packages/f5/87/ffe21d1bf7d9991354ad93949286f643b2bb6ddbeab66373922b44c3b8cc/msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9", size = 404920, upload-time = "2025-10-08T09:15:08.179Z" }, { url = "https://files.pythonhosted.org/packages/ff/41/8543ed2b8604f7c0d89ce066f42007faac1eaa7d79a81555f206a5cdb889/msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620", size = 415013, upload-time = "2025-10-08T09:15:09.83Z" }, { url = "https://files.pythonhosted.org/packages/41/0d/2ddfaa8b7e1cee6c490d46cb0a39742b19e2481600a7a0e96537e9c22f43/msgpack-1.1.2-cp312-cp312-win32.whl", hash = "sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029", size = 65096, upload-time = "2025-10-08T09:15:11.11Z" }, { url = "https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b", size = 72708, upload-time = "2025-10-08T09:15:12.554Z" }, { url = "https://files.pythonhosted.org/packages/c5/31/5b1a1f70eb0e87d1678e9624908f86317787b536060641d6798e3cf70ace/msgpack-1.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69", size = 64119, upload-time = "2025-10-08T09:15:13.589Z" }, { url = "https://files.pythonhosted.org/packages/6b/31/b46518ecc604d7edf3a4f94cb3bf021fc62aa301f0cb849936968164ef23/msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf", size = 81212, upload-time = "2025-10-08T09:15:14.552Z" }, { url = "https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7", size = 84315, upload-time = "2025-10-08T09:15:15.543Z" }, { url = "https://files.pythonhosted.org/packages/d3/68/93180dce57f684a61a88a45ed13047558ded2be46f03acb8dec6d7c513af/msgpack-1.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999", size = 412721, upload-time = "2025-10-08T09:15:16.567Z" }, { url = "https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e", size = 424657, upload-time = "2025-10-08T09:15:17.825Z" }, { url = "https://files.pythonhosted.org/packages/38/f8/4398c46863b093252fe67368b44edc6c13b17f4e6b0e4929dbf0bdb13f23/msgpack-1.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162", size = 402668, upload-time = "2025-10-08T09:15:19.003Z" }, { url = "https://files.pythonhosted.org/packages/28/ce/698c1eff75626e4124b4d78e21cca0b4cc90043afb80a507626ea354ab52/msgpack-1.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794", size = 419040, upload-time = "2025-10-08T09:15:20.183Z" }, { url = "https://files.pythonhosted.org/packages/67/32/f3cd1667028424fa7001d82e10ee35386eea1408b93d399b09fb0aa7875f/msgpack-1.1.2-cp313-cp313-win32.whl", hash = "sha256:a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c", size = 65037, upload-time = "2025-10-08T09:15:21.416Z" }, { url = "https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9", size = 72631, upload-time = "2025-10-08T09:15:22.431Z" }, { url = "https://files.pythonhosted.org/packages/e5/db/0314e4e2db56ebcf450f277904ffd84a7988b9e5da8d0d61ab2d057df2b6/msgpack-1.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84", size = 64118, upload-time = "2025-10-08T09:15:23.402Z" }, { url = "https://files.pythonhosted.org/packages/22/71/201105712d0a2ff07b7873ed3c220292fb2ea5120603c00c4b634bcdafb3/msgpack-1.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e23ce8d5f7aa6ea6d2a2b326b4ba46c985dbb204523759984430db7114f8aa00", size = 81127, upload-time = "2025-10-08T09:15:24.408Z" }, { url = "https://files.pythonhosted.org/packages/1b/9f/38ff9e57a2eade7bf9dfee5eae17f39fc0e998658050279cbb14d97d36d9/msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939", size = 84981, upload-time = "2025-10-08T09:15:25.812Z" }, { url = "https://files.pythonhosted.org/packages/8e/a9/3536e385167b88c2cc8f4424c49e28d49a6fc35206d4a8060f136e71f94c/msgpack-1.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99e2cb7b9031568a2a5c73aa077180f93dd2e95b4f8d3b8e14a73ae94a9e667e", size = 411885, upload-time = "2025-10-08T09:15:27.22Z" }, { url = "https://files.pythonhosted.org/packages/2f/40/dc34d1a8d5f1e51fc64640b62b191684da52ca469da9cd74e84936ffa4a6/msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931", size = 419658, upload-time = "2025-10-08T09:15:28.4Z" }, { url = "https://files.pythonhosted.org/packages/3b/ef/2b92e286366500a09a67e03496ee8b8ba00562797a52f3c117aa2b29514b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:04fb995247a6e83830b62f0b07bf36540c213f6eac8e851166d8d86d83cbd014", size = 403290, upload-time = "2025-10-08T09:15:29.764Z" }, { url = "https://files.pythonhosted.org/packages/78/90/e0ea7990abea5764e4655b8177aa7c63cdfa89945b6e7641055800f6c16b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8e22ab046fa7ede9e36eeb4cfad44d46450f37bb05d5ec482b02868f451c95e2", size = 415234, upload-time = "2025-10-08T09:15:31.022Z" }, { url = "https://files.pythonhosted.org/packages/72/4e/9390aed5db983a2310818cd7d3ec0aecad45e1f7007e0cda79c79507bb0d/msgpack-1.1.2-cp314-cp314-win32.whl", hash = "sha256:80a0ff7d4abf5fecb995fcf235d4064b9a9a8a40a3ab80999e6ac1e30b702717", size = 66391, upload-time = "2025-10-08T09:15:32.265Z" }, { url = "https://files.pythonhosted.org/packages/6e/f1/abd09c2ae91228c5f3998dbd7f41353def9eac64253de3c8105efa2082f7/msgpack-1.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:9ade919fac6a3e7260b7f64cea89df6bec59104987cbea34d34a2fa15d74310b", size = 73787, upload-time = "2025-10-08T09:15:33.219Z" }, { url = "https://files.pythonhosted.org/packages/6a/b0/9d9f667ab48b16ad4115c1935d94023b82b3198064cb84a123e97f7466c1/msgpack-1.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:59415c6076b1e30e563eb732e23b994a61c159cec44deaf584e5cc1dd662f2af", size = 66453, upload-time = "2025-10-08T09:15:34.225Z" }, { url = "https://files.pythonhosted.org/packages/16/67/93f80545eb1792b61a217fa7f06d5e5cb9e0055bed867f43e2b8e012e137/msgpack-1.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:897c478140877e5307760b0ea66e0932738879e7aa68144d9b78ea4c8302a84a", size = 85264, upload-time = "2025-10-08T09:15:35.61Z" }, { url = "https://files.pythonhosted.org/packages/87/1c/33c8a24959cf193966ef11a6f6a2995a65eb066bd681fd085afd519a57ce/msgpack-1.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a668204fa43e6d02f89dbe79a30b0d67238d9ec4c5bd8a940fc3a004a47b721b", size = 89076, upload-time = "2025-10-08T09:15:36.619Z" }, { url = "https://files.pythonhosted.org/packages/fc/6b/62e85ff7193663fbea5c0254ef32f0c77134b4059f8da89b958beb7696f3/msgpack-1.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5559d03930d3aa0f3aacb4c42c776af1a2ace2611871c84a75afe436695e6245", size = 435242, upload-time = "2025-10-08T09:15:37.647Z" }, { url = "https://files.pythonhosted.org/packages/c1/47/5c74ecb4cc277cf09f64e913947871682ffa82b3b93c8dad68083112f412/msgpack-1.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70c5a7a9fea7f036b716191c29047374c10721c389c21e9ffafad04df8c52c90", size = 432509, upload-time = "2025-10-08T09:15:38.794Z" }, { url = "https://files.pythonhosted.org/packages/24/a4/e98ccdb56dc4e98c929a3f150de1799831c0a800583cde9fa022fa90602d/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2cb069d8b981abc72b41aea1c580ce92d57c673ec61af4c500153a626cb9e20", size = 415957, upload-time = "2025-10-08T09:15:40.238Z" }, { url = "https://files.pythonhosted.org/packages/da/28/6951f7fb67bc0a4e184a6b38ab71a92d9ba58080b27a77d3e2fb0be5998f/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d62ce1f483f355f61adb5433ebfd8868c5f078d1a52d042b0a998682b4fa8c27", size = 422910, upload-time = "2025-10-08T09:15:41.505Z" }, { url = "https://files.pythonhosted.org/packages/f0/03/42106dcded51f0a0b5284d3ce30a671e7bd3f7318d122b2ead66ad289fed/msgpack-1.1.2-cp314-cp314t-win32.whl", hash = "sha256:1d1418482b1ee984625d88aa9585db570180c286d942da463533b238b98b812b", size = 75197, upload-time = "2025-10-08T09:15:42.954Z" }, { url = "https://files.pythonhosted.org/packages/15/86/d0071e94987f8db59d4eeb386ddc64d0bb9b10820a8d82bcd3e53eeb2da6/msgpack-1.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5a46bf7e831d09470ad92dff02b8b1ac92175ca36b087f904a0519857c6be3ff", size = 85772, upload-time = "2025-10-08T09:15:43.954Z" }, { url = "https://files.pythonhosted.org/packages/81/f2/08ace4142eb281c12701fc3b93a10795e4d4dc7f753911d836675050f886/msgpack-1.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d99ef64f349d5ec3293688e91486c5fdb925ed03807f64d98d205d2713c60b46", size = 70868, upload-time = "2025-10-08T09:15:44.959Z" }, ] [[package]] name = "mypy" version = "1.19.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "librt" }, { name = "mypy-extensions" }, { name = "pathspec" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f9/b5/b58cdc25fadd424552804bf410855d52324183112aa004f0732c5f6324cf/mypy-1.19.0.tar.gz", hash = "sha256:f6b874ca77f733222641e5c46e4711648c4037ea13646fd0cdc814c2eaec2528", size = 3579025, upload-time = "2025-11-28T15:49:01.26Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/98/8f/55fb488c2b7dabd76e3f30c10f7ab0f6190c1fcbc3e97b1e588ec625bbe2/mypy-1.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6148ede033982a8c5ca1143de34c71836a09f105068aaa8b7d5edab2b053e6c8", size = 13093239, upload-time = "2025-11-28T15:45:11.342Z" }, { url = "https://files.pythonhosted.org/packages/72/1b/278beea978456c56b3262266274f335c3ba5ff2c8108b3b31bec1ffa4c1d/mypy-1.19.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a9ac09e52bb0f7fb912f5d2a783345c72441a08ef56ce3e17c1752af36340a39", size = 12156128, upload-time = "2025-11-28T15:46:02.566Z" }, { url = "https://files.pythonhosted.org/packages/21/f8/e06f951902e136ff74fd7a4dc4ef9d884faeb2f8eb9c49461235714f079f/mypy-1.19.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11f7254c15ab3f8ed68f8e8f5cbe88757848df793e31c36aaa4d4f9783fd08ab", size = 12753508, upload-time = "2025-11-28T15:44:47.538Z" }, { url = "https://files.pythonhosted.org/packages/67/5a/d035c534ad86e09cee274d53cf0fd769c0b29ca6ed5b32e205be3c06878c/mypy-1.19.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318ba74f75899b0e78b847d8c50821e4c9637c79d9a59680fc1259f29338cb3e", size = 13507553, upload-time = "2025-11-28T15:44:39.26Z" }, { url = "https://files.pythonhosted.org/packages/6a/17/c4a5498e00071ef29e483a01558b285d086825b61cf1fb2629fbdd019d94/mypy-1.19.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cf7d84f497f78b682edd407f14a7b6e1a2212b433eedb054e2081380b7395aa3", size = 13792898, upload-time = "2025-11-28T15:44:31.102Z" }, { url = "https://files.pythonhosted.org/packages/67/f6/bb542422b3ee4399ae1cdc463300d2d91515ab834c6233f2fd1d52fa21e0/mypy-1.19.0-cp310-cp310-win_amd64.whl", hash = "sha256:c3385246593ac2b97f155a0e9639be906e73534630f663747c71908dfbf26134", size = 10048835, upload-time = "2025-11-28T15:48:15.744Z" }, { url = "https://files.pythonhosted.org/packages/0f/d2/010fb171ae5ac4a01cc34fbacd7544531e5ace95c35ca166dd8fd1b901d0/mypy-1.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a31e4c28e8ddb042c84c5e977e28a21195d086aaffaf08b016b78e19c9ef8106", size = 13010563, upload-time = "2025-11-28T15:48:23.975Z" }, { url = "https://files.pythonhosted.org/packages/41/6b/63f095c9f1ce584fdeb595d663d49e0980c735a1d2004720ccec252c5d47/mypy-1.19.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34ec1ac66d31644f194b7c163d7f8b8434f1b49719d403a5d26c87fff7e913f7", size = 12077037, upload-time = "2025-11-28T15:47:51.582Z" }, { url = "https://files.pythonhosted.org/packages/d7/83/6cb93d289038d809023ec20eb0b48bbb1d80af40511fa077da78af6ff7c7/mypy-1.19.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb64b0ba5980466a0f3f9990d1c582bcab8db12e29815ecb57f1408d99b4bff7", size = 12680255, upload-time = "2025-11-28T15:46:57.628Z" }, { url = "https://files.pythonhosted.org/packages/99/db/d217815705987d2cbace2edd9100926196d6f85bcb9b5af05058d6e3c8ad/mypy-1.19.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:120cffe120cca5c23c03c77f84abc0c14c5d2e03736f6c312480020082f1994b", size = 13421472, upload-time = "2025-11-28T15:47:59.655Z" }, { url = "https://files.pythonhosted.org/packages/4e/51/d2beaca7c497944b07594f3f8aad8d2f0e8fc53677059848ae5d6f4d193e/mypy-1.19.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7a500ab5c444268a70565e374fc803972bfd1f09545b13418a5174e29883dab7", size = 13651823, upload-time = "2025-11-28T15:45:29.318Z" }, { url = "https://files.pythonhosted.org/packages/aa/d1/7883dcf7644db3b69490f37b51029e0870aac4a7ad34d09ceae709a3df44/mypy-1.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:c14a98bc63fd867530e8ec82f217dae29d0550c86e70debc9667fff1ec83284e", size = 10049077, upload-time = "2025-11-28T15:45:39.818Z" }, { url = "https://files.pythonhosted.org/packages/11/7e/1afa8fb188b876abeaa14460dc4983f909aaacaa4bf5718c00b2c7e0b3d5/mypy-1.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0fb3115cb8fa7c5f887c8a8d81ccdcb94cff334684980d847e5a62e926910e1d", size = 13207728, upload-time = "2025-11-28T15:46:26.463Z" }, { url = "https://files.pythonhosted.org/packages/b2/13/f103d04962bcbefb1644f5ccb235998b32c337d6c13145ea390b9da47f3e/mypy-1.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3e19e3b897562276bb331074d64c076dbdd3e79213f36eed4e592272dabd760", size = 12202945, upload-time = "2025-11-28T15:48:49.143Z" }, { url = "https://files.pythonhosted.org/packages/e4/93/a86a5608f74a22284a8ccea8592f6e270b61f95b8588951110ad797c2ddd/mypy-1.19.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9d491295825182fba01b6ffe2c6fe4e5a49dbf4e2bb4d1217b6ced3b4797bc6", size = 12718673, upload-time = "2025-11-28T15:47:37.193Z" }, { url = "https://files.pythonhosted.org/packages/3d/58/cf08fff9ced0423b858f2a7495001fda28dc058136818ee9dffc31534ea9/mypy-1.19.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6016c52ab209919b46169651b362068f632efcd5eb8ef9d1735f6f86da7853b2", size = 13608336, upload-time = "2025-11-28T15:48:32.625Z" }, { url = "https://files.pythonhosted.org/packages/64/ed/9c509105c5a6d4b73bb08733102a3ea62c25bc02c51bca85e3134bf912d3/mypy-1.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f188dcf16483b3e59f9278c4ed939ec0254aa8a60e8fc100648d9ab5ee95a431", size = 13833174, upload-time = "2025-11-28T15:45:48.091Z" }, { url = "https://files.pythonhosted.org/packages/cd/71/01939b66e35c6f8cb3e6fdf0b657f0fd24de2f8ba5e523625c8e72328208/mypy-1.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:0e3c3d1e1d62e678c339e7ade72746a9e0325de42cd2cccc51616c7b2ed1a018", size = 10112208, upload-time = "2025-11-28T15:46:41.702Z" }, { url = "https://files.pythonhosted.org/packages/cb/0d/a1357e6bb49e37ce26fcf7e3cc55679ce9f4ebee0cd8b6ee3a0e301a9210/mypy-1.19.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7686ed65dbabd24d20066f3115018d2dce030d8fa9db01aa9f0a59b6813e9f9e", size = 13191993, upload-time = "2025-11-28T15:47:22.336Z" }, { url = "https://files.pythonhosted.org/packages/5d/75/8e5d492a879ec4490e6ba664b5154e48c46c85b5ac9785792a5ec6a4d58f/mypy-1.19.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fd4a985b2e32f23bead72e2fb4bbe5d6aceee176be471243bd831d5b2644672d", size = 12174411, upload-time = "2025-11-28T15:44:55.492Z" }, { url = "https://files.pythonhosted.org/packages/71/31/ad5dcee9bfe226e8eaba777e9d9d251c292650130f0450a280aec3485370/mypy-1.19.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc51a5b864f73a3a182584b1ac75c404396a17eced54341629d8bdcb644a5bba", size = 12727751, upload-time = "2025-11-28T15:44:14.169Z" }, { url = "https://files.pythonhosted.org/packages/77/06/b6b8994ce07405f6039701f4b66e9d23f499d0b41c6dd46ec28f96d57ec3/mypy-1.19.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:37af5166f9475872034b56c5efdcf65ee25394e9e1d172907b84577120714364", size = 13593323, upload-time = "2025-11-28T15:46:34.699Z" }, { url = "https://files.pythonhosted.org/packages/68/b1/126e274484cccdf099a8e328d4fda1c7bdb98a5e888fa6010b00e1bbf330/mypy-1.19.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:510c014b722308c9bd377993bcbf9a07d7e0692e5fa8fc70e639c1eb19fc6bee", size = 13818032, upload-time = "2025-11-28T15:46:18.286Z" }, { url = "https://files.pythonhosted.org/packages/f8/56/53a8f70f562dfc466c766469133a8a4909f6c0012d83993143f2a9d48d2d/mypy-1.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:cabbee74f29aa9cd3b444ec2f1e4fa5a9d0d746ce7567a6a609e224429781f53", size = 10120644, upload-time = "2025-11-28T15:47:43.99Z" }, { url = "https://files.pythonhosted.org/packages/b0/f4/7751f32f56916f7f8c229fe902cbdba3e4dd3f3ea9e8b872be97e7fc546d/mypy-1.19.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f2e36bed3c6d9b5f35d28b63ca4b727cb0228e480826ffc8953d1892ddc8999d", size = 13185236, upload-time = "2025-11-28T15:45:20.696Z" }, { url = "https://files.pythonhosted.org/packages/35/31/871a9531f09e78e8d145032355890384f8a5b38c95a2c7732d226b93242e/mypy-1.19.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a18d8abdda14035c5718acb748faec09571432811af129bf0d9e7b2d6699bf18", size = 12213902, upload-time = "2025-11-28T15:46:10.117Z" }, { url = "https://files.pythonhosted.org/packages/58/b8/af221910dd40eeefa2077a59107e611550167b9994693fc5926a0b0f87c0/mypy-1.19.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f75e60aca3723a23511948539b0d7ed514dda194bc3755eae0bfc7a6b4887aa7", size = 12738600, upload-time = "2025-11-28T15:44:22.521Z" }, { url = "https://files.pythonhosted.org/packages/11/9f/c39e89a3e319c1d9c734dedec1183b2cc3aefbab066ec611619002abb932/mypy-1.19.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f44f2ae3c58421ee05fe609160343c25f70e3967f6e32792b5a78006a9d850f", size = 13592639, upload-time = "2025-11-28T15:48:08.55Z" }, { url = "https://files.pythonhosted.org/packages/97/6d/ffaf5f01f5e284d9033de1267e6c1b8f3783f2cf784465378a86122e884b/mypy-1.19.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63ea6a00e4bd6822adbfc75b02ab3653a17c02c4347f5bb0cf1d5b9df3a05835", size = 13799132, upload-time = "2025-11-28T15:47:06.032Z" }, { url = "https://files.pythonhosted.org/packages/fe/b0/c33921e73aaa0106224e5a34822411bea38046188eb781637f5a5b07e269/mypy-1.19.0-cp314-cp314-win_amd64.whl", hash = "sha256:3ad925b14a0bb99821ff6f734553294aa6a3440a8cb082fe1f5b84dfb662afb1", size = 10269832, upload-time = "2025-11-28T15:47:29.392Z" }, { url = "https://files.pythonhosted.org/packages/09/0e/fe228ed5aeab470c6f4eb82481837fadb642a5aa95cc8215fd2214822c10/mypy-1.19.0-py3-none-any.whl", hash = "sha256:0c01c99d626380752e527d5ce8e69ffbba2046eb8a060db0329690849cf9b6f9", size = 2469714, upload-time = "2025-11-28T15:45:33.22Z" }, ] [[package]] name = "mypy-extensions" version = "1.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] [[package]] name = "nodeenv" version = "1.10.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, ] [[package]] name = "outcome" version = "1.3.0.post0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, ] sdist = { url = "https://files.pythonhosted.org/packages/98/df/77698abfac98571e65ffeb0c1fba8ffd692ab8458d617a0eed7d9a8d38f2/outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8", size = 21060, upload-time = "2023-10-26T04:26:04.361Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/55/8b/5ab7257531a5d830fc8000c476e63c935488d74609b50f9384a643ec0a62/outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b", size = 10692, upload-time = "2023-10-26T04:26:02.532Z" }, ] [[package]] name = "packaging" version = "26.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, ] [[package]] name = "paginate" version = "0.5.7" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, ] [[package]] name = "pathspec" version = "1.0.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, ] [[package]] name = "pexpect" version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ptyprocess" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, ] [[package]] name = "platformdirs" version = "4.5.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, ] [[package]] name = "pluggy" version = "1.6.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] [[package]] name = "pprintpp" version = "0.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/06/1a/7737e7a0774da3c3824d654993cf57adc915cb04660212f03406334d8c0b/pprintpp-0.4.0.tar.gz", hash = "sha256:ea826108e2c7f49dc6d66c752973c3fc9749142a798d6b254e1e301cfdbc6403", size = 17995, upload-time = "2018-07-01T01:42:34.87Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/4e/d1/e4ed95fdd3ef13b78630280d9e9e240aeb65cc7c544ec57106149c3942fb/pprintpp-0.4.0-py2.py3-none-any.whl", hash = "sha256:b6b4dcdd0c0c0d75e4d7b2f21a9e933e5b2ce62b26e1a54537f9651ae5a5c01d", size = 16952, upload-time = "2018-07-01T01:42:36.496Z" }, ] [[package]] name = "ptyprocess" version = "0.7.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, ] [[package]] name = "pycparser" version = "3.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, ] [[package]] name = "pydantic" version = "2.12.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, { name = "pydantic-core" }, { name = "typing-extensions" }, { name = "typing-inspection" }, ] sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, ] [package.optional-dependencies] email = [ { name = "email-validator" }, ] [[package]] name = "pydantic-core" version = "2.41.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, ] [[package]] name = "pygithub" version = "2.8.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyjwt", extra = ["crypto"] }, { name = "pynacl" }, { name = "requests" }, { name = "typing-extensions" }, { name = "urllib3" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c1/74/e560bdeffea72ecb26cff27f0fad548bbff5ecc51d6a155311ea7f9e4c4c/pygithub-2.8.1.tar.gz", hash = "sha256:341b7c78521cb07324ff670afd1baa2bf5c286f8d9fd302c1798ba594a5400c9", size = 2246994, upload-time = "2025-09-02T17:41:54.674Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/07/ba/7049ce39f653f6140aac4beb53a5aaf08b4407b6a3019aae394c1c5244ff/pygithub-2.8.1-py3-none-any.whl", hash = "sha256:23a0a5bca93baef082e03411bf0ce27204c32be8bfa7abc92fe4a3e132936df0", size = 432709, upload-time = "2025-09-02T17:41:52.947Z" }, ] [[package]] name = "pygments" version = "2.19.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] [[package]] name = "pyjwt" version = "2.11.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/5c/5a/b46fa56bf322901eee5b0454a34343cdbdae202cd421775a8ee4e42fd519/pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623", size = 98019, upload-time = "2026-01-30T19:59:55.694Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/6f/01/c26ce75ba460d5cd503da9e13b21a33804d38c2165dec7b716d06b13010c/pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469", size = 28224, upload-time = "2026-01-30T19:59:54.539Z" }, ] [package.optional-dependencies] crypto = [ { name = "cryptography" }, ] [[package]] name = "pymdown-extensions" version = "10.20.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown" }, { name = "pyyaml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1e/6c/9e370934bfa30e889d12e61d0dae009991294f40055c238980066a7fbd83/pymdown_extensions-10.20.1.tar.gz", hash = "sha256:e7e39c865727338d434b55f1dd8da51febcffcaebd6e1a0b9c836243f660740a", size = 852860, upload-time = "2026-01-24T05:56:56.758Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/40/6d/b6ee155462a0156b94312bdd82d2b92ea56e909740045a87ccb98bf52405/pymdown_extensions-10.20.1-py3-none-any.whl", hash = "sha256:24af7feacbca56504b313b7b418c4f5e1317bb5fea60f03d57be7fcc40912aa0", size = 268768, upload-time = "2026-01-24T05:56:54.537Z" }, ] [[package]] name = "pynacl" version = "1.6.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d9/9a/4019b524b03a13438637b11538c82781a5eda427394380381af8f04f467a/pynacl-1.6.2.tar.gz", hash = "sha256:018494d6d696ae03c7e656e5e74cdfd8ea1326962cc401bcf018f1ed8436811c", size = 3511692, upload-time = "2026-01-01T17:48:10.851Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/4b/79/0e3c34dc3c4671f67d251c07aa8eb100916f250ee470df230b0ab89551b4/pynacl-1.6.2-cp314-cp314t-macosx_10_10_universal2.whl", hash = "sha256:622d7b07cc5c02c666795792931b50c91f3ce3c2649762efb1ef0d5684c81594", size = 390064, upload-time = "2026-01-01T17:31:57.264Z" }, { url = "https://files.pythonhosted.org/packages/eb/1c/23a26e931736e13b16483795c8a6b2f641bf6a3d5238c22b070a5112722c/pynacl-1.6.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d071c6a9a4c94d79eb665db4ce5cedc537faf74f2355e4d502591d850d3913c0", size = 809370, upload-time = "2026-01-01T17:31:59.198Z" }, { url = "https://files.pythonhosted.org/packages/87/74/8d4b718f8a22aea9e8dcc8b95deb76d4aae380e2f5b570cc70b5fd0a852d/pynacl-1.6.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe9847ca47d287af41e82be1dd5e23023d3c31a951da134121ab02e42ac218c9", size = 1408304, upload-time = "2026-01-01T17:32:01.162Z" }, { url = "https://files.pythonhosted.org/packages/fd/73/be4fdd3a6a87fe8a4553380c2b47fbd1f7f58292eb820902f5c8ac7de7b0/pynacl-1.6.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:04316d1fc625d860b6c162fff704eb8426b1a8bcd3abacea11142cbd99a6b574", size = 844871, upload-time = "2026-01-01T17:32:02.824Z" }, { url = "https://files.pythonhosted.org/packages/55/ad/6efc57ab75ee4422e96b5f2697d51bbcf6cdcc091e66310df91fbdc144a8/pynacl-1.6.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44081faff368d6c5553ccf55322ef2819abb40e25afaec7e740f159f74813634", size = 1446356, upload-time = "2026-01-01T17:32:04.452Z" }, { url = "https://files.pythonhosted.org/packages/78/b7/928ee9c4779caa0a915844311ab9fb5f99585621c5d6e4574538a17dca07/pynacl-1.6.2-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:a9f9932d8d2811ce1a8ffa79dcbdf3970e7355b5c8eb0c1a881a57e7f7d96e88", size = 826814, upload-time = "2026-01-01T17:32:06.078Z" }, { url = "https://files.pythonhosted.org/packages/f7/a9/1bdba746a2be20f8809fee75c10e3159d75864ef69c6b0dd168fc60e485d/pynacl-1.6.2-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:bc4a36b28dd72fb4845e5d8f9760610588a96d5a51f01d84d8c6ff9849968c14", size = 1411742, upload-time = "2026-01-01T17:32:07.651Z" }, { url = "https://files.pythonhosted.org/packages/f3/2f/5e7ea8d85f9f3ea5b6b87db1d8388daa3587eed181bdeb0306816fdbbe79/pynacl-1.6.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bffb6d0f6becacb6526f8f42adfb5efb26337056ee0831fb9a7044d1a964444", size = 801714, upload-time = "2026-01-01T17:32:09.558Z" }, { url = "https://files.pythonhosted.org/packages/06/ea/43fe2f7eab5f200e40fb10d305bf6f87ea31b3bbc83443eac37cd34a9e1e/pynacl-1.6.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2fef529ef3ee487ad8113d287a593fa26f48ee3620d92ecc6f1d09ea38e0709b", size = 1372257, upload-time = "2026-01-01T17:32:11.026Z" }, { url = "https://files.pythonhosted.org/packages/4d/54/c9ea116412788629b1347e415f72195c25eb2f3809b2d3e7b25f5c79f13a/pynacl-1.6.2-cp314-cp314t-win32.whl", hash = "sha256:a84bf1c20339d06dc0c85d9aea9637a24f718f375d861b2668b2f9f96fa51145", size = 231319, upload-time = "2026-01-01T17:32:12.46Z" }, { url = "https://files.pythonhosted.org/packages/ce/04/64e9d76646abac2dccf904fccba352a86e7d172647557f35b9fe2a5ee4a1/pynacl-1.6.2-cp314-cp314t-win_amd64.whl", hash = "sha256:320ef68a41c87547c91a8b58903c9caa641ab01e8512ce291085b5fe2fcb7590", size = 244044, upload-time = "2026-01-01T17:32:13.781Z" }, { url = "https://files.pythonhosted.org/packages/33/33/7873dc161c6a06f43cda13dec67b6fe152cb2f982581151956fa5e5cdb47/pynacl-1.6.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d29bfe37e20e015a7d8b23cfc8bd6aa7909c92a1b8f41ee416bbb3e79ef182b2", size = 188740, upload-time = "2026-01-01T17:32:15.083Z" }, { url = "https://files.pythonhosted.org/packages/be/7b/4845bbf88e94586ec47a432da4e9107e3fc3ce37eb412b1398630a37f7dd/pynacl-1.6.2-cp38-abi3-macosx_10_10_universal2.whl", hash = "sha256:c949ea47e4206af7c8f604b8278093b674f7c79ed0d4719cc836902bf4517465", size = 388458, upload-time = "2026-01-01T17:32:16.829Z" }, { url = "https://files.pythonhosted.org/packages/1e/b4/e927e0653ba63b02a4ca5b4d852a8d1d678afbf69b3dbf9c4d0785ac905c/pynacl-1.6.2-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8845c0631c0be43abdd865511c41eab235e0be69c81dc66a50911594198679b0", size = 800020, upload-time = "2026-01-01T17:32:18.34Z" }, { url = "https://files.pythonhosted.org/packages/7f/81/d60984052df5c97b1d24365bc1e30024379b42c4edcd79d2436b1b9806f2/pynacl-1.6.2-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22de65bb9010a725b0dac248f353bb072969c94fa8d6b1f34b87d7953cf7bbe4", size = 1399174, upload-time = "2026-01-01T17:32:20.239Z" }, { url = "https://files.pythonhosted.org/packages/68/f7/322f2f9915c4ef27d140101dd0ed26b479f7e6f5f183590fd32dfc48c4d3/pynacl-1.6.2-cp38-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46065496ab748469cdd999246d17e301b2c24ae2fdf739132e580a0e94c94a87", size = 835085, upload-time = "2026-01-01T17:32:22.24Z" }, { url = "https://files.pythonhosted.org/packages/3e/d0/f301f83ac8dbe53442c5a43f6a39016f94f754d7a9815a875b65e218a307/pynacl-1.6.2-cp38-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a66d6fb6ae7661c58995f9c6435bda2b1e68b54b598a6a10247bfcdadac996c", size = 1437614, upload-time = "2026-01-01T17:32:23.766Z" }, { url = "https://files.pythonhosted.org/packages/c4/58/fc6e649762b029315325ace1a8c6be66125e42f67416d3dbd47b69563d61/pynacl-1.6.2-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:26bfcd00dcf2cf160f122186af731ae30ab120c18e8375684ec2670dccd28130", size = 818251, upload-time = "2026-01-01T17:32:25.69Z" }, { url = "https://files.pythonhosted.org/packages/c9/a8/b917096b1accc9acd878819a49d3d84875731a41eb665f6ebc826b1af99e/pynacl-1.6.2-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c8a231e36ec2cab018c4ad4358c386e36eede0319a0c41fed24f840b1dac59f6", size = 1402859, upload-time = "2026-01-01T17:32:27.215Z" }, { url = "https://files.pythonhosted.org/packages/85/42/fe60b5f4473e12c72f977548e4028156f4d340b884c635ec6b063fe7e9a5/pynacl-1.6.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:68be3a09455743ff9505491220b64440ced8973fe930f270c8e07ccfa25b1f9e", size = 791926, upload-time = "2026-01-01T17:32:29.314Z" }, { url = "https://files.pythonhosted.org/packages/fa/f9/e40e318c604259301cc091a2a63f237d9e7b424c4851cafaea4ea7c4834e/pynacl-1.6.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8b097553b380236d51ed11356c953bf8ce36a29a3e596e934ecabe76c985a577", size = 1363101, upload-time = "2026-01-01T17:32:31.263Z" }, { url = "https://files.pythonhosted.org/packages/48/47/e761c254f410c023a469284a9bc210933e18588ca87706ae93002c05114c/pynacl-1.6.2-cp38-abi3-win32.whl", hash = "sha256:5811c72b473b2f38f7e2a3dc4f8642e3a3e9b5e7317266e4ced1fba85cae41aa", size = 227421, upload-time = "2026-01-01T17:32:33.076Z" }, { url = "https://files.pythonhosted.org/packages/41/ad/334600e8cacc7d86587fe5f565480fde569dfb487389c8e1be56ac21d8ac/pynacl-1.6.2-cp38-abi3-win_amd64.whl", hash = "sha256:62985f233210dee6548c223301b6c25440852e13d59a8b81490203c3227c5ba0", size = 239754, upload-time = "2026-01-01T17:32:34.557Z" }, { url = "https://files.pythonhosted.org/packages/29/7d/5945b5af29534641820d3bd7b00962abbbdfee84ec7e19f0d5b3175f9a31/pynacl-1.6.2-cp38-abi3-win_arm64.whl", hash = "sha256:834a43af110f743a754448463e8fd61259cd4ab5bbedcf70f9dabad1d28a394c", size = 184801, upload-time = "2026-01-01T17:32:36.309Z" }, ] [[package]] name = "pyparsing" version = "3.3.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, ] [[package]] name = "pyright" version = "1.1.404" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nodeenv" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e2/6e/026be64c43af681d5632722acd100b06d3d39f383ec382ff50a71a6d5bce/pyright-1.1.404.tar.gz", hash = "sha256:455e881a558ca6be9ecca0b30ce08aa78343ecc031d37a198ffa9a7a1abeb63e", size = 4065679, upload-time = "2025-08-20T18:46:14.029Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/84/30/89aa7f7d7a875bbb9a577d4b1dc5a3e404e3d2ae2657354808e905e358e0/pyright-1.1.404-py3-none-any.whl", hash = "sha256:c7b7ff1fdb7219c643079e4c3e7d4125f0dafcc19d253b47e898d130ea426419", size = 5902951, upload-time = "2025-08-20T18:46:12.096Z" }, ] [[package]] name = "pytest" version = "8.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, ] [[package]] name = "pytest-cov" version = "6.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage", extra = ["toml"] }, { name = "pluggy" }, { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/18/99/668cade231f434aaa59bbfbf49469068d2ddd945000621d3d165d2e7dd7b/pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2", size = 69432, upload-time = "2025-06-12T10:47:47.684Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5", size = 24644, upload-time = "2025-06-12T10:47:45.932Z" }, ] [[package]] name = "pytest-icdiff" version = "0.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "icdiff" }, { name = "pprintpp" }, { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5a/0c/66e1e2590e98f4428e374a3b6448dc086a908d15b1e24b914539d13b7ac4/pytest-icdiff-0.9.tar.gz", hash = "sha256:13aede616202e57fcc882568b64589002ef85438046f012ac30a8d959dac8b75", size = 7110, upload-time = "2023-12-05T11:18:30.192Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e2/e1/cafe1edf7a30be6fa1bbbf43f7af12b34682eadcf19eb6e9f7352062c422/pytest_icdiff-0.9-py3-none-any.whl", hash = "sha256:efee0da3bd1b24ef2d923751c5c547fbb8df0a46795553fba08ef57c3ca03d82", size = 4994, upload-time = "2023-12-05T11:18:28.572Z" }, ] [[package]] name = "python-dateutil" version = "2.9.0.post0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "six" }, ] sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] [[package]] name = "python-dotenv" version = "1.2.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, ] [[package]] name = "python-multipart" version = "0.0.22" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, ] [[package]] name = "pytz" version = "2025.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, ] [[package]] name = "pywin32-ctypes" version = "0.2.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, ] [[package]] name = "pyyaml" version = "6.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] [[package]] name = "pyyaml-env-tag" version = "1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyyaml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, ] [[package]] name = "requests" version = "2.32.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "charset-normalizer" }, { name = "idna" }, { name = "urllib3" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] [[package]] name = "rich" version = "14.3.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/74/99/a4cab2acbb884f80e558b0771e97e21e939c5dfb460f488d19df485e8298/rich-14.3.2.tar.gz", hash = "sha256:e712f11c1a562a11843306f5ed999475f09ac31ffb64281f73ab29ffdda8b3b8", size = 230143, upload-time = "2026-02-01T16:20:47.908Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ef/45/615f5babd880b4bd7d405cc0dc348234c5ffb6ed1ea33e152ede08b2072d/rich-14.3.2-py3-none-any.whl", hash = "sha256:08e67c3e90884651da3239ea668222d19bea7b589149d8014a21c633420dbb69", size = 309963, upload-time = "2026-02-01T16:20:46.078Z" }, ] [[package]] name = "rich-toolkit" version = "0.18.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "rich" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/da/f1/bcfbde3ca38db54b5dcf7ee3d0caf3ed9133a169aec5a58ad9ec50ba12e8/rich_toolkit-0.18.1.tar.gz", hash = "sha256:bf104f1945a7252debeda7d7138118eaf848fff5ea81d9eda556cbc5f911122c", size = 192514, upload-time = "2026-02-01T10:56:31.857Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/da/43/6f9860c4bfb1f181c347941542a8955ce24b228f84550253765aa1854d53/rich_toolkit-0.18.1-py3-none-any.whl", hash = "sha256:04011a9751f4c2becdf44bd1aaff8562d4b00caf04f14e483a9873c15fbe3154", size = 32255, upload-time = "2026-02-01T10:56:33.071Z" }, ] [[package]] name = "rignore" version = "0.7.6" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e5/f5/8bed2310abe4ae04b67a38374a4d311dd85220f5d8da56f47ae9361be0b0/rignore-0.7.6.tar.gz", hash = "sha256:00d3546cd793c30cb17921ce674d2c8f3a4b00501cb0e3dd0e82217dbeba2671", size = 57140, upload-time = "2025-11-05T21:41:21.968Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/86/7a/b970cd0138b0ece72eb28f086e933f9ed75b795716ad3de5ab22994b3b54/rignore-0.7.6-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:f3c74a7e5ee77aea669c95fdb3933f2a6c7549893700082e759128a29cf67e45", size = 884999, upload-time = "2025-11-05T20:42:38.373Z" }, { url = "https://files.pythonhosted.org/packages/ca/05/23faca29616d8966ada63fb0e13c214107811fa9a0aba2275e4c7ca63bd5/rignore-0.7.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b7202404958f5fe3474bac91f65350f0b1dde1a5e05089f2946549b7e91e79ec", size = 824824, upload-time = "2025-11-05T20:42:22.1Z" }, { url = "https://files.pythonhosted.org/packages/fa/2e/05a1e61f04cf2548524224f0b5f21ca19ea58f7273a863bac10846b8ff69/rignore-0.7.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bde7c5835fa3905bfb7e329a4f1d7eccb676de63da7a3f934ddd5c06df20597", size = 899121, upload-time = "2025-11-05T20:40:48.94Z" }, { url = "https://files.pythonhosted.org/packages/ff/35/71518847e10bdbf359badad8800e4681757a01f4777b3c5e03dbde8a42d8/rignore-0.7.6-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:626c3d4ba03af266694d25101bc1d8d16eda49c5feb86cedfec31c614fceca7d", size = 873813, upload-time = "2025-11-05T20:41:04.71Z" }, { url = "https://files.pythonhosted.org/packages/f6/c8/32ae405d3e7fd4d9f9b7838f2fcca0a5005bb87fa514b83f83fd81c0df22/rignore-0.7.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0a43841e651e7a05a4274b9026cc408d1912e64016ede8cd4c145dae5d0635be", size = 1168019, upload-time = "2025-11-05T20:41:20.723Z" }, { url = "https://files.pythonhosted.org/packages/25/98/013c955982bc5b4719bf9a5bea58be317eea28aa12bfd004025e3cd7c000/rignore-0.7.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7978c498dbf7f74d30cdb8859fe612167d8247f0acd377ae85180e34490725da", size = 942822, upload-time = "2025-11-05T20:41:36.99Z" }, { url = "https://files.pythonhosted.org/packages/90/fb/9a3f3156c6ed30bcd597e63690353edac1fcffe9d382ad517722b56ac195/rignore-0.7.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d22f72ab695c07d2d96d2a645208daff17084441b5d58c07378c9dd6f9c4c87", size = 959820, upload-time = "2025-11-05T20:42:06.364Z" }, { url = "https://files.pythonhosted.org/packages/5e/b2/93bf609633021e9658acaff24cfb055d8cdaf7f5855d10ebb35307900dda/rignore-0.7.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d5bd8e1a91ed1a789b2cbe39eeea9204a6719d4f2cf443a9544b521a285a295f", size = 985050, upload-time = "2025-11-05T20:41:51.124Z" }, { url = "https://files.pythonhosted.org/packages/69/bc/ec2d040469bdfd7b743df10f2201c5d285009a4263d506edbf7a06a090bb/rignore-0.7.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bc1fc03efad5789365018e94ac4079f851a999bc154d1551c45179f7fcf45322", size = 1079164, upload-time = "2025-11-05T21:40:10.368Z" }, { url = "https://files.pythonhosted.org/packages/df/26/4b635f4ea5baf4baa8ba8eee06163f6af6e76dfbe72deb57da34bb24b19d/rignore-0.7.6-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:ce2617fe28c51367fd8abfd4eeea9e61664af63c17d4ea00353d8ef56dfb95fa", size = 1139028, upload-time = "2025-11-05T21:40:27.977Z" }, { url = "https://files.pythonhosted.org/packages/6a/54/a3147ebd1e477b06eb24e2c2c56d951ae5faa9045b7b36d7892fec5080d9/rignore-0.7.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:7c4ad2cee85068408e7819a38243043214e2c3047e9bd4c506f8de01c302709e", size = 1119024, upload-time = "2025-11-05T21:40:45.148Z" }, { url = "https://files.pythonhosted.org/packages/fb/f4/27475db769a57cff18fe7e7267b36e6cdb5b1281caa185ba544171106cba/rignore-0.7.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:02cd240bfd59ecc3907766f4839cbba20530a2e470abca09eaa82225e4d946fb", size = 1128531, upload-time = "2025-11-05T21:41:02.734Z" }, { url = "https://files.pythonhosted.org/packages/97/32/6e782d3b352e4349fa0e90bf75b13cb7f11d8908b36d9e2b262224b65d9a/rignore-0.7.6-cp310-cp310-win32.whl", hash = "sha256:fe2bd8fa1ff555259df54c376abc73855cb02628a474a40d51b358c3a1ddc55b", size = 646817, upload-time = "2025-11-05T21:41:47.51Z" }, { url = "https://files.pythonhosted.org/packages/c0/8a/53185c69abb3bb362e8a46b8089999f820bf15655629ff8395107633c8ab/rignore-0.7.6-cp310-cp310-win_amd64.whl", hash = "sha256:d80afd6071c78baf3765ec698841071b19e41c326f994cfa69b5a1df676f5d39", size = 727001, upload-time = "2025-11-05T21:41:32.778Z" }, { url = "https://files.pythonhosted.org/packages/25/41/b6e2be3069ef3b7f24e35d2911bd6deb83d20ed5642ad81d5a6d1c015473/rignore-0.7.6-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:40be8226e12d6653abbebaffaea2885f80374c1c8f76fe5ca9e0cadd120a272c", size = 885285, upload-time = "2025-11-05T20:42:39.763Z" }, { url = "https://files.pythonhosted.org/packages/52/66/ba7f561b6062402022887706a7f2b2c2e2e2a28f1e3839202b0a2f77e36d/rignore-0.7.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:182f4e5e4064d947c756819446a7d4cdede8e756b8c81cf9e509683fe38778d7", size = 823882, upload-time = "2025-11-05T20:42:23.488Z" }, { url = "https://files.pythonhosted.org/packages/f5/81/4087453df35a90b07370647b19017029324950c1b9137d54bf1f33843f17/rignore-0.7.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16b63047648a916a87be1e51bb5c009063f1b8b6f5afe4f04f875525507e63dc", size = 899362, upload-time = "2025-11-05T20:40:51.111Z" }, { url = "https://files.pythonhosted.org/packages/fb/c9/390a8fdfabb76d71416be773bd9f162977bd483084f68daf19da1dec88a6/rignore-0.7.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ba5524f5178deca4d7695e936604ebc742acb8958f9395776e1fcb8133f8257a", size = 873633, upload-time = "2025-11-05T20:41:06.193Z" }, { url = "https://files.pythonhosted.org/packages/df/c9/79404fcb0faa76edfbc9df0901f8ef18568d1104919ebbbad6d608c888d1/rignore-0.7.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:62020dbb89a1dd4b84ab3d60547b3b2eb2723641d5fb198463643f71eaaed57d", size = 1167633, upload-time = "2025-11-05T20:41:22.491Z" }, { url = "https://files.pythonhosted.org/packages/6e/8d/b3466d32d445d158a0aceb80919085baaae495b1f540fb942f91d93b5e5b/rignore-0.7.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b34acd532769d5a6f153a52a98dcb81615c949ab11697ce26b2eb776af2e174d", size = 941434, upload-time = "2025-11-05T20:41:38.151Z" }, { url = "https://files.pythonhosted.org/packages/e8/40/9cd949761a7af5bc27022a939c91ff622d29c7a0b66d0c13a863097dde2d/rignore-0.7.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c5e53b752f9de44dff7b3be3c98455ce3bf88e69d6dc0cf4f213346c5e3416c", size = 959461, upload-time = "2025-11-05T20:42:08.476Z" }, { url = "https://files.pythonhosted.org/packages/b5/87/1e1a145731f73bdb7835e11f80da06f79a00d68b370d9a847de979575e6d/rignore-0.7.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:25b3536d13a5d6409ce85f23936f044576eeebf7b6db1d078051b288410fc049", size = 985323, upload-time = "2025-11-05T20:41:52.735Z" }, { url = "https://files.pythonhosted.org/packages/6c/31/1ecff992fc3f59c4fcdcb6c07d5f6c1e6dfb55ccda19c083aca9d86fa1c6/rignore-0.7.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6e01cad2b0b92f6b1993f29fc01f23f2d78caf4bf93b11096d28e9d578eb08ce", size = 1079173, upload-time = "2025-11-05T21:40:12.007Z" }, { url = "https://files.pythonhosted.org/packages/17/18/162eedadb4c2282fa4c521700dbf93c9b14b8842e8354f7d72b445b8d593/rignore-0.7.6-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5991e46ab9b4868334c9e372ab0892b0150f3f586ff2b1e314272caeb38aaedb", size = 1139012, upload-time = "2025-11-05T21:40:29.399Z" }, { url = "https://files.pythonhosted.org/packages/78/96/a9ca398a8af74bb143ad66c2a31303c894111977e28b0d0eab03867f1b43/rignore-0.7.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6c8ae562e5d1246cba5eaeb92a47b2a279e7637102828dde41dcbe291f529a3e", size = 1118827, upload-time = "2025-11-05T21:40:46.6Z" }, { url = "https://files.pythonhosted.org/packages/9f/22/1c1a65047df864def9a047dbb40bc0b580b8289a4280e62779cd61ae21f2/rignore-0.7.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:aaf938530dcc0b47c4cfa52807aa2e5bfd5ca6d57a621125fe293098692f6345", size = 1128182, upload-time = "2025-11-05T21:41:04.239Z" }, { url = "https://files.pythonhosted.org/packages/bd/f4/1526eb01fdc2235aca1fd9d0189bee4021d009a8dcb0161540238c24166e/rignore-0.7.6-cp311-cp311-win32.whl", hash = "sha256:166ebce373105dd485ec213a6a2695986346e60c94ff3d84eb532a237b24a4d5", size = 646547, upload-time = "2025-11-05T21:41:49.439Z" }, { url = "https://files.pythonhosted.org/packages/7c/c8/dda0983e1845706beb5826459781549a840fe5a7eb934abc523e8cd17814/rignore-0.7.6-cp311-cp311-win_amd64.whl", hash = "sha256:44f35ee844b1a8cea50d056e6a595190ce9d42d3cccf9f19d280ae5f3058973a", size = 727139, upload-time = "2025-11-05T21:41:34.367Z" }, { url = "https://files.pythonhosted.org/packages/e3/47/eb1206b7bf65970d41190b879e1723fc6bbdb2d45e53565f28991a8d9d96/rignore-0.7.6-cp311-cp311-win_arm64.whl", hash = "sha256:14b58f3da4fa3d5c3fa865cab49821675371f5e979281c683e131ae29159a581", size = 657598, upload-time = "2025-11-05T21:41:23.758Z" }, { url = "https://files.pythonhosted.org/packages/0b/0e/012556ef3047a2628842b44e753bb15f4dc46806780ff090f1e8fe4bf1eb/rignore-0.7.6-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:03e82348cb7234f8d9b2834f854400ddbbd04c0f8f35495119e66adbd37827a8", size = 883488, upload-time = "2025-11-05T20:42:41.359Z" }, { url = "https://files.pythonhosted.org/packages/93/b0/d4f1f3fe9eb3f8e382d45ce5b0547ea01c4b7e0b4b4eb87bcd66a1d2b888/rignore-0.7.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9e624f6be6116ea682e76c5feb71ea91255c67c86cb75befe774365b2931961", size = 820411, upload-time = "2025-11-05T20:42:24.782Z" }, { url = "https://files.pythonhosted.org/packages/4a/c8/dea564b36dedac8de21c18e1851789545bc52a0c22ece9843444d5608a6a/rignore-0.7.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bda49950d405aa8d0ebe26af807c4e662dd281d926530f03f29690a2e07d649a", size = 897821, upload-time = "2025-11-05T20:40:52.613Z" }, { url = "https://files.pythonhosted.org/packages/b3/2b/ee96db17ac1835e024c5d0742eefb7e46de60020385ac883dd3d1cde2c1f/rignore-0.7.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5fd5ab3840b8c16851d327ed06e9b8be6459702a53e5ab1fc4073b684b3789e", size = 873963, upload-time = "2025-11-05T20:41:07.49Z" }, { url = "https://files.pythonhosted.org/packages/a5/8c/ad5a57bbb9d14d5c7e5960f712a8a0b902472ea3f4a2138cbf70d1777b75/rignore-0.7.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ced2a248352636a5c77504cb755dc02c2eef9a820a44d3f33061ce1bb8a7f2d2", size = 1169216, upload-time = "2025-11-05T20:41:23.73Z" }, { url = "https://files.pythonhosted.org/packages/80/e6/5b00bc2a6bc1701e6878fca798cf5d9125eb3113193e33078b6fc0d99123/rignore-0.7.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a04a3b73b75ddc12c9c9b21efcdaab33ca3832941d6f1d67bffd860941cd448a", size = 942942, upload-time = "2025-11-05T20:41:39.393Z" }, { url = "https://files.pythonhosted.org/packages/85/e5/7f99bd0cc9818a91d0e8b9acc65b792e35750e3bdccd15a7ee75e64efca4/rignore-0.7.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d24321efac92140b7ec910ac7c53ab0f0c86a41133d2bb4b0e6a7c94967f44dd", size = 959787, upload-time = "2025-11-05T20:42:09.765Z" }, { url = "https://files.pythonhosted.org/packages/55/54/2ffea79a7c1eabcede1926347ebc2a81bc6b81f447d05b52af9af14948b9/rignore-0.7.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:73c7aa109d41e593785c55fdaa89ad80b10330affa9f9d3e3a51fa695f739b20", size = 984245, upload-time = "2025-11-05T20:41:54.062Z" }, { url = "https://files.pythonhosted.org/packages/41/f7/e80f55dfe0f35787fa482aa18689b9c8251e045076c35477deb0007b3277/rignore-0.7.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1734dc49d1e9501b07852ef44421f84d9f378da9fbeda729e77db71f49cac28b", size = 1078647, upload-time = "2025-11-05T21:40:13.463Z" }, { url = "https://files.pythonhosted.org/packages/d4/cf/2c64f0b6725149f7c6e7e5a909d14354889b4beaadddaa5fff023ec71084/rignore-0.7.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5719ea14ea2b652c0c0894be5dfde954e1853a80dea27dd2fbaa749618d837f5", size = 1139186, upload-time = "2025-11-05T21:40:31.27Z" }, { url = "https://files.pythonhosted.org/packages/75/95/a86c84909ccc24af0d094b50d54697951e576c252a4d9f21b47b52af9598/rignore-0.7.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8e23424fc7ce35726854f639cb7968151a792c0c3d9d082f7f67e0c362cfecca", size = 1117604, upload-time = "2025-11-05T21:40:48.07Z" }, { url = "https://files.pythonhosted.org/packages/7f/5e/13b249613fd5d18d58662490ab910a9f0be758981d1797789913adb4e918/rignore-0.7.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3efdcf1dd84d45f3e2bd2f93303d9be103888f56dfa7c3349b5bf4f0657ec696", size = 1127725, upload-time = "2025-11-05T21:41:05.804Z" }, { url = "https://files.pythonhosted.org/packages/c7/28/fa5dcd1e2e16982c359128664e3785f202d3eca9b22dd0b2f91c4b3d242f/rignore-0.7.6-cp312-cp312-win32.whl", hash = "sha256:ccca9d1a8b5234c76b71546fc3c134533b013f40495f394a65614a81f7387046", size = 646145, upload-time = "2025-11-05T21:41:51.096Z" }, { url = "https://files.pythonhosted.org/packages/26/87/69387fb5dd81a0f771936381431780b8cf66fcd2cfe9495e1aaf41548931/rignore-0.7.6-cp312-cp312-win_amd64.whl", hash = "sha256:c96a285e4a8bfec0652e0bfcf42b1aabcdda1e7625f5006d188e3b1c87fdb543", size = 726090, upload-time = "2025-11-05T21:41:36.485Z" }, { url = "https://files.pythonhosted.org/packages/24/5f/e8418108dcda8087fb198a6f81caadbcda9fd115d61154bf0df4d6d3619b/rignore-0.7.6-cp312-cp312-win_arm64.whl", hash = "sha256:a64a750e7a8277a323f01ca50b7784a764845f6cce2fe38831cb93f0508d0051", size = 656317, upload-time = "2025-11-05T21:41:25.305Z" }, { url = "https://files.pythonhosted.org/packages/b7/8a/a4078f6e14932ac7edb171149c481de29969d96ddee3ece5dc4c26f9e0c3/rignore-0.7.6-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:2bdab1d31ec9b4fb1331980ee49ea051c0d7f7bb6baa28b3125ef03cdc48fdaf", size = 883057, upload-time = "2025-11-05T20:42:42.741Z" }, { url = "https://files.pythonhosted.org/packages/f9/8f/f8daacd177db4bf7c2223bab41e630c52711f8af9ed279be2058d2fe4982/rignore-0.7.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:90f0a00ce0c866c275bf888271f1dc0d2140f29b82fcf33cdbda1e1a6af01010", size = 820150, upload-time = "2025-11-05T20:42:26.545Z" }, { url = "https://files.pythonhosted.org/packages/36/31/b65b837e39c3f7064c426754714ac633b66b8c2290978af9d7f513e14aa9/rignore-0.7.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1ad295537041dc2ed4b540fb1a3906bd9ede6ccdad3fe79770cd89e04e3c73c", size = 897406, upload-time = "2025-11-05T20:40:53.854Z" }, { url = "https://files.pythonhosted.org/packages/ca/58/1970ce006c427e202ac7c081435719a076c478f07b3a23f469227788dc23/rignore-0.7.6-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f782dbd3a65a5ac85adfff69e5c6b101285ef3f845c3a3cae56a54bebf9fe116", size = 874050, upload-time = "2025-11-05T20:41:08.922Z" }, { url = "https://files.pythonhosted.org/packages/d4/00/eb45db9f90137329072a732273be0d383cb7d7f50ddc8e0bceea34c1dfdf/rignore-0.7.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65cece3b36e5b0826d946494734c0e6aaf5a0337e18ff55b071438efe13d559e", size = 1167835, upload-time = "2025-11-05T20:41:24.997Z" }, { url = "https://files.pythonhosted.org/packages/f3/f1/6f1d72ddca41a64eed569680587a1236633587cc9f78136477ae69e2c88a/rignore-0.7.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d7e4bb66c13cd7602dc8931822c02dfbbd5252015c750ac5d6152b186f0a8be0", size = 941945, upload-time = "2025-11-05T20:41:40.628Z" }, { url = "https://files.pythonhosted.org/packages/48/6f/2f178af1c1a276a065f563ec1e11e7a9e23d4996fd0465516afce4b5c636/rignore-0.7.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:297e500c15766e196f68aaaa70e8b6db85fa23fdc075b880d8231fdfba738cd7", size = 959067, upload-time = "2025-11-05T20:42:11.09Z" }, { url = "https://files.pythonhosted.org/packages/5b/db/423a81c4c1e173877c7f9b5767dcaf1ab50484a94f60a0b2ed78be3fa765/rignore-0.7.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a07084211a8d35e1a5b1d32b9661a5ed20669970b369df0cf77da3adea3405de", size = 984438, upload-time = "2025-11-05T20:41:55.443Z" }, { url = "https://files.pythonhosted.org/packages/31/eb/c4f92cc3f2825d501d3c46a244a671eb737fc1bcf7b05a3ecd34abb3e0d7/rignore-0.7.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:181eb2a975a22256a1441a9d2f15eb1292839ea3f05606620bd9e1938302cf79", size = 1078365, upload-time = "2025-11-05T21:40:15.148Z" }, { url = "https://files.pythonhosted.org/packages/26/09/99442f02794bd7441bfc8ed1c7319e890449b816a7493b2db0e30af39095/rignore-0.7.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:7bbcdc52b5bf9f054b34ce4af5269df5d863d9c2456243338bc193c28022bd7b", size = 1139066, upload-time = "2025-11-05T21:40:32.771Z" }, { url = "https://files.pythonhosted.org/packages/2c/88/bcfc21e520bba975410e9419450f4b90a2ac8236b9a80fd8130e87d098af/rignore-0.7.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f2e027a6da21a7c8c0d87553c24ca5cc4364def18d146057862c23a96546238e", size = 1118036, upload-time = "2025-11-05T21:40:49.646Z" }, { url = "https://files.pythonhosted.org/packages/e2/25/d37215e4562cda5c13312636393aea0bafe38d54d4e0517520a4cc0753ec/rignore-0.7.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee4a18b82cbbc648e4aac1510066682fe62beb5dc88e2c67c53a83954e541360", size = 1127550, upload-time = "2025-11-05T21:41:07.648Z" }, { url = "https://files.pythonhosted.org/packages/dc/76/a264ab38bfa1620ec12a8ff1c07778da89e16d8c0f3450b0333020d3d6dc/rignore-0.7.6-cp313-cp313-win32.whl", hash = "sha256:a7d7148b6e5e95035d4390396895adc384d37ff4e06781a36fe573bba7c283e5", size = 646097, upload-time = "2025-11-05T21:41:53.201Z" }, { url = "https://files.pythonhosted.org/packages/62/44/3c31b8983c29ea8832b6082ddb1d07b90379c2d993bd20fce4487b71b4f4/rignore-0.7.6-cp313-cp313-win_amd64.whl", hash = "sha256:b037c4b15a64dced08fc12310ee844ec2284c4c5c1ca77bc37d0a04f7bff386e", size = 726170, upload-time = "2025-11-05T21:41:38.131Z" }, { url = "https://files.pythonhosted.org/packages/aa/41/e26a075cab83debe41a42661262f606166157df84e0e02e2d904d134c0d8/rignore-0.7.6-cp313-cp313-win_arm64.whl", hash = "sha256:e47443de9b12fe569889bdbe020abe0e0b667516ee2ab435443f6d0869bd2804", size = 656184, upload-time = "2025-11-05T21:41:27.396Z" }, { url = "https://files.pythonhosted.org/packages/9a/b9/1f5bd82b87e5550cd843ceb3768b4a8ef274eb63f29333cf2f29644b3d75/rignore-0.7.6-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:8e41be9fa8f2f47239ded8920cc283699a052ac4c371f77f5ac017ebeed75732", size = 882632, upload-time = "2025-11-05T20:42:44.063Z" }, { url = "https://files.pythonhosted.org/packages/e9/6b/07714a3efe4a8048864e8a5b7db311ba51b921e15268b17defaebf56d3db/rignore-0.7.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6dc1e171e52cefa6c20e60c05394a71165663b48bca6c7666dee4f778f2a7d90", size = 820760, upload-time = "2025-11-05T20:42:27.885Z" }, { url = "https://files.pythonhosted.org/packages/ac/0f/348c829ea2d8d596e856371b14b9092f8a5dfbb62674ec9b3f67e4939a9d/rignore-0.7.6-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ce2268837c3600f82ab8db58f5834009dc638ee17103582960da668963bebc5", size = 899044, upload-time = "2025-11-05T20:40:55.336Z" }, { url = "https://files.pythonhosted.org/packages/f0/30/2e1841a19b4dd23878d73edd5d82e998a83d5ed9570a89675f140ca8b2ad/rignore-0.7.6-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:690a3e1b54bfe77e89c4bacb13f046e642f8baadafc61d68f5a726f324a76ab6", size = 874144, upload-time = "2025-11-05T20:41:10.195Z" }, { url = "https://files.pythonhosted.org/packages/c2/bf/0ce9beb2e5f64c30e3580bef09f5829236889f01511a125f98b83169b993/rignore-0.7.6-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09d12ac7a0b6210c07bcd145007117ebd8abe99c8eeb383e9e4673910c2754b2", size = 1168062, upload-time = "2025-11-05T20:41:26.511Z" }, { url = "https://files.pythonhosted.org/packages/b9/8b/571c178414eb4014969865317da8a02ce4cf5241a41676ef91a59aab24de/rignore-0.7.6-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a2b2b74a8c60203b08452479b90e5ce3dbe96a916214bc9eb2e5af0b6a9beb0", size = 942542, upload-time = "2025-11-05T20:41:41.838Z" }, { url = "https://files.pythonhosted.org/packages/19/62/7a3cf601d5a45137a7e2b89d10c05b5b86499190c4b7ca5c3c47d79ee519/rignore-0.7.6-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fc5a531ef02131e44359419a366bfac57f773ea58f5278c2cdd915f7d10ea94", size = 958739, upload-time = "2025-11-05T20:42:12.463Z" }, { url = "https://files.pythonhosted.org/packages/5f/1f/4261f6a0d7caf2058a5cde2f5045f565ab91aa7badc972b57d19ce58b14e/rignore-0.7.6-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b7a1f77d9c4cd7e76229e252614d963442686bfe12c787a49f4fe481df49e7a9", size = 984138, upload-time = "2025-11-05T20:41:56.775Z" }, { url = "https://files.pythonhosted.org/packages/2b/bf/628dfe19c75e8ce1f45f7c248f5148b17dfa89a817f8e3552ab74c3ae812/rignore-0.7.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ead81f728682ba72b5b1c3d5846b011d3e0174da978de87c61645f2ed36659a7", size = 1079299, upload-time = "2025-11-05T21:40:16.639Z" }, { url = "https://files.pythonhosted.org/packages/af/a5/be29c50f5c0c25c637ed32db8758fdf5b901a99e08b608971cda8afb293b/rignore-0.7.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:12ffd50f520c22ffdabed8cd8bfb567d9ac165b2b854d3e679f4bcaef11a9441", size = 1139618, upload-time = "2025-11-05T21:40:34.507Z" }, { url = "https://files.pythonhosted.org/packages/2a/40/3c46cd7ce4fa05c20b525fd60f599165e820af66e66f2c371cd50644558f/rignore-0.7.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:e5a16890fbe3c894f8ca34b0fcacc2c200398d4d46ae654e03bc9b3dbf2a0a72", size = 1117626, upload-time = "2025-11-05T21:40:51.494Z" }, { url = "https://files.pythonhosted.org/packages/8c/b9/aea926f263b8a29a23c75c2e0d8447965eb1879d3feb53cfcf84db67ed58/rignore-0.7.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3abab3bf99e8a77488ef6c7c9a799fac22224c28fe9f25cc21aa7cc2b72bfc0b", size = 1128144, upload-time = "2025-11-05T21:41:09.169Z" }, { url = "https://files.pythonhosted.org/packages/a4/f6/0d6242f8d0df7f2ecbe91679fefc1f75e7cd2072cb4f497abaab3f0f8523/rignore-0.7.6-cp314-cp314-win32.whl", hash = "sha256:eeef421c1782953c4375aa32f06ecae470c1285c6381eee2a30d2e02a5633001", size = 646385, upload-time = "2025-11-05T21:41:55.105Z" }, { url = "https://files.pythonhosted.org/packages/d5/38/c0dcd7b10064f084343d6af26fe9414e46e9619c5f3224b5272e8e5d9956/rignore-0.7.6-cp314-cp314-win_amd64.whl", hash = "sha256:6aeed503b3b3d5af939b21d72a82521701a4bd3b89cd761da1e7dc78621af304", size = 725738, upload-time = "2025-11-05T21:41:39.736Z" }, { url = "https://files.pythonhosted.org/packages/d9/7a/290f868296c1ece914d565757ab363b04730a728b544beb567ceb3b2d96f/rignore-0.7.6-cp314-cp314-win_arm64.whl", hash = "sha256:104f215b60b3c984c386c3e747d6ab4376d5656478694e22c7bd2f788ddd8304", size = 656008, upload-time = "2025-11-05T21:41:29.028Z" }, { url = "https://files.pythonhosted.org/packages/ca/d2/3c74e3cd81fe8ea08a8dcd2d755c09ac2e8ad8fe409508904557b58383d3/rignore-0.7.6-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:bb24a5b947656dd94cb9e41c4bc8b23cec0c435b58be0d74a874f63c259549e8", size = 882835, upload-time = "2025-11-05T20:42:45.443Z" }, { url = "https://files.pythonhosted.org/packages/77/61/a772a34b6b63154877433ac2d048364815b24c2dd308f76b212c408101a2/rignore-0.7.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5b1e33c9501cefe24b70a1eafd9821acfd0ebf0b35c3a379430a14df089993e3", size = 820301, upload-time = "2025-11-05T20:42:29.226Z" }, { url = "https://files.pythonhosted.org/packages/71/30/054880b09c0b1b61d17eeb15279d8bf729c0ba52b36c3ada52fb827cbb3c/rignore-0.7.6-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bec3994665a44454df86deb762061e05cd4b61e3772f5b07d1882a8a0d2748d5", size = 897611, upload-time = "2025-11-05T20:40:56.475Z" }, { url = "https://files.pythonhosted.org/packages/1e/40/b2d1c169f833d69931bf232600eaa3c7998ba4f9a402e43a822dad2ea9f2/rignore-0.7.6-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26cba2edfe3cff1dfa72bddf65d316ddebf182f011f2f61538705d6dbaf54986", size = 873875, upload-time = "2025-11-05T20:41:11.561Z" }, { url = "https://files.pythonhosted.org/packages/55/59/ca5ae93d83a1a60e44b21d87deb48b177a8db1b85e82fc8a9abb24a8986d/rignore-0.7.6-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ffa86694fec604c613696cb91e43892aa22e1fec5f9870e48f111c603e5ec4e9", size = 1167245, upload-time = "2025-11-05T20:41:28.29Z" }, { url = "https://files.pythonhosted.org/packages/a5/52/cf3dce392ba2af806cba265aad6bcd9c48bb2a6cb5eee448d3319f6e505b/rignore-0.7.6-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48efe2ed95aa8104145004afb15cdfa02bea5cdde8b0344afeb0434f0d989aa2", size = 941750, upload-time = "2025-11-05T20:41:43.111Z" }, { url = "https://files.pythonhosted.org/packages/ec/be/3f344c6218d779395e785091d05396dfd8b625f6aafbe502746fcd880af2/rignore-0.7.6-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dcae43eb44b7f2457fef7cc87f103f9a0013017a6f4e62182c565e924948f21", size = 958896, upload-time = "2025-11-05T20:42:13.784Z" }, { url = "https://files.pythonhosted.org/packages/c9/34/d3fa71938aed7d00dcad87f0f9bcb02ad66c85d6ffc83ba31078ce53646a/rignore-0.7.6-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2cd649a7091c0dad2f11ef65630d30c698d505cbe8660dd395268e7c099cc99f", size = 983992, upload-time = "2025-11-05T20:41:58.022Z" }, { url = "https://files.pythonhosted.org/packages/24/a4/52a697158e9920705bdbd0748d59fa63e0f3233fb92e9df9a71afbead6ca/rignore-0.7.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42de84b0289d478d30ceb7ae59023f7b0527786a9a5b490830e080f0e4ea5aeb", size = 1078181, upload-time = "2025-11-05T21:40:18.151Z" }, { url = "https://files.pythonhosted.org/packages/ac/65/aa76dbcdabf3787a6f0fd61b5cc8ed1e88580590556d6c0207960d2384bb/rignore-0.7.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:875a617e57b53b4acbc5a91de418233849711c02e29cc1f4f9febb2f928af013", size = 1139232, upload-time = "2025-11-05T21:40:35.966Z" }, { url = "https://files.pythonhosted.org/packages/08/44/31b31a49b3233c6842acc1c0731aa1e7fb322a7170612acf30327f700b44/rignore-0.7.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8703998902771e96e49968105207719f22926e4431b108450f3f430b4e268b7c", size = 1117349, upload-time = "2025-11-05T21:40:53.013Z" }, { url = "https://files.pythonhosted.org/packages/e9/ae/1b199a2302c19c658cf74e5ee1427605234e8c91787cfba0015f2ace145b/rignore-0.7.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:602ef33f3e1b04c1e9a10a3c03f8bc3cef2d2383dcc250d309be42b49923cabc", size = 1127702, upload-time = "2025-11-05T21:41:10.881Z" }, { url = "https://files.pythonhosted.org/packages/fc/d3/18210222b37e87e36357f7b300b7d98c6dd62b133771e71ae27acba83a4f/rignore-0.7.6-cp314-cp314t-win32.whl", hash = "sha256:c1d8f117f7da0a4a96a8daef3da75bc090e3792d30b8b12cfadc240c631353f9", size = 647033, upload-time = "2025-11-05T21:42:00.095Z" }, { url = "https://files.pythonhosted.org/packages/3e/87/033eebfbee3ec7d92b3bb1717d8f68c88e6fc7de54537040f3b3a405726f/rignore-0.7.6-cp314-cp314t-win_amd64.whl", hash = "sha256:ca36e59408bec81de75d307c568c2d0d410fb880b1769be43611472c61e85c96", size = 725647, upload-time = "2025-11-05T21:41:44.449Z" }, { url = "https://files.pythonhosted.org/packages/79/62/b88e5879512c55b8ee979c666ee6902adc4ed05007226de266410ae27965/rignore-0.7.6-cp314-cp314t-win_arm64.whl", hash = "sha256:b83adabeb3e8cf662cabe1931b83e165b88c526fa6af6b3aa90429686e474896", size = 656035, upload-time = "2025-11-05T21:41:31.13Z" }, { url = "https://files.pythonhosted.org/packages/85/12/62d690b4644c330d7ac0f739b7f078190ab4308faa909a60842d0e4af5b2/rignore-0.7.6-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c3d3a523af1cd4ed2c0cba8d277a32d329b0c96ef9901fb7ca45c8cfaccf31a5", size = 887462, upload-time = "2025-11-05T20:42:50.804Z" }, { url = "https://files.pythonhosted.org/packages/05/bc/6528a0e97ed2bd7a7c329183367d1ffbc5b9762ae8348d88dae72cc9d1f5/rignore-0.7.6-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:990853566e65184a506e1e2af2d15045afad3ebaebb8859cb85b882081915110", size = 826918, upload-time = "2025-11-05T20:42:33.689Z" }, { url = "https://files.pythonhosted.org/packages/3e/2c/7d7bad116e09a04e9e1688c6f891fa2d4fd33f11b69ac0bd92419ddebeae/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cab9ff2e436ce7240d7ee301c8ef806ed77c1fd6b8a8239ff65f9bbbcb5b8a3", size = 900922, upload-time = "2025-11-05T20:41:00.361Z" }, { url = "https://files.pythonhosted.org/packages/09/ba/e5ea89fbde8e37a90ce456e31c5e9d85512cef5ae38e0f4d2426eb776a19/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d1a6671b2082c13bfd9a5cf4ce64670f832a6d41470556112c4ab0b6519b2fc4", size = 876987, upload-time = "2025-11-05T20:41:16.219Z" }, { url = "https://files.pythonhosted.org/packages/d0/fb/93d14193f0ec0c3d35b763f0a000e9780f63b2031f3d3756442c2152622d/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2468729b4c5295c199d084ab88a40afcb7c8b974276805105239c07855bbacee", size = 1171110, upload-time = "2025-11-05T20:41:32.631Z" }, { url = "https://files.pythonhosted.org/packages/9e/46/08436312ff96ffa29cfa4e1a987efc37e094531db46ba5e9fda9bb792afd/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:775710777fd71e5fdf54df69cdc249996a1d6f447a2b5bfb86dbf033fddd9cf9", size = 943339, upload-time = "2025-11-05T20:41:47.128Z" }, { url = "https://files.pythonhosted.org/packages/34/28/3b3c51328f505cfaf7e53f408f78a1e955d561135d02f9cb0341ea99f69a/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4565407f4a77f72cf9d91469e75d15d375f755f0a01236bb8aaa176278cc7085", size = 961680, upload-time = "2025-11-05T20:42:18.061Z" }, { url = "https://files.pythonhosted.org/packages/5c/9e/cbff75c8676d4f4a90bd58a1581249d255c7305141b0868f0abc0324836b/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dc44c33f8fb2d5c9da748de7a6e6653a78aa740655e7409895e94a247ffa97c8", size = 987045, upload-time = "2025-11-05T20:42:02.315Z" }, { url = "https://files.pythonhosted.org/packages/8c/25/d802d1d369502a7ddb8816059e7c79d2d913e17df975b863418e0aca4d8a/rignore-0.7.6-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:8f32478f05540513c11923e8838afab9efef0131d66dca7f67f0e1bbd118af6a", size = 1080310, upload-time = "2025-11-05T21:40:23.184Z" }, { url = "https://files.pythonhosted.org/packages/43/f0/250b785c2e473b1ab763eaf2be820934c2a5409a722e94b279dddac21c7d/rignore-0.7.6-pp310-pypy310_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:1b63a3dd76225ea35b01dd6596aa90b275b5d0f71d6dc28fce6dd295d98614aa", size = 1140998, upload-time = "2025-11-05T21:40:40.603Z" }, { url = "https://files.pythonhosted.org/packages/f5/d6/bb42fd2a8bba6aea327962656e20621fd495523259db40cfb4c5f760f05c/rignore-0.7.6-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:fe6c41175c36554a4ef0994cd1b4dbd6d73156fca779066456b781707402048e", size = 1121178, upload-time = "2025-11-05T21:40:57.585Z" }, { url = "https://files.pythonhosted.org/packages/97/f4/aeb548374129dce3dc191a4bb598c944d9ed663f467b9af830315d86059c/rignore-0.7.6-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:9a0c6792406ae36f4e7664dc772da909451d46432ff8485774526232d4885063", size = 1130190, upload-time = "2025-11-05T21:41:16.403Z" }, { url = "https://files.pythonhosted.org/packages/82/78/a6250ff0c49a3cdb943910ada4116e708118e9b901c878cfae616c80a904/rignore-0.7.6-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a20b6fb61bcced9a83dfcca6599ad45182b06ba720cff7c8d891e5b78db5b65f", size = 886470, upload-time = "2025-11-05T20:42:52.314Z" }, { url = "https://files.pythonhosted.org/packages/35/af/c69c0c51b8f9f7914d95c4ea91c29a2ac067572048cae95dd6d2efdbe05d/rignore-0.7.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:392dcabfecbe176c9ebbcb40d85a5e86a5989559c4f988c2741da7daf1b5be25", size = 825976, upload-time = "2025-11-05T20:42:35.118Z" }, { url = "https://files.pythonhosted.org/packages/f1/d2/1b264f56132264ea609d3213ab603d6a27016b19559a1a1ede1a66a03dcd/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22baa462abdc36fdd5a5e2dae423107723351b85ff093762f9261148b9d0a04a", size = 899739, upload-time = "2025-11-05T20:41:01.518Z" }, { url = "https://files.pythonhosted.org/packages/55/e4/b3c5dfdd8d8a10741dfe7199ef45d19a0e42d0c13aa377c83bd6caf65d90/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53fb28882d2538cb2d231972146c4927a9d9455e62b209f85d634408c4103538", size = 874843, upload-time = "2025-11-05T20:41:17.687Z" }, { url = "https://files.pythonhosted.org/packages/cc/10/d6f3750233881a2a154cefc9a6a0a9b19da526b19f7f08221b552c6f827d/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87409f7eeb1103d6b77f3472a3a0d9a5953e3ae804a55080bdcb0120ee43995b", size = 1170348, upload-time = "2025-11-05T20:41:34.21Z" }, { url = "https://files.pythonhosted.org/packages/6e/10/ad98ca05c9771c15af734cee18114a3c280914b6e34fde9ffea2e61e88aa/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:684014e42e4341ab3ea23a203551857fcc03a7f8ae96ca3aefb824663f55db32", size = 942315, upload-time = "2025-11-05T20:41:48.508Z" }, { url = "https://files.pythonhosted.org/packages/de/00/ab5c0f872acb60d534e687e629c17e0896c62da9b389c66d3aa16b817aa8/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77356ebb01ba13f8a425c3d30fcad40e57719c0e37670d022d560884a30e4767", size = 961047, upload-time = "2025-11-05T20:42:19.403Z" }, { url = "https://files.pythonhosted.org/packages/b8/86/3030fdc363a8f0d1cd155b4c453d6db9bab47a24fcc64d03f61d9d78fe6a/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6cbd8a48abbd3747a6c830393cd578782fab5d43f4deea48c5f5e344b8fed2b0", size = 986090, upload-time = "2025-11-05T20:42:03.581Z" }, { url = "https://files.pythonhosted.org/packages/33/b8/133aa4002cee0ebbb39362f94e4898eec7fbd09cec9fcbce1cd65b355b7f/rignore-0.7.6-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2673225dcec7f90497e79438c35e34638d0d0391ccea3cbb79bfb9adc0dc5bd7", size = 1079656, upload-time = "2025-11-05T21:40:24.89Z" }, { url = "https://files.pythonhosted.org/packages/67/56/36d5d34210e5e7dfcd134eed8335b19e80ae940ee758f493e4f2b344dd70/rignore-0.7.6-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:c081f17290d8a2b96052b79207622aa635686ea39d502b976836384ede3d303c", size = 1139789, upload-time = "2025-11-05T21:40:42.119Z" }, { url = "https://files.pythonhosted.org/packages/6b/5b/bb4f9420802bf73678033a4a55ab1bede36ce2e9b41fec5f966d83d932b3/rignore-0.7.6-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:57e8327aacc27f921968cb2a174f9e47b084ce9a7dd0122c8132d22358f6bd79", size = 1120308, upload-time = "2025-11-05T21:40:59.402Z" }, { url = "https://files.pythonhosted.org/packages/ce/8b/a1299085b28a2f6135e30370b126e3c5055b61908622f2488ade67641479/rignore-0.7.6-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:d8955b57e42f2a5434670d5aa7b75eaf6e74602ccd8955dddf7045379cd762fb", size = 1129444, upload-time = "2025-11-05T21:41:17.906Z" }, ] [[package]] name = "ruff" version = "0.14.7" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b7/5b/dd7406afa6c95e3d8fa9d652b6d6dd17dd4a6bf63cb477014e8ccd3dcd46/ruff-0.14.7.tar.gz", hash = "sha256:3417deb75d23bd14a722b57b0a1435561db65f0ad97435b4cf9f85ffcef34ae5", size = 5727324, upload-time = "2025-11-28T20:55:10.525Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8c/b1/7ea5647aaf90106f6d102230e5df874613da43d1089864da1553b899ba5e/ruff-0.14.7-py3-none-linux_armv6l.whl", hash = "sha256:b9d5cb5a176c7236892ad7224bc1e63902e4842c460a0b5210701b13e3de4fca", size = 13414475, upload-time = "2025-11-28T20:54:54.569Z" }, { url = "https://files.pythonhosted.org/packages/af/19/fddb4cd532299db9cdaf0efdc20f5c573ce9952a11cb532d3b859d6d9871/ruff-0.14.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3f64fe375aefaf36ca7d7250292141e39b4cea8250427482ae779a2aa5d90015", size = 13634613, upload-time = "2025-11-28T20:55:17.54Z" }, { url = "https://files.pythonhosted.org/packages/40/2b/469a66e821d4f3de0440676ed3e04b8e2a1dc7575cf6fa3ba6d55e3c8557/ruff-0.14.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:93e83bd3a9e1a3bda64cb771c0d47cda0e0d148165013ae2d3554d718632d554", size = 12765458, upload-time = "2025-11-28T20:55:26.128Z" }, { url = "https://files.pythonhosted.org/packages/f1/05/0b001f734fe550bcfde4ce845948ac620ff908ab7241a39a1b39bb3c5f49/ruff-0.14.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3838948e3facc59a6070795de2ae16e5786861850f78d5914a03f12659e88f94", size = 13236412, upload-time = "2025-11-28T20:55:28.602Z" }, { url = "https://files.pythonhosted.org/packages/11/36/8ed15d243f011b4e5da75cd56d6131c6766f55334d14ba31cce5461f28aa/ruff-0.14.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:24c8487194d38b6d71cd0fd17a5b6715cda29f59baca1defe1e3a03240f851d1", size = 13182949, upload-time = "2025-11-28T20:55:33.265Z" }, { url = "https://files.pythonhosted.org/packages/3b/cf/fcb0b5a195455729834f2a6eadfe2e4519d8ca08c74f6d2b564a4f18f553/ruff-0.14.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79c73db6833f058a4be8ffe4a0913b6d4ad41f6324745179bd2aa09275b01d0b", size = 13816470, upload-time = "2025-11-28T20:55:08.203Z" }, { url = "https://files.pythonhosted.org/packages/7f/5d/34a4748577ff7a5ed2f2471456740f02e86d1568a18c9faccfc73bd9ca3f/ruff-0.14.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:12eb7014fccff10fc62d15c79d8a6be4d0c2d60fe3f8e4d169a0d2def75f5dad", size = 15289621, upload-time = "2025-11-28T20:55:30.837Z" }, { url = "https://files.pythonhosted.org/packages/53/53/0a9385f047a858ba133d96f3f8e3c9c66a31cc7c4b445368ef88ebeac209/ruff-0.14.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c623bbdc902de7ff715a93fa3bb377a4e42dd696937bf95669118773dbf0c50", size = 14975817, upload-time = "2025-11-28T20:55:24.107Z" }, { url = "https://files.pythonhosted.org/packages/a8/d7/2f1c32af54c3b46e7fadbf8006d8b9bcfbea535c316b0bd8813d6fb25e5d/ruff-0.14.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f53accc02ed2d200fa621593cdb3c1ae06aa9b2c3cae70bc96f72f0000ae97a9", size = 14284549, upload-time = "2025-11-28T20:55:06.08Z" }, { url = "https://files.pythonhosted.org/packages/92/05/434ddd86becd64629c25fb6b4ce7637dd52a45cc4a4415a3008fe61c27b9/ruff-0.14.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:281f0e61a23fcdcffca210591f0f53aafaa15f9025b5b3f9706879aaa8683bc4", size = 14071389, upload-time = "2025-11-28T20:55:35.617Z" }, { url = "https://files.pythonhosted.org/packages/ff/50/fdf89d4d80f7f9d4f420d26089a79b3bb1538fe44586b148451bc2ba8d9c/ruff-0.14.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:dbbaa5e14148965b91cb090236931182ee522a5fac9bc5575bafc5c07b9f9682", size = 14202679, upload-time = "2025-11-28T20:55:01.472Z" }, { url = "https://files.pythonhosted.org/packages/77/54/87b34988984555425ce967f08a36df0ebd339bb5d9d0e92a47e41151eafc/ruff-0.14.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1464b6e54880c0fe2f2d6eaefb6db15373331414eddf89d6b903767ae2458143", size = 13147677, upload-time = "2025-11-28T20:55:19.933Z" }, { url = "https://files.pythonhosted.org/packages/67/29/f55e4d44edfe053918a16a3299e758e1c18eef216b7a7092550d7a9ec51c/ruff-0.14.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f217ed871e4621ea6128460df57b19ce0580606c23aeab50f5de425d05226784", size = 13151392, upload-time = "2025-11-28T20:55:21.967Z" }, { url = "https://files.pythonhosted.org/packages/36/69/47aae6dbd4f1d9b4f7085f4d9dcc84e04561ee7ad067bf52e0f9b02e3209/ruff-0.14.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6be02e849440ed3602d2eb478ff7ff07d53e3758f7948a2a598829660988619e", size = 13412230, upload-time = "2025-11-28T20:55:12.749Z" }, { url = "https://files.pythonhosted.org/packages/b7/4b/6e96cb6ba297f2ba502a231cd732ed7c3de98b1a896671b932a5eefa3804/ruff-0.14.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:19a0f116ee5e2b468dfe80c41c84e2bbd6b74f7b719bee86c2ecde0a34563bcc", size = 14195397, upload-time = "2025-11-28T20:54:56.896Z" }, { url = "https://files.pythonhosted.org/packages/69/82/251d5f1aa4dcad30aed491b4657cecd9fb4274214da6960ffec144c260f7/ruff-0.14.7-py3-none-win32.whl", hash = "sha256:e33052c9199b347c8937937163b9b149ef6ab2e4bb37b042e593da2e6f6cccfa", size = 13126751, upload-time = "2025-11-28T20:55:03.47Z" }, { url = "https://files.pythonhosted.org/packages/a8/b5/d0b7d145963136b564806f6584647af45ab98946660d399ec4da79cae036/ruff-0.14.7-py3-none-win_amd64.whl", hash = "sha256:e17a20ad0d3fad47a326d773a042b924d3ac31c6ca6deb6c72e9e6b5f661a7c6", size = 14531726, upload-time = "2025-11-28T20:54:59.121Z" }, { url = "https://files.pythonhosted.org/packages/1d/d2/1637f4360ada6a368d3265bf39f2cf737a0aaab15ab520fc005903e883f8/ruff-0.14.7-py3-none-win_arm64.whl", hash = "sha256:be4d653d3bea1b19742fcc6502354e32f65cd61ff2fbdb365803ef2c2aec6228", size = 13609215, upload-time = "2025-11-28T20:55:15.375Z" }, ] [[package]] name = "secretstorage" version = "3.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "jeepney" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, ] [[package]] name = "sentry-sdk" version = "2.52.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] sdist = { url = "https://files.pythonhosted.org/packages/59/eb/1b497650eb564701f9a7b8a95c51b2abe9347ed2c0b290ba78f027ebe4ea/sentry_sdk-2.52.0.tar.gz", hash = "sha256:fa0bec872cfec0302970b2996825723d67390cdd5f0229fb9efed93bd5384899", size = 410273, upload-time = "2026-02-04T15:03:54.706Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ca/63/2c6daf59d86b1c30600bff679d039f57fd1932af82c43c0bde1cbc55e8d4/sentry_sdk-2.52.0-py2.py3-none-any.whl", hash = "sha256:931c8f86169fc6f2752cb5c4e6480f0d516112e78750c312e081ababecbaf2ed", size = 435547, upload-time = "2026-02-04T15:03:51.567Z" }, ] [[package]] name = "shellingham" version = "1.5.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, ] [[package]] name = "six" version = "1.17.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] [[package]] name = "smmap" version = "5.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329, upload-time = "2025-01-02T07:14:40.909Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload-time = "2025-01-02T07:14:38.724Z" }, ] [[package]] name = "sniffio" version = "1.3.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, ] [[package]] name = "sortedcontainers" version = "2.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, ] [[package]] name = "starlette" version = "0.48.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a7/a5/d6f429d43394057b67a6b5bbe6eae2f77a6bf7459d961fdb224bf206eee6/starlette-0.48.0.tar.gz", hash = "sha256:7e8cee469a8ab2352911528110ce9088fdc6a37d9876926e73da7ce4aa4c7a46", size = 2652949, upload-time = "2025-09-13T08:41:05.699Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/be/72/2db2f49247d0a18b4f1bb9a5a39a0162869acf235f3a96418363947b3d46/starlette-0.48.0-py3-none-any.whl", hash = "sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659", size = 73736, upload-time = "2025-09-13T08:41:03.869Z" }, ] [[package]] name = "time-machine" version = "2.19.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "python-dateutil" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f8/a4/1b5fdd165f61b67f445fac2a7feb0c655118edef429cd09ff5a8067f7f1d/time_machine-2.19.0.tar.gz", hash = "sha256:7c5065a8b3f2bbb449422c66ef71d114d3f909c276a6469642ecfffb6a0fcd29", size = 14576, upload-time = "2025-08-19T17:22:08.402Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9d/8f/19125611ebbcb3a14da14cd982b9eb4573e2733db60c9f1fbf6a39534f40/time_machine-2.19.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b5169018ef47206997b46086ce01881cd3a4666fd2998c9d76a87858ca3e49e9", size = 19659, upload-time = "2025-08-19T17:20:30.062Z" }, { url = "https://files.pythonhosted.org/packages/74/da/9b0a928321e7822a3ff96dbd1eae089883848e30e9e1b149b85fb96ba56b/time_machine-2.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:85bb7ed440fccf6f6d0c8f7d68d849e7c3d1f771d5e0b2cdf871fa6561da569f", size = 15157, upload-time = "2025-08-19T17:20:31.931Z" }, { url = "https://files.pythonhosted.org/packages/36/ff/d7e943422038f5f2161fe2c2d791e64a45be691ef946020b20f3a6efc4d4/time_machine-2.19.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a3b12028af1cdc09ccd595be2168b7b26f206c1e190090b048598fbe278beb8e", size = 32860, upload-time = "2025-08-19T17:20:33.241Z" }, { url = "https://files.pythonhosted.org/packages/fc/80/2b0f1070ed9808ee7da7a6da62a4a0b776957cb4d861578348f86446e778/time_machine-2.19.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c261f073086cf081d1443cbf7684148c662659d3d139d06b772bfe3fe7cc71a6", size = 34510, upload-time = "2025-08-19T17:20:34.221Z" }, { url = "https://files.pythonhosted.org/packages/ef/b4/48038691c8d89924b36c83335a73adeeb68c884f5a1da08a5b17b8a956f3/time_machine-2.19.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:011954d951230a9f1079f22b39ed1a3a9abb50ee297dfb8c557c46351659d94d", size = 36204, upload-time = "2025-08-19T17:20:35.163Z" }, { url = "https://files.pythonhosted.org/packages/37/2e/60e8adb541df195e83cb74b720b2cfb1f22ed99c5a7f8abf2a9ab3442cb5/time_machine-2.19.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b0f83308b29c7872006803f2e77318874eb84d0654f2afe0e48e3822e7a2e39b", size = 34936, upload-time = "2025-08-19T17:20:36.61Z" }, { url = "https://files.pythonhosted.org/packages/5e/72/e8cee59c6cd99dd3b25b8001a0253e779a286aa8f44d5b40777cbd66210b/time_machine-2.19.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:39733ef844e2984620ec9382a42d00cccc4757d75a5dd572be8c2572e86e50b9", size = 32932, upload-time = "2025-08-19T17:20:37.901Z" }, { url = "https://files.pythonhosted.org/packages/2c/eb/83f300d93c1504965d944e03679f1c943a923bce2d0fdfadef0e2e22cc13/time_machine-2.19.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f8db99f6334432e9ffbf00c215caf2ae9773f17cec08304d77e9e90febc3507b", size = 34010, upload-time = "2025-08-19T17:20:39.202Z" }, { url = "https://files.pythonhosted.org/packages/e1/77/f35f2500e04daac5033a22fbfd17e68467822b8406ee77966bf222ccaa26/time_machine-2.19.0-cp310-cp310-win32.whl", hash = "sha256:72bf66cd19e27ffd26516b9cbe676d50c2e0b026153289765dfe0cf406708128", size = 17121, upload-time = "2025-08-19T17:20:40.108Z" }, { url = "https://files.pythonhosted.org/packages/db/df/32d3e0404be1760a64a44caab2af34b07e952bfe00a23134fea9ddba3e8a/time_machine-2.19.0-cp310-cp310-win_amd64.whl", hash = "sha256:46f1c945934ce3d6b4f388b8e581fce7f87ec891ea90d7128e19520e434f96f0", size = 17957, upload-time = "2025-08-19T17:20:41.079Z" }, { url = "https://files.pythonhosted.org/packages/66/df/598a71a1afb4b509a4587273b76590b16d9110a3e9106f01eedc68d02bb2/time_machine-2.19.0-cp310-cp310-win_arm64.whl", hash = "sha256:fb4897c7a5120a4fd03f0670f332d83b7e55645886cd8864a71944c4c2e5b35b", size = 16821, upload-time = "2025-08-19T17:20:41.967Z" }, { url = "https://files.pythonhosted.org/packages/1d/ed/4815ebcc9b6c14273f692b9be38a9b09eae52a7e532407cc61a51912b121/time_machine-2.19.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5ee91664880434d98e41585c3446dac7180ec408c786347451ddfca110d19296", size = 19342, upload-time = "2025-08-19T17:20:43.207Z" }, { url = "https://files.pythonhosted.org/packages/ee/08/154cce8b11b60d8238b0b751b8901d369999f4e8f7c3a5f917caa5d95b0b/time_machine-2.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ed3732b83a893d1c7b8cabde762968b4dc5680ee0d305b3ecca9bb516f4e3862", size = 14978, upload-time = "2025-08-19T17:20:44.134Z" }, { url = "https://files.pythonhosted.org/packages/c7/b7/b689d8c8eeca7af375cfcd64973e49e83aa817cc00f80f98548d42c0eb50/time_machine-2.19.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6ba0303e9cc9f7f947e344f501e26bedfb68fab521e3c2729d370f4f332d2d55", size = 30964, upload-time = "2025-08-19T17:20:45.366Z" }, { url = "https://files.pythonhosted.org/packages/80/91/38bf9c79674e95ce32e23c267055f281dff651eec77ed32a677db3dc011a/time_machine-2.19.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2851825b524a988ee459c37c1c26bdfaa7eff78194efb2b562ea497a6f375b0a", size = 32606, upload-time = "2025-08-19T17:20:46.693Z" }, { url = "https://files.pythonhosted.org/packages/19/4a/e9222d85d4de68975a5e799f539a9d32f3a134a9101fca0a61fa6aa33d68/time_machine-2.19.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68d32b09ecfd7fef59255c091e8e7c24dd117f882c4880b5c7ab8c5c32a98f89", size = 34405, upload-time = "2025-08-19T17:20:48.032Z" }, { url = "https://files.pythonhosted.org/packages/14/e2/09480d608d42d6876f9ff74593cfc9197a7eb2c31381a74fb2b145575b65/time_machine-2.19.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:60c46ab527bf2fa144b530f639cc9e12803524c9e1f111dc8c8f493bb6586eeb", size = 33181, upload-time = "2025-08-19T17:20:48.937Z" }, { url = "https://files.pythonhosted.org/packages/84/64/f9359e000fad32d9066305c48abc527241d608bcdf77c19d67d66e268455/time_machine-2.19.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:56f26ab9f0201c453d18fe76bb7d1cf05fe58c1b9d9cb0c7d243d05132e01292", size = 31036, upload-time = "2025-08-19T17:20:50.276Z" }, { url = "https://files.pythonhosted.org/packages/71/0d/fab2aacec71e3e482bd7fce0589381f9414a4a97f8766bddad04ad047b7b/time_machine-2.19.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6c806cf3c1185baa1d807b7f51bed0db7a6506832c961d5d1b4c94c775749bc0", size = 32145, upload-time = "2025-08-19T17:20:51.449Z" }, { url = "https://files.pythonhosted.org/packages/44/fb/faeba2405fb27553f7b28db441a500e2064ffdb2dcba001ee315fdd2c121/time_machine-2.19.0-cp311-cp311-win32.whl", hash = "sha256:b30039dfd89855c12138095bee39c540b4633cbc3684580d684ef67a99a91587", size = 17004, upload-time = "2025-08-19T17:20:52.38Z" }, { url = "https://files.pythonhosted.org/packages/2f/84/87e483d660ca669426192969280366635c845c3154a9fe750be546ed3afc/time_machine-2.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:13ed8b34430f1de79905877f5600adffa626793ab4546a70a99fb72c6a3350d8", size = 17822, upload-time = "2025-08-19T17:20:53.348Z" }, { url = "https://files.pythonhosted.org/packages/41/f4/ebf7bbf5047854a528adaf54a5e8780bc5f7f0104c298ab44566a3053bf8/time_machine-2.19.0-cp311-cp311-win_arm64.whl", hash = "sha256:cc29a50a0257d8750b08056b66d7225daab47606832dea1a69e8b017323bf511", size = 16680, upload-time = "2025-08-19T17:20:54.26Z" }, { url = "https://files.pythonhosted.org/packages/9b/aa/7e00614d339e4d687f6e96e312a1566022528427d237ec639df66c4547bc/time_machine-2.19.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c85cf437dc3c07429456d8d6670ac90ecbd8241dcd0fbf03e8db2800576f91ff", size = 19308, upload-time = "2025-08-19T17:20:55.25Z" }, { url = "https://files.pythonhosted.org/packages/ab/3c/bde3c757394f5bca2fbc1528d4117960a26c38f9b160bf471b38d2378d8f/time_machine-2.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d9238897e8ef54acdf59f5dff16f59ca0720e7c02d820c56b4397c11db5d3eb9", size = 15019, upload-time = "2025-08-19T17:20:56.204Z" }, { url = "https://files.pythonhosted.org/packages/c8/e0/8ca916dd918018352d377f1f5226ee071cfbeb7dbbde2b03d14a411ac2b1/time_machine-2.19.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e312c7d5d6bfffb96c6a7b39ff29e3046de100d7efaa3c01552654cfbd08f14c", size = 33079, upload-time = "2025-08-19T17:20:57.166Z" }, { url = "https://files.pythonhosted.org/packages/48/69/184a0209f02dd0cb5e01e8d13cd4c97a5f389c4e3d09b95160dd676ad1e7/time_machine-2.19.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:714c40b2c90d1c57cc403382d5a9cf16e504cb525bfe9650095317da3c3d62b5", size = 34925, upload-time = "2025-08-19T17:20:58.117Z" }, { url = "https://files.pythonhosted.org/packages/43/42/4bbf4309e8e57cea1086eb99052d97ff6ddecc1ab6a3b07aa4512f8bf963/time_machine-2.19.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2eaa1c675d500dc3ccae19e9fb1feff84458a68c132bbea47a80cc3dd2df7072", size = 36384, upload-time = "2025-08-19T17:20:59.108Z" }, { url = "https://files.pythonhosted.org/packages/b1/af/9f510dc1719157348c1a2e87423aed406589070b54b503cb237d9bf3a4fe/time_machine-2.19.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e77a414e9597988af53b2b2e67242c9d2f409769df0d264b6d06fda8ca3360d4", size = 34881, upload-time = "2025-08-19T17:21:00.116Z" }, { url = "https://files.pythonhosted.org/packages/ca/28/61764a635c70cc76c76ba582dfdc1a84834cddaeb96789023af5214426b2/time_machine-2.19.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cd93996970e11c382b04d4937c3cd0b0167adeef14725ece35aae88d8a01733c", size = 32931, upload-time = "2025-08-19T17:21:01.095Z" }, { url = "https://files.pythonhosted.org/packages/b6/e0/f028d93b266e6ade8aca5851f76ebbc605b2905cdc29981a2943b43e1a6c/time_machine-2.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8e20a6d8d6e23174bd7e931e134d9610b136db460b249d07e84ecdad029ec352", size = 34241, upload-time = "2025-08-19T17:21:02.052Z" }, { url = "https://files.pythonhosted.org/packages/7d/a6/36d1950ed1d3f613158024cf1dcc73db1d9ef0b9117cf51ef2e37dc06499/time_machine-2.19.0-cp312-cp312-win32.whl", hash = "sha256:95afc9bc65228b27be80c2756799c20b8eb97c4ef382a9b762b6d7888bc84099", size = 17021, upload-time = "2025-08-19T17:21:03.374Z" }, { url = "https://files.pythonhosted.org/packages/b1/0d/e2dce93355abda3cac69e77fe96566757e98b8fe7fdcbddce89c9ced3f5f/time_machine-2.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:e84909af950e2448f4e2562ea5759c946248c99ab380d2b47d79b62bd76fa236", size = 17857, upload-time = "2025-08-19T17:21:04.331Z" }, { url = "https://files.pythonhosted.org/packages/eb/28/50ae6fb83b7feeeca7a461c0dc156cf7ef5e6ef594a600d06634fde6a2cb/time_machine-2.19.0-cp312-cp312-win_arm64.whl", hash = "sha256:0390a1ea9fa7e9d772a39b7c61b34fdcca80eb9ffac339cc0441c6c714c81470", size = 16677, upload-time = "2025-08-19T17:21:05.39Z" }, { url = "https://files.pythonhosted.org/packages/a9/b8/24ebce67aa531bae2cbe164bb3f4abc6467dc31f3aead35e77f5a075ea3e/time_machine-2.19.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5e172866753e6041d3b29f3037dc47c20525176a494a71bbd0998dfdc4f11f2f", size = 19373, upload-time = "2025-08-19T17:21:06.701Z" }, { url = "https://files.pythonhosted.org/packages/53/a5/c9a5240fd2f845d3ff9fa26f8c8eaa29f7239af9d65007e61d212250f15b/time_machine-2.19.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f70f68379bd6f542ae6775cce9a4fa3dcc20bf7959c42eaef871c14469e18097", size = 15056, upload-time = "2025-08-19T17:21:07.667Z" }, { url = "https://files.pythonhosted.org/packages/b9/92/66cce5d2fb2a5e68459aca85fd18a7e2d216f725988940cd83f96630f2f1/time_machine-2.19.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e69e0b0f694728a00e72891ef8dd00c7542952cb1c87237db594b6b27d504a96", size = 33172, upload-time = "2025-08-19T17:21:08.619Z" }, { url = "https://files.pythonhosted.org/packages/ae/20/b499e9ab4364cd466016c33dcdf4f56629ca4c20b865bd4196d229f31d92/time_machine-2.19.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3ae0a8b869574301ec5637e32c270c7384cca5cd6e230f07af9d29271a7fa293", size = 35042, upload-time = "2025-08-19T17:21:09.622Z" }, { url = "https://files.pythonhosted.org/packages/41/32/b252d3d32791eb16c07d553c820dbc33d9c7fa771de3d1c602190bded2b7/time_machine-2.19.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:554e4317de90e2f7605ff80d153c8bb56b38c0d0c0279feb17e799521e987b8c", size = 36535, upload-time = "2025-08-19T17:21:10.571Z" }, { url = "https://files.pythonhosted.org/packages/98/cf/4d0470062b9742e1b040ab81bad04d1a5d1de09806507bb6188989cfa1a7/time_machine-2.19.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6567a5ec5538ed550539ac29be11b3cb36af1f9894e2a72940cba0292cc7c3c9", size = 34945, upload-time = "2025-08-19T17:21:11.538Z" }, { url = "https://files.pythonhosted.org/packages/24/71/2f741b29d98b1c18f6777a32236497c3d3264b6077e431cea4695684c8a1/time_machine-2.19.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:82e9ffe8dfff07b0d810a2ad015a82cd78c6a237f6c7cf185fa7f747a3256f8a", size = 33014, upload-time = "2025-08-19T17:21:12.858Z" }, { url = "https://files.pythonhosted.org/packages/e8/83/ca8dba6106562843fd99f672e5aaf95badbc10f4f13f7cfe8d8640a7019d/time_machine-2.19.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7e1c4e578cdd69b3531d8dd3fbcb92a0cd879dadb912ee37af99c3a9e3c0d285", size = 34350, upload-time = "2025-08-19T17:21:13.923Z" }, { url = "https://files.pythonhosted.org/packages/21/7f/34fe540450e18d0a993240100e4b86e8d03d831b92af8bb6ddb2662dc6fc/time_machine-2.19.0-cp313-cp313-win32.whl", hash = "sha256:72dbd4cbc3d96dec9dd281ddfbb513982102776b63e4e039f83afb244802a9e5", size = 17047, upload-time = "2025-08-19T17:21:14.874Z" }, { url = "https://files.pythonhosted.org/packages/bf/5d/c8be73df82c7ebe7cd133279670e89b8b110af3ce1412c551caa9d08e625/time_machine-2.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:e17e3e089ac95f9a145ce07ff615e3c85674f7de36f2d92aaf588493a23ffb4b", size = 17868, upload-time = "2025-08-19T17:21:15.819Z" }, { url = "https://files.pythonhosted.org/packages/92/13/2dfd3b8fb285308f61cd7aa9bfa96f46ddf916e3549a0f0afd094c556599/time_machine-2.19.0-cp313-cp313-win_arm64.whl", hash = "sha256:149072aff8e3690e14f4916103d898ea0d5d9c95531b6aa0995251c299533f7b", size = 16710, upload-time = "2025-08-19T17:21:16.748Z" }, { url = "https://files.pythonhosted.org/packages/05/c1/deebb361727d2c5790f9d4d874be1b19afd41f4375581df465e6718b46a2/time_machine-2.19.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f3589fee1ed0ab6ee424a55b0ea1ec694c4ba64cc26895bcd7d99f3d1bc6a28a", size = 20053, upload-time = "2025-08-19T17:21:17.704Z" }, { url = "https://files.pythonhosted.org/packages/45/e8/fe3376951e6118d8ec1d1f94066a169b791424fe4a26c7dfc069b153ee08/time_machine-2.19.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7887e85275c4975fe54df03dcdd5f38bd36be973adc68a8c77e17441c3b443d6", size = 15423, upload-time = "2025-08-19T17:21:18.668Z" }, { url = "https://files.pythonhosted.org/packages/9c/c7/f88d95cd1a87c650cf3749b4d64afdaf580297aa18ad7f4b44ec9d252dfc/time_machine-2.19.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ce0be294c209928563fcce1c587963e60ec803436cf1e181acd5bc1e425d554b", size = 39630, upload-time = "2025-08-19T17:21:19.645Z" }, { url = "https://files.pythonhosted.org/packages/cc/5d/65a5c48a65357e56ec6f032972e4abd1c02d4fca4b0717a3aaefd19014d4/time_machine-2.19.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a62fd1ab380012c86f4c042010418ed45eb31604f4bf4453e17c9fa60bc56a29", size = 41242, upload-time = "2025-08-19T17:21:20.979Z" }, { url = "https://files.pythonhosted.org/packages/f6/f9/fe5209e1615fde0a8cad6c4e857157b150333ed1fe31a7632b08cfe0ebdd/time_machine-2.19.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b25ec853a4530a5800731257f93206b12cbdee85ede964ebf8011b66086a7914", size = 44278, upload-time = "2025-08-19T17:21:21.984Z" }, { url = "https://files.pythonhosted.org/packages/4a/3a/a5e5fe9c5d614cde0a9387ff35e8dfd12c5ef6384e4c1a21b04e6e0b905d/time_machine-2.19.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a430e4d0e0556f021a9c78e9b9f68e5e8910bdace4aa34ed4d1a73e239ed9384", size = 42321, upload-time = "2025-08-19T17:21:23.755Z" }, { url = "https://files.pythonhosted.org/packages/a1/c5/56eca774e9162bc1ce59111d2bd69140dc8908c9478c92ec7bd15d547600/time_machine-2.19.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2415b7495ec4364c8067071e964fbadfe746dd4cdb43983f2f0bd6ebed13315c", size = 39270, upload-time = "2025-08-19T17:21:26.009Z" }, { url = "https://files.pythonhosted.org/packages/9b/69/5dd0c420667578169a12acc8c8fd7452e8cfb181e41c9b4ac7e88fa36686/time_machine-2.19.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dbfc6b90c10f288594e1bf89a728a98cc0030791fd73541bbdc6b090aff83143", size = 40193, upload-time = "2025-08-19T17:21:27.054Z" }, { url = "https://files.pythonhosted.org/packages/75/a7/de974d421bd55c9355583427c2a38fb0237bb5fd6614af492ba89dacb2f9/time_machine-2.19.0-cp313-cp313t-win32.whl", hash = "sha256:16f5d81f650c0a4d117ab08036dc30b5f8b262e11a4a0becc458e7f1c011b228", size = 17542, upload-time = "2025-08-19T17:21:28.674Z" }, { url = "https://files.pythonhosted.org/packages/76/0a/aa0d05becd5d06ae8d3f16d657dc8cc9400c8d79aef80299de196467ff12/time_machine-2.19.0-cp313-cp313t-win_amd64.whl", hash = "sha256:645699616ec14e147094f601e6ab9553ff6cea37fad9c42720a6d7ed04bcd5dc", size = 18703, upload-time = "2025-08-19T17:21:29.663Z" }, { url = "https://files.pythonhosted.org/packages/1f/c0/f785a4c7c73aa176510f7c48b84b49c26be84af0d534deb222e0327f750e/time_machine-2.19.0-cp313-cp313t-win_arm64.whl", hash = "sha256:b32daa965d13237536ea3afaa5ad61ade2b2d9314bc3a20196a0d2e1d7b57c6a", size = 17020, upload-time = "2025-08-19T17:21:30.653Z" }, { url = "https://files.pythonhosted.org/packages/ed/97/c5fb51def06c0b2b6735332ad118ab35b4d9b85368792e5b638e99b1b686/time_machine-2.19.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:31cb43c8fd2d961f31bed0ff4e0026964d2b35e5de9e0fabbfecf756906d3612", size = 19360, upload-time = "2025-08-19T17:21:31.94Z" }, { url = "https://files.pythonhosted.org/packages/2d/4e/2d795f7d6b7f5205ffe737a05bb1cf19d8038233b797062b2ef412b8512b/time_machine-2.19.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:bdf481a75afc6bff3e520db594501975b652f7def21cd1de6aa971d35ba644e6", size = 15033, upload-time = "2025-08-19T17:21:32.934Z" }, { url = "https://files.pythonhosted.org/packages/dd/32/9bad501e360b4e758c58fae616ca5f8c7ad974b343f2463a15b2bf77a366/time_machine-2.19.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:00bee4bb950ac6a08d62af78e4da0cf2b4fc2abf0de2320d0431bf610db06e7c", size = 33379, upload-time = "2025-08-19T17:21:33.925Z" }, { url = "https://files.pythonhosted.org/packages/a3/45/eda0ca4d793dfd162478d6163759b1c6ce7f6e61daa7fd7d62b31f21f87f/time_machine-2.19.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9f02199490906582302ce09edd32394fb393271674c75d7aa76c7a3245f16003", size = 35123, upload-time = "2025-08-19T17:21:34.945Z" }, { url = "https://files.pythonhosted.org/packages/f0/5a/97e16325442ae5731fcaac794f0a1ef9980eff8a5491e58201d7eb814a34/time_machine-2.19.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e35726c7ba625f844c13b1fc0d4f81f394eefaee1d3a094a9093251521f2ef15", size = 36588, upload-time = "2025-08-19T17:21:35.975Z" }, { url = "https://files.pythonhosted.org/packages/e8/9d/bf0b2ccc930cc4a316f26f1c78d3f313cd0fa13bb7480369b730a8f129db/time_machine-2.19.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:304315023999cd401ff02698870932b893369e1cfeb2248d09f6490507a92e97", size = 35013, upload-time = "2025-08-19T17:21:37.017Z" }, { url = "https://files.pythonhosted.org/packages/f0/5a/39ac6a3078174f9715d88364871348b249631f12e76de1b862433b3f8862/time_machine-2.19.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9765d4f003f263ea8bfd90d2d15447ca4b3dfa181922cf6cf808923b02ac180a", size = 33303, upload-time = "2025-08-19T17:21:38.352Z" }, { url = "https://files.pythonhosted.org/packages/b3/ac/d8646baf9f95f2e792a6d7a7b35e92fca253c4a992afff801beafae0e5c2/time_machine-2.19.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7837ef3fd5911eb9b480909bb93d922737b6bdecea99dfcedb0a03807de9b2d3", size = 34440, upload-time = "2025-08-19T17:21:39.382Z" }, { url = "https://files.pythonhosted.org/packages/ce/8b/8b6568c5ae966d80ead03ab537be3c6acf2af06fb501c2d466a3162c6295/time_machine-2.19.0-cp314-cp314-win32.whl", hash = "sha256:4bb5bd43b1bdfac3007b920b51d8e761f024ed465cfeec63ac4296922a4ec428", size = 17162, upload-time = "2025-08-19T17:21:40.381Z" }, { url = "https://files.pythonhosted.org/packages/46/a5/211c1ab4566eba5308b2dc001b6349e3a032e3f6afa67ca2f27ea6b27af5/time_machine-2.19.0-cp314-cp314-win_amd64.whl", hash = "sha256:f583bbd0aa8ab4a7c45a684bf636d9e042d466e30bcbae1d13e7541e2cbe7207", size = 18040, upload-time = "2025-08-19T17:21:41.363Z" }, { url = "https://files.pythonhosted.org/packages/b8/fc/4c2fb705f6371cb83824da45a8b967514a922fc092a0ef53979334d97a70/time_machine-2.19.0-cp314-cp314-win_arm64.whl", hash = "sha256:f379c6f8a6575a8284592179cf528ce89373f060301323edcc44f1fa1d37be12", size = 16752, upload-time = "2025-08-19T17:21:42.336Z" }, { url = "https://files.pythonhosted.org/packages/79/ab/6437d18f31c666b5116c97572a282ac2590a82a0a9867746a6647eaf4613/time_machine-2.19.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:a3b8981f9c663b0906b05ab4d0ca211fae4b63b47c6ec26de5374fe56c836162", size = 20057, upload-time = "2025-08-19T17:21:43.35Z" }, { url = "https://files.pythonhosted.org/packages/6c/a2/e03639ec2ba7200328bbcad8a2b2b1d5fccca9cceb9481b164a1cabdcb33/time_machine-2.19.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8e9c6363893e7f52c226afbebb23e825259222d100e67dfd24c8a6d35f1a1907", size = 15430, upload-time = "2025-08-19T17:21:44.725Z" }, { url = "https://files.pythonhosted.org/packages/5d/ff/39e63a48e840f3e36ce24846ee51dd99c6dba635659b1750a2993771e88e/time_machine-2.19.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:206fcd6c9a6f00cac83db446ad1effc530a8cec244d2780af62db3a2d0a9871b", size = 39622, upload-time = "2025-08-19T17:21:45.821Z" }, { url = "https://files.pythonhosted.org/packages/9a/2e/ee5ac79c4954768705801e54817c7d58e07e25a0bb227e775f501f3e2122/time_machine-2.19.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bf33016a1403c123373ffaeff25e26e69d63bf2c63b6163932efed94160db7ef", size = 41235, upload-time = "2025-08-19T17:21:46.783Z" }, { url = "https://files.pythonhosted.org/packages/3a/3e/9af5f39525e779185c77285b8bbae15340eeeaa0afb33d458bc8b47d459b/time_machine-2.19.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9247c4bb9bbd3ff584ef4efbdec8efd9f37aa08bcfc4728bde1e489c2cb445bd", size = 44276, upload-time = "2025-08-19T17:21:47.759Z" }, { url = "https://files.pythonhosted.org/packages/59/fe/572c7443cc27140bbeae3947279bbd4a120f9e8622253a20637f260b7813/time_machine-2.19.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:77f9bb0b86758d1f2d9352642c874946ad5815df53ef4ca22eb9d532179fe50d", size = 42330, upload-time = "2025-08-19T17:21:48.881Z" }, { url = "https://files.pythonhosted.org/packages/cf/24/1a81c2e08ee7dae13ec8ceed27a29afa980c3d63852e42f1e023bf0faa03/time_machine-2.19.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0b529e262df3b9c449f427385f4d98250828c879168c2e00eec844439f40b370", size = 39281, upload-time = "2025-08-19T17:21:49.907Z" }, { url = "https://files.pythonhosted.org/packages/d2/60/6f0d6e5108978ca1a2a4ffb4d1c7e176d9199bb109fd44efe2680c60b52a/time_machine-2.19.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9199246e31cdc810e5d89cb71d09144c4d745960fdb0824da4994d152aca3303", size = 40201, upload-time = "2025-08-19T17:21:50.953Z" }, { url = "https://files.pythonhosted.org/packages/73/b9/3ea4951e8293b0643feb98c0b9a176fa822154f1810835db3f282968ab10/time_machine-2.19.0-cp314-cp314t-win32.whl", hash = "sha256:0fe81bae55b7aefc2c2a34eb552aa82e6c61a86b3353a3c70df79b9698cb02ca", size = 17743, upload-time = "2025-08-19T17:21:51.948Z" }, { url = "https://files.pythonhosted.org/packages/e4/8b/cd802884ca8a98e2b6cdc2397d57dd12ff8a7d1481e06fc3fad3d4e7e5ff/time_machine-2.19.0-cp314-cp314t-win_amd64.whl", hash = "sha256:7253791b8d7e7399fbeed7a8193cb01bc004242864306288797056badbdaf80b", size = 18956, upload-time = "2025-08-19T17:21:52.997Z" }, { url = "https://files.pythonhosted.org/packages/c6/49/cabb1593896082fd55e34768029b8b0ca23c9be8b2dc127e0fc14796d33e/time_machine-2.19.0-cp314-cp314t-win_arm64.whl", hash = "sha256:536bd1ac31ab06a1522e7bf287602188f502dc19d122b1502c4f60b1e8efac79", size = 17068, upload-time = "2025-08-19T17:21:54.064Z" }, ] [[package]] name = "tomli" version = "2.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, ] [[package]] name = "tomli-w" version = "1.2.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184, upload-time = "2025-01-15T12:07:24.262Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" }, ] [[package]] name = "tomlkit" version = "0.14.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c3/af/14b24e41977adb296d6bd1fb59402cf7d60ce364f90c890bd2ec65c43b5a/tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064", size = 187167, upload-time = "2026-01-13T01:14:53.304Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b5/11/87d6d29fb5d237229d67973a6c9e06e048f01cf4994dee194ab0ea841814/tomlkit-0.14.0-py3-none-any.whl", hash = "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680", size = 39310, upload-time = "2026-01-13T01:14:51.965Z" }, ] [[package]] name = "trio" version = "0.31.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "cffi", marker = "implementation_name != 'pypy' and os_name == 'nt'" }, { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "idna" }, { name = "outcome" }, { name = "sniffio" }, { name = "sortedcontainers" }, ] sdist = { url = "https://files.pythonhosted.org/packages/76/8f/c6e36dd11201e2a565977d8b13f0b027ba4593c1a80bed5185489178e257/trio-0.31.0.tar.gz", hash = "sha256:f71d551ccaa79d0cb73017a33ef3264fde8335728eb4c6391451fe5d253a9d5b", size = 605825, upload-time = "2025-09-09T15:17:15.242Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/31/5b/94237a3485620dbff9741df02ff6d8acaa5fdec67d81ab3f62e4d8511bf7/trio-0.31.0-py3-none-any.whl", hash = "sha256:b5d14cd6293d79298b49c3485ffd9c07e3ce03a6da8c7dfbe0cb3dd7dc9a4774", size = 512679, upload-time = "2025-09-09T15:17:13.821Z" }, ] [[package]] name = "trove-classifiers" version = "2026.1.14.14" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d8/43/7935f8ea93fcb6680bc10a6fdbf534075c198eeead59150dd5ed68449642/trove_classifiers-2026.1.14.14.tar.gz", hash = "sha256:00492545a1402b09d4858605ba190ea33243d361e2b01c9c296ce06b5c3325f3", size = 16997, upload-time = "2026-01-14T14:54:50.526Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/bb/4a/2e5583e544bc437d5e8e54b47db87430df9031b29b48d17f26d129fa60c0/trove_classifiers-2026.1.14.14-py3-none-any.whl", hash = "sha256:1f9553927f18d0513d8e5ff80ab8980b8202ce37ecae0e3274ed2ef11880e74d", size = 14197, upload-time = "2026-01-14T14:54:49.067Z" }, ] [[package]] name = "typer" version = "0.21.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "rich" }, { name = "shellingham" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/36/bf/8825b5929afd84d0dabd606c67cd57b8388cb3ec385f7ef19c5cc2202069/typer-0.21.1.tar.gz", hash = "sha256:ea835607cd752343b6b2b7ce676893e5a0324082268b48f27aa058bdb7d2145d", size = 110371, upload-time = "2026-01-06T11:21:10.989Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a0/1d/d9257dd49ff2ca23ea5f132edf1281a0c4f9de8a762b9ae399b670a59235/typer-0.21.1-py3-none-any.whl", hash = "sha256:7985e89081c636b88d172c2ee0cfe33c253160994d47bdfdc302defd7d1f1d01", size = 47381, upload-time = "2026-01-06T11:21:09.824Z" }, ] [[package]] name = "types-awscrt" version = "0.31.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/97/be/589b7bba42b5681a72bac4d714287afef4e1bb84d07c859610ff631d449e/types_awscrt-0.31.1.tar.gz", hash = "sha256:08b13494f93f45c1a92eb264755fce50ed0d1dc75059abb5e31670feb9a09724", size = 17839, upload-time = "2026-01-16T02:01:23.394Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5e/fd/ddca80617f230bd833f99b4fb959abebffd8651f520493cae2e96276b1bd/types_awscrt-0.31.1-py3-none-any.whl", hash = "sha256:7e4364ac635f72bd57f52b093883640b1448a6eded0ecbac6e900bf4b1e4777b", size = 42516, upload-time = "2026-01-16T02:01:21.637Z" }, ] [[package]] name = "types-boto3" version = "1.41.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore-stubs" }, { name = "types-s3transfer" }, { name = "typing-extensions", marker = "python_full_version < '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ab/d1/ace522f5b8f9c0e04bcee550ec02a9a3e007bf534738e294a095b83a1b2a/types_boto3-1.41.5.tar.gz", hash = "sha256:d37e3615f2319404407efc8dbdf10111c74c9e47296d3e0d691d2b95742254bd", size = 100416, upload-time = "2025-11-26T20:33:01.152Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/73/46/a4a65258a3fcee5208dc5a30f48d493487b059b6eba12c2d5b72fe5aa17f/types_boto3-1.41.5-py3-none-any.whl", hash = "sha256:c0e129bfd62b2ae58c53285c7a4895127f1bdfe7921d558d3c8525cf387d6d0c", size = 69198, upload-time = "2025-11-26T20:32:55.932Z" }, ] [[package]] name = "types-pyyaml" version = "6.0.12.20250915" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7e/69/3c51b36d04da19b92f9e815be12753125bd8bc247ba0470a982e6979e71c/types_pyyaml-6.0.12.20250915.tar.gz", hash = "sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3", size = 17522, upload-time = "2025-09-15T03:01:00.728Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/bd/e0/1eed384f02555dde685fff1a1ac805c1c7dcb6dd019c916fe659b1c1f9ec/types_pyyaml-6.0.12.20250915-py3-none-any.whl", hash = "sha256:e7d4d9e064e89a3b3cae120b4990cd370874d2bf12fa5f46c97018dd5d3c9ab6", size = 20338, upload-time = "2025-09-15T03:00:59.218Z" }, ] [[package]] name = "types-requests" version = "2.31.0.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "types-urllib3" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f9/b8/c1e8d39996b4929b918aba10dba5de07a8b3f4c8487bb61bb79882544e69/types-requests-2.31.0.6.tar.gz", hash = "sha256:cd74ce3b53c461f1228a9b783929ac73a666658f223e28ed29753771477b3bd0", size = 15535, upload-time = "2023-09-27T06:19:38.443Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5c/a1/6f8dc74d9069e790d604ddae70cb46dcbac668f1bb08136e7b0f2f5cd3bf/types_requests-2.31.0.6-py3-none-any.whl", hash = "sha256:a2db9cb228a81da8348b49ad6db3f5519452dd20a9c1e1a868c83c5fe88fd1a9", size = 14516, upload-time = "2023-09-27T06:19:36.373Z" }, ] [[package]] name = "types-s3transfer" version = "0.16.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/fe/64/42689150509eb3e6e82b33ee3d89045de1592488842ddf23c56957786d05/types_s3transfer-0.16.0.tar.gz", hash = "sha256:b4636472024c5e2b62278c5b759661efeb52a81851cde5f092f24100b1ecb443", size = 13557, upload-time = "2025-12-08T08:13:09.928Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/98/27/e88220fe6274eccd3bdf95d9382918716d312f6f6cef6a46332d1ee2feff/types_s3transfer-0.16.0-py3-none-any.whl", hash = "sha256:1c0cd111ecf6e21437cb410f5cddb631bfb2263b77ad973e79b9c6d0cb24e0ef", size = 19247, upload-time = "2025-12-08T08:13:08.426Z" }, ] [[package]] name = "types-urllib3" version = "1.26.25.14" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/73/de/b9d7a68ad39092368fb21dd6194b362b98a1daeea5dcfef5e1adb5031c7e/types-urllib3-1.26.25.14.tar.gz", hash = "sha256:229b7f577c951b8c1b92c1bc2b2fdb0b49847bd2af6d1cc2a2e3dd340f3bda8f", size = 11239, upload-time = "2023-07-20T15:19:31.307Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/11/7b/3fc711b2efea5e85a7a0bbfe269ea944aa767bbba5ec52f9ee45d362ccf3/types_urllib3-1.26.25.14-py3-none-any.whl", hash = "sha256:9683bbb7fb72e32bfe9d2be6e04875fbe1b3eeec3cbb4ea231435aa7fd6b4f0e", size = 15377, upload-time = "2023-07-20T15:19:30.379Z" }, ] [[package]] name = "typing-extensions" version = "4.14.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, ] [[package]] name = "typing-inspection" version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] [[package]] name = "urllib3" version = "2.6.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, ] [[package]] name = "userpath" version = "1.9.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d5/b7/30753098208505d7ff9be5b3a32112fb8a4cb3ddfccbbb7ba9973f2e29ff/userpath-1.9.2.tar.gz", hash = "sha256:6c52288dab069257cc831846d15d48133522455d4677ee69a9781f11dbefd815", size = 11140, upload-time = "2024-02-29T21:39:08.742Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/43/99/3ec6335ded5b88c2f7ed25c56ffd952546f7ed007ffb1e1539dc3b57015a/userpath-1.9.2-py3-none-any.whl", hash = "sha256:2cbf01a23d655a1ff8fc166dfb78da1b641d1ceabf0fe5f970767d380b14e89d", size = 9065, upload-time = "2024-02-29T21:39:07.551Z" }, ] [[package]] name = "uv" version = "0.9.30" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/4e/a0/63cea38fe839fb89592728b91928ee6d15705f1376a7940fee5bbc77fea0/uv-0.9.30.tar.gz", hash = "sha256:03ebd4b22769e0a8d825fa09d038e31cbab5d3d48edf755971cb0cec7920ab95", size = 3846526, upload-time = "2026-02-04T21:45:37.58Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a3/3c/71be72f125f0035348b415468559cc3b335ec219376d17a3d242d2bd9b23/uv-0.9.30-py3-none-linux_armv6l.whl", hash = "sha256:a5467dddae1cd5f4e093f433c0f0d9a0df679b92696273485ec91bbb5a8620e6", size = 21927585, upload-time = "2026-02-04T21:46:14.935Z" }, { url = "https://files.pythonhosted.org/packages/0f/fd/8070b5423a77d4058d14e48a970aa075762bbff4c812dda3bb3171543e44/uv-0.9.30-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6ec38ae29aa83a37c6e50331707eac8ecc90cf2b356d60ea6382a94de14973be", size = 21050392, upload-time = "2026-02-04T21:45:55.649Z" }, { url = "https://files.pythonhosted.org/packages/42/5f/3ccc9415ef62969ed01829572338ea7bdf4c5cf1ffb9edc1f8cb91b571f3/uv-0.9.30-py3-none-macosx_11_0_arm64.whl", hash = "sha256:777ecd117cf1d8d6bb07de8c9b7f6c5f3e802415b926cf059d3423699732eb8c", size = 19817085, upload-time = "2026-02-04T21:45:40.881Z" }, { url = "https://files.pythonhosted.org/packages/8b/3f/76b44e2a224f4c4a8816fc92686ef6d4c2656bc5fc9d4f673816162c994d/uv-0.9.30-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:93049ba3c41fa2cc38b467cb78ef61b2ddedca34b6be924a5481d7750c8111c6", size = 21620537, upload-time = "2026-02-04T21:45:47.846Z" }, { url = "https://files.pythonhosted.org/packages/60/2a/50f7e8c6d532af8dd327f77bdc75ce4652322ac34f5e29f79a8e04ea3cc8/uv-0.9.30-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:f295604fee71224ebe2685a0f1f4ff7a45c77211a60bd57133a4a02056d7c775", size = 21550855, upload-time = "2026-02-04T21:46:26.269Z" }, { url = "https://files.pythonhosted.org/packages/0e/10/f823d4af1125fae559194b356757dc7d4a8ac79d10d11db32c2d4c9e2f63/uv-0.9.30-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2faf84e1f3b6fc347a34c07f1291d11acf000b0dd537a61d541020f22b17ccd9", size = 21516576, upload-time = "2026-02-04T21:46:03.494Z" }, { url = "https://files.pythonhosted.org/packages/91/f3/64b02db11f38226ed34458c7fbdb6f16b6d4fd951de24c3e51acf02b30f8/uv-0.9.30-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b3b3700ecf64a09a07fd04d10ec35f0973ec15595d38bbafaa0318252f7e31f", size = 22718097, upload-time = "2026-02-04T21:45:51.875Z" }, { url = "https://files.pythonhosted.org/packages/28/21/a48d1872260f04a68bb5177b0f62ddef62ab892d544ed1922f2d19fd2b00/uv-0.9.30-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b176fc2937937dd81820445cb7e7e2e3cd1009a003c512f55fa0ae10064c8a38", size = 24107844, upload-time = "2026-02-04T21:46:19.032Z" }, { url = "https://files.pythonhosted.org/packages/1c/c6/d7e5559bfe1ab7a215a7ad49c58c8a5701728f2473f7f436ef00b4664e88/uv-0.9.30-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:180e8070b8c438b9a3fb3fde8a37b365f85c3c06e17090f555dc68fdebd73333", size = 23685378, upload-time = "2026-02-04T21:46:07.166Z" }, { url = "https://files.pythonhosted.org/packages/a8/bf/b937bbd50d14c6286e353fd4c7bdc09b75f6b3a26bd4e2f3357e99891f28/uv-0.9.30-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4125a9aa2a751e1589728f6365cfe204d1be41499148ead44b6180b7df576f27", size = 22848471, upload-time = "2026-02-04T21:45:18.728Z" }, { url = "https://files.pythonhosted.org/packages/6a/57/12a67c569e69b71508ad669adad266221f0b1d374be88eaf60109f551354/uv-0.9.30-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4366dd740ac9ad3ec50a58868a955b032493bb7d7e6ed368289e6ced8bbc70f3", size = 22774258, upload-time = "2026-02-04T21:46:10.798Z" }, { url = "https://files.pythonhosted.org/packages/3d/b8/a26cc64685dddb9fb13f14c3dc1b12009f800083405f854f84eb8c86b494/uv-0.9.30-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:33e50f208e01a0c20b3c5f87d453356a5cbcfd68f19e47a28b274cd45618881c", size = 21699573, upload-time = "2026-02-04T21:45:44.365Z" }, { url = "https://files.pythonhosted.org/packages/c8/59/995af0c5f0740f8acb30468e720269e720352df1d204e82c2d52d9a8c586/uv-0.9.30-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5e7a6fa7a3549ce893cf91fe4b06629e3e594fc1dca0a6050aba2ea08722e964", size = 22460799, upload-time = "2026-02-04T21:45:26.658Z" }, { url = "https://files.pythonhosted.org/packages/bb/0b/6affe815ecbaebf38b35d6230fbed2f44708c67d5dd5720f81f2ec8f96ff/uv-0.9.30-py3-none-musllinux_1_1_i686.whl", hash = "sha256:62d7e408d41e392b55ffa4cf9b07f7bbd8b04e0929258a42e19716c221ac0590", size = 22001777, upload-time = "2026-02-04T21:45:34.656Z" }, { url = "https://files.pythonhosted.org/packages/f3/b6/47a515171c891b0d29f8e90c8a1c0e233e4813c95a011799605cfe04c74c/uv-0.9.30-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:6dc65c24f5b9cdc78300fa6631368d3106e260bbffa66fb1e831a318374da2df", size = 22968416, upload-time = "2026-02-04T21:45:22.863Z" }, { url = "https://files.pythonhosted.org/packages/3d/3a/c1df8615385138bb7c43342586431ca32b77466c5fb086ac0ed14ab6ca28/uv-0.9.30-py3-none-win32.whl", hash = "sha256:74e94c65d578657db94a753d41763d0364e5468ec0d368fb9ac8ddab0fb6e21f", size = 20889232, upload-time = "2026-02-04T21:46:22.617Z" }, { url = "https://files.pythonhosted.org/packages/f2/a8/e8761c8414a880d70223723946576069e042765475f73b4436d78b865dba/uv-0.9.30-py3-none-win_amd64.whl", hash = "sha256:88a2190810684830a1ba4bb1cf8fb06b0308988a1589559404259d295260891c", size = 23432208, upload-time = "2026-02-04T21:45:30.85Z" }, { url = "https://files.pythonhosted.org/packages/49/e8/6f2ebab941ec559f97110bbbae1279cd0333d6bc352b55f6fa3fefb020d9/uv-0.9.30-py3-none-win_arm64.whl", hash = "sha256:7fde83a5b5ea027315223c33c30a1ab2f2186910b933d091a1b7652da879e230", size = 21887273, upload-time = "2026-02-04T21:45:59.787Z" }, ] [[package]] name = "uvicorn" version = "0.40.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, ] [package.optional-dependencies] standard = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "httptools" }, { name = "python-dotenv" }, { name = "pyyaml" }, { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, { name = "watchfiles" }, { name = "websockets" }, ] [[package]] name = "uvloop" version = "0.22.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/eb/14/ecceb239b65adaaf7fde510aa8bd534075695d1e5f8dadfa32b5723d9cfb/uvloop-0.22.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ef6f0d4cc8a9fa1f6a910230cd53545d9a14479311e87e3cb225495952eb672c", size = 1343335, upload-time = "2025-10-16T22:16:11.43Z" }, { url = "https://files.pythonhosted.org/packages/ba/ae/6f6f9af7f590b319c94532b9567409ba11f4fa71af1148cab1bf48a07048/uvloop-0.22.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7cd375a12b71d33d46af85a3343b35d98e8116134ba404bd657b3b1d15988792", size = 742903, upload-time = "2025-10-16T22:16:12.979Z" }, { url = "https://files.pythonhosted.org/packages/09/bd/3667151ad0702282a1f4d5d29288fce8a13c8b6858bf0978c219cd52b231/uvloop-0.22.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac33ed96229b7790eb729702751c0e93ac5bc3bcf52ae9eccbff30da09194b86", size = 3648499, upload-time = "2025-10-16T22:16:14.451Z" }, { url = "https://files.pythonhosted.org/packages/b3/f6/21657bb3beb5f8c57ce8be3b83f653dd7933c2fd00545ed1b092d464799a/uvloop-0.22.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:481c990a7abe2c6f4fc3d98781cc9426ebd7f03a9aaa7eb03d3bfc68ac2a46bd", size = 3700133, upload-time = "2025-10-16T22:16:16.272Z" }, { url = "https://files.pythonhosted.org/packages/09/e0/604f61d004ded805f24974c87ddd8374ef675644f476f01f1df90e4cdf72/uvloop-0.22.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a592b043a47ad17911add5fbd087c76716d7c9ccc1d64ec9249ceafd735f03c2", size = 3512681, upload-time = "2025-10-16T22:16:18.07Z" }, { url = "https://files.pythonhosted.org/packages/bb/ce/8491fd370b0230deb5eac69c7aae35b3be527e25a911c0acdffb922dc1cd/uvloop-0.22.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1489cf791aa7b6e8c8be1c5a080bae3a672791fcb4e9e12249b05862a2ca9cec", size = 3615261, upload-time = "2025-10-16T22:16:19.596Z" }, { url = "https://files.pythonhosted.org/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420, upload-time = "2025-10-16T22:16:21.187Z" }, { url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677, upload-time = "2025-10-16T22:16:22.558Z" }, { url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819, upload-time = "2025-10-16T22:16:23.903Z" }, { url = "https://files.pythonhosted.org/packages/74/4f/256aca690709e9b008b7108bc85fba619a2bc37c6d80743d18abad16ee09/uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", size = 3804529, upload-time = "2025-10-16T22:16:25.246Z" }, { url = "https://files.pythonhosted.org/packages/7f/74/03c05ae4737e871923d21a76fe28b6aad57f5c03b6e6bfcfa5ad616013e4/uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", size = 3621267, upload-time = "2025-10-16T22:16:26.819Z" }, { url = "https://files.pythonhosted.org/packages/75/be/f8e590fe61d18b4a92070905497aec4c0e64ae1761498cad09023f3f4b3e/uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", size = 3723105, upload-time = "2025-10-16T22:16:28.252Z" }, { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, ] [[package]] name = "verspec" version = "0.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e7/44/8126f9f0c44319b2efc65feaad589cadef4d77ece200ae3c9133d58464d0/verspec-0.1.0.tar.gz", hash = "sha256:c4504ca697b2056cdb4bfa7121461f5a0e81809255b41c03dda4ba823637c01e", size = 27123, upload-time = "2020-11-30T02:24:09.646Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl", hash = "sha256:741877d5633cc9464c45a469ae2a31e801e6dbbaa85b9675d481cda100f11c31", size = 19640, upload-time = "2020-11-30T02:24:08.387Z" }, ] [[package]] name = "virtualenv" version = "20.36.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, { name = "filelock" }, { name = "platformdirs" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/aa/a3/4d310fa5f00863544e1d0f4de93bddec248499ccf97d4791bc3122c9d4f3/virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba", size = 6032239, upload-time = "2026-01-09T18:21:01.296Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/6a/2a/dc2228b2888f51192c7dc766106cd475f1b768c10caaf9727659726f7391/virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f", size = 6008258, upload-time = "2026-01-09T18:20:59.425Z" }, ] [[package]] name = "watchdog" version = "6.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" }, { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" }, { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" }, { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" }, { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" }, { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, ] [[package]] name = "watchfiles" version = "1.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a7/1a/206e8cf2dd86fddf939165a57b4df61607a1e0add2785f170a3f616b7d9f/watchfiles-1.1.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:eef58232d32daf2ac67f42dea51a2c80f0d03379075d44a587051e63cc2e368c", size = 407318, upload-time = "2025-10-14T15:04:18.753Z" }, { url = "https://files.pythonhosted.org/packages/b3/0f/abaf5262b9c496b5dad4ed3c0e799cbecb1f8ea512ecb6ddd46646a9fca3/watchfiles-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03fa0f5237118a0c5e496185cafa92878568b652a2e9a9382a5151b1a0380a43", size = 394478, upload-time = "2025-10-14T15:04:20.297Z" }, { url = "https://files.pythonhosted.org/packages/b1/04/9cc0ba88697b34b755371f5ace8d3a4d9a15719c07bdc7bd13d7d8c6a341/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca65483439f9c791897f7db49202301deb6e15fe9f8fe2fed555bf986d10c31", size = 449894, upload-time = "2025-10-14T15:04:21.527Z" }, { url = "https://files.pythonhosted.org/packages/d2/9c/eda4615863cd8621e89aed4df680d8c3ec3da6a4cf1da113c17decd87c7f/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0ab1c1af0cb38e3f598244c17919fb1a84d1629cc08355b0074b6d7f53138ac", size = 459065, upload-time = "2025-10-14T15:04:22.795Z" }, { url = "https://files.pythonhosted.org/packages/84/13/f28b3f340157d03cbc8197629bc109d1098764abe1e60874622a0be5c112/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bc570d6c01c206c46deb6e935a260be44f186a2f05179f52f7fcd2be086a94d", size = 488377, upload-time = "2025-10-14T15:04:24.138Z" }, { url = "https://files.pythonhosted.org/packages/86/93/cfa597fa9389e122488f7ffdbd6db505b3b915ca7435ecd7542e855898c2/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e84087b432b6ac94778de547e08611266f1f8ffad28c0ee4c82e028b0fc5966d", size = 595837, upload-time = "2025-10-14T15:04:25.057Z" }, { url = "https://files.pythonhosted.org/packages/57/1e/68c1ed5652b48d89fc24d6af905d88ee4f82fa8bc491e2666004e307ded1/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:620bae625f4cb18427b1bb1a2d9426dc0dd5a5ba74c7c2cdb9de405f7b129863", size = 473456, upload-time = "2025-10-14T15:04:26.497Z" }, { url = "https://files.pythonhosted.org/packages/d5/dc/1a680b7458ffa3b14bb64878112aefc8f2e4f73c5af763cbf0bd43100658/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:544364b2b51a9b0c7000a4b4b02f90e9423d97fbbf7e06689236443ebcad81ab", size = 455614, upload-time = "2025-10-14T15:04:27.539Z" }, { url = "https://files.pythonhosted.org/packages/61/a5/3d782a666512e01eaa6541a72ebac1d3aae191ff4a31274a66b8dd85760c/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bbe1ef33d45bc71cf21364df962af171f96ecaeca06bd9e3d0b583efb12aec82", size = 630690, upload-time = "2025-10-14T15:04:28.495Z" }, { url = "https://files.pythonhosted.org/packages/9b/73/bb5f38590e34687b2a9c47a244aa4dd50c56a825969c92c9c5fc7387cea1/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a0bb430adb19ef49389e1ad368450193a90038b5b752f4ac089ec6942c4dff4", size = 622459, upload-time = "2025-10-14T15:04:29.491Z" }, { url = "https://files.pythonhosted.org/packages/f1/ac/c9bb0ec696e07a20bd58af5399aeadaef195fb2c73d26baf55180fe4a942/watchfiles-1.1.1-cp310-cp310-win32.whl", hash = "sha256:3f6d37644155fb5beca5378feb8c1708d5783145f2a0f1c4d5a061a210254844", size = 272663, upload-time = "2025-10-14T15:04:30.435Z" }, { url = "https://files.pythonhosted.org/packages/11/a0/a60c5a7c2ec59fa062d9a9c61d02e3b6abd94d32aac2d8344c4bdd033326/watchfiles-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a36d8efe0f290835fd0f33da35042a1bb5dc0e83cbc092dcf69bce442579e88e", size = 287453, upload-time = "2025-10-14T15:04:31.53Z" }, { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" }, { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" }, { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" }, { url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" }, { url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" }, { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" }, { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" }, { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" }, { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" }, { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" }, { url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" }, { url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" }, { url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" }, { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, { url = "https://files.pythonhosted.org/packages/ba/4c/a888c91e2e326872fa4705095d64acd8aa2fb9c1f7b9bd0588f33850516c/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:17ef139237dfced9da49fb7f2232c86ca9421f666d78c264c7ffca6601d154c3", size = 409611, upload-time = "2025-10-14T15:06:05.809Z" }, { url = "https://files.pythonhosted.org/packages/1e/c7/5420d1943c8e3ce1a21c0a9330bcf7edafb6aa65d26b21dbb3267c9e8112/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:672b8adf25b1a0d35c96b5888b7b18699d27d4194bac8beeae75be4b7a3fc9b2", size = 396889, upload-time = "2025-10-14T15:06:07.035Z" }, { url = "https://files.pythonhosted.org/packages/0c/e5/0072cef3804ce8d3aaddbfe7788aadff6b3d3f98a286fdbee9fd74ca59a7/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a13aea58bc2b90173bc69f2a90de8e282648939a00a602e1dc4ee23e26b66d", size = 451616, upload-time = "2025-10-14T15:06:08.072Z" }, { url = "https://files.pythonhosted.org/packages/83/4e/b87b71cbdfad81ad7e83358b3e447fedd281b880a03d64a760fe0a11fc2e/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b495de0bb386df6a12b18335a0285dda90260f51bdb505503c02bcd1ce27a8b", size = 458413, upload-time = "2025-10-14T15:06:09.209Z" }, { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" }, { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" }, { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" }, { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" }, ] [[package]] name = "websockets" version = "16.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/20/74/221f58decd852f4b59cc3354cccaf87e8ef695fede361d03dc9a7396573b/websockets-16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04cdd5d2d1dacbad0a7bf36ccbcd3ccd5a30ee188f2560b7a62a30d14107b31a", size = 177343, upload-time = "2026-01-10T09:22:21.28Z" }, { url = "https://files.pythonhosted.org/packages/19/0f/22ef6107ee52ab7f0b710d55d36f5a5d3ef19e8a205541a6d7ffa7994e5a/websockets-16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8ff32bb86522a9e5e31439a58addbb0166f0204d64066fb955265c4e214160f0", size = 175021, upload-time = "2026-01-10T09:22:22.696Z" }, { url = "https://files.pythonhosted.org/packages/10/40/904a4cb30d9b61c0e278899bf36342e9b0208eb3c470324a9ecbaac2a30f/websockets-16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:583b7c42688636f930688d712885cf1531326ee05effd982028212ccc13e5957", size = 175320, upload-time = "2026-01-10T09:22:23.94Z" }, { url = "https://files.pythonhosted.org/packages/9d/2f/4b3ca7e106bc608744b1cdae041e005e446124bebb037b18799c2d356864/websockets-16.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7d837379b647c0c4c2355c2499723f82f1635fd2c26510e1f587d89bc2199e72", size = 183815, upload-time = "2026-01-10T09:22:25.469Z" }, { url = "https://files.pythonhosted.org/packages/86/26/d40eaa2a46d4302becec8d15b0fc5e45bdde05191e7628405a19cf491ccd/websockets-16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df57afc692e517a85e65b72e165356ed1df12386ecb879ad5693be08fac65dde", size = 185054, upload-time = "2026-01-10T09:22:27.101Z" }, { url = "https://files.pythonhosted.org/packages/b0/ba/6500a0efc94f7373ee8fefa8c271acdfd4dca8bd49a90d4be7ccabfc397e/websockets-16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2b9f1e0d69bc60a4a87349d50c09a037a2607918746f07de04df9e43252c77a3", size = 184565, upload-time = "2026-01-10T09:22:28.293Z" }, { url = "https://files.pythonhosted.org/packages/04/b4/96bf2cee7c8d8102389374a2616200574f5f01128d1082f44102140344cc/websockets-16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:335c23addf3d5e6a8633f9f8eda77efad001671e80b95c491dd0924587ece0b3", size = 183848, upload-time = "2026-01-10T09:22:30.394Z" }, { url = "https://files.pythonhosted.org/packages/02/8e/81f40fb00fd125357814e8c3025738fc4ffc3da4b6b4a4472a82ba304b41/websockets-16.0-cp310-cp310-win32.whl", hash = "sha256:37b31c1623c6605e4c00d466c9d633f9b812ea430c11c8a278774a1fde1acfa9", size = 178249, upload-time = "2026-01-10T09:22:32.083Z" }, { url = "https://files.pythonhosted.org/packages/b4/5f/7e40efe8df57db9b91c88a43690ac66f7b7aa73a11aa6a66b927e44f26fa/websockets-16.0-cp310-cp310-win_amd64.whl", hash = "sha256:8e1dab317b6e77424356e11e99a432b7cb2f3ec8c5ab4dabbcee6add48f72b35", size = 178685, upload-time = "2026-01-10T09:22:33.345Z" }, { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" }, { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" }, { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" }, { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" }, { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" }, { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" }, { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" }, { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" }, { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" }, { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, { url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" }, { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" }, { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" }, { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" }, { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" }, { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, ] [[package]] name = "zipp" version = "3.20.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/0e/af/9f2de5bd32549a1b705af7a7c054af3878816a1267cb389c03cc4f342a51/zipp-3.20.0.tar.gz", hash = "sha256:0145e43d89664cfe1a2e533adc75adafed82fe2da404b4bbb6b026c0157bdb31", size = 23244, upload-time = "2024-08-11T17:20:06.939Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/da/cc/b9958af9f9c86b51f846d8487440af495ecf19b16e426fce1ed0b0796175/zipp-3.20.0-py3-none-any.whl", hash = "sha256:58da6168be89f0be59beb194da1250516fdaa062ccebd30127ac65d30045e10d", size = 9432, upload-time = "2024-08-11T17:20:05.826Z" }, ] [[package]] name = "zstandard" version = "0.25.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/56/7a/28efd1d371f1acd037ac64ed1c5e2b41514a6cc937dd6ab6a13ab9f0702f/zstandard-0.25.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e59fdc271772f6686e01e1b3b74537259800f57e24280be3f29c8a0deb1904dd", size = 795256, upload-time = "2025-09-14T22:15:56.415Z" }, { url = "https://files.pythonhosted.org/packages/96/34/ef34ef77f1ee38fc8e4f9775217a613b452916e633c4f1d98f31db52c4a5/zstandard-0.25.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4d441506e9b372386a5271c64125f72d5df6d2a8e8a2a45a0ae09b03cb781ef7", size = 640565, upload-time = "2025-09-14T22:15:58.177Z" }, { url = "https://files.pythonhosted.org/packages/9d/1b/4fdb2c12eb58f31f28c4d28e8dc36611dd7205df8452e63f52fb6261d13e/zstandard-0.25.0-cp310-cp310-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:ab85470ab54c2cb96e176f40342d9ed41e58ca5733be6a893b730e7af9c40550", size = 5345306, upload-time = "2025-09-14T22:16:00.165Z" }, { url = "https://files.pythonhosted.org/packages/73/28/a44bdece01bca027b079f0e00be3b6bd89a4df180071da59a3dd7381665b/zstandard-0.25.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e05ab82ea7753354bb054b92e2f288afb750e6b439ff6ca78af52939ebbc476d", size = 5055561, upload-time = "2025-09-14T22:16:02.22Z" }, { url = "https://files.pythonhosted.org/packages/e9/74/68341185a4f32b274e0fc3410d5ad0750497e1acc20bd0f5b5f64ce17785/zstandard-0.25.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:78228d8a6a1c177a96b94f7e2e8d012c55f9c760761980da16ae7546a15a8e9b", size = 5402214, upload-time = "2025-09-14T22:16:04.109Z" }, { url = "https://files.pythonhosted.org/packages/8b/67/f92e64e748fd6aaffe01e2b75a083c0c4fd27abe1c8747fee4555fcee7dd/zstandard-0.25.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b6bd67528ee8b5c5f10255735abc21aa106931f0dbaf297c7be0c886353c3d0", size = 5449703, upload-time = "2025-09-14T22:16:06.312Z" }, { url = "https://files.pythonhosted.org/packages/fd/e5/6d36f92a197c3c17729a2125e29c169f460538a7d939a27eaaa6dcfcba8e/zstandard-0.25.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4b6d83057e713ff235a12e73916b6d356e3084fd3d14ced499d84240f3eecee0", size = 5556583, upload-time = "2025-09-14T22:16:08.457Z" }, { url = "https://files.pythonhosted.org/packages/d7/83/41939e60d8d7ebfe2b747be022d0806953799140a702b90ffe214d557638/zstandard-0.25.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9174f4ed06f790a6869b41cba05b43eeb9a35f8993c4422ab853b705e8112bbd", size = 5045332, upload-time = "2025-09-14T22:16:10.444Z" }, { url = "https://files.pythonhosted.org/packages/b3/87/d3ee185e3d1aa0133399893697ae91f221fda79deb61adbe998a7235c43f/zstandard-0.25.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:25f8f3cd45087d089aef5ba3848cd9efe3ad41163d3400862fb42f81a3a46701", size = 5572283, upload-time = "2025-09-14T22:16:12.128Z" }, { url = "https://files.pythonhosted.org/packages/0a/1d/58635ae6104df96671076ac7d4ae7816838ce7debd94aecf83e30b7121b0/zstandard-0.25.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3756b3e9da9b83da1796f8809dd57cb024f838b9eeafde28f3cb472012797ac1", size = 4959754, upload-time = "2025-09-14T22:16:14.225Z" }, { url = "https://files.pythonhosted.org/packages/75/d6/57e9cb0a9983e9a229dd8fd2e6e96593ef2aa82a3907188436f22b111ccd/zstandard-0.25.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:81dad8d145d8fd981b2962b686b2241d3a1ea07733e76a2f15435dfb7fb60150", size = 5266477, upload-time = "2025-09-14T22:16:16.343Z" }, { url = "https://files.pythonhosted.org/packages/d1/a9/ee891e5edf33a6ebce0a028726f0bbd8567effe20fe3d5808c42323e8542/zstandard-0.25.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a5a419712cf88862a45a23def0ae063686db3d324cec7edbe40509d1a79a0aab", size = 5440914, upload-time = "2025-09-14T22:16:18.453Z" }, { url = "https://files.pythonhosted.org/packages/58/08/a8522c28c08031a9521f27abc6f78dbdee7312a7463dd2cfc658b813323b/zstandard-0.25.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e7360eae90809efd19b886e59a09dad07da4ca9ba096752e61a2e03c8aca188e", size = 5819847, upload-time = "2025-09-14T22:16:20.559Z" }, { url = "https://files.pythonhosted.org/packages/6f/11/4c91411805c3f7b6f31c60e78ce347ca48f6f16d552fc659af6ec3b73202/zstandard-0.25.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:75ffc32a569fb049499e63ce68c743155477610532da1eb38e7f24bf7cd29e74", size = 5363131, upload-time = "2025-09-14T22:16:22.206Z" }, { url = "https://files.pythonhosted.org/packages/ef/d6/8c4bd38a3b24c4c7676a7a3d8de85d6ee7a983602a734b9f9cdefb04a5d6/zstandard-0.25.0-cp310-cp310-win32.whl", hash = "sha256:106281ae350e494f4ac8a80470e66d1fe27e497052c8d9c3b95dc4cf1ade81aa", size = 436469, upload-time = "2025-09-14T22:16:25.002Z" }, { url = "https://files.pythonhosted.org/packages/93/90/96d50ad417a8ace5f841b3228e93d1bb13e6ad356737f42e2dde30d8bd68/zstandard-0.25.0-cp310-cp310-win_amd64.whl", hash = "sha256:ea9d54cc3d8064260114a0bbf3479fc4a98b21dffc89b3459edd506b69262f6e", size = 506100, upload-time = "2025-09-14T22:16:23.569Z" }, { url = "https://files.pythonhosted.org/packages/2a/83/c3ca27c363d104980f1c9cee1101cc8ba724ac8c28a033ede6aab89585b1/zstandard-0.25.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:933b65d7680ea337180733cf9e87293cc5500cc0eb3fc8769f4d3c88d724ec5c", size = 795254, upload-time = "2025-09-14T22:16:26.137Z" }, { url = "https://files.pythonhosted.org/packages/ac/4d/e66465c5411a7cf4866aeadc7d108081d8ceba9bc7abe6b14aa21c671ec3/zstandard-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3f79487c687b1fc69f19e487cd949bf3aae653d181dfb5fde3bf6d18894706f", size = 640559, upload-time = "2025-09-14T22:16:27.973Z" }, { url = "https://files.pythonhosted.org/packages/12/56/354fe655905f290d3b147b33fe946b0f27e791e4b50a5f004c802cb3eb7b/zstandard-0.25.0-cp311-cp311-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:0bbc9a0c65ce0eea3c34a691e3c4b6889f5f3909ba4822ab385fab9057099431", size = 5348020, upload-time = "2025-09-14T22:16:29.523Z" }, { url = "https://files.pythonhosted.org/packages/3b/13/2b7ed68bd85e69a2069bcc72141d378f22cae5a0f3b353a2c8f50ef30c1b/zstandard-0.25.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:01582723b3ccd6939ab7b3a78622c573799d5d8737b534b86d0e06ac18dbde4a", size = 5058126, upload-time = "2025-09-14T22:16:31.811Z" }, { url = "https://files.pythonhosted.org/packages/c9/dd/fdaf0674f4b10d92cb120ccff58bbb6626bf8368f00ebfd2a41ba4a0dc99/zstandard-0.25.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5f1ad7bf88535edcf30038f6919abe087f606f62c00a87d7e33e7fc57cb69fcc", size = 5405390, upload-time = "2025-09-14T22:16:33.486Z" }, { url = "https://files.pythonhosted.org/packages/0f/67/354d1555575bc2490435f90d67ca4dd65238ff2f119f30f72d5cde09c2ad/zstandard-0.25.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:06acb75eebeedb77b69048031282737717a63e71e4ae3f77cc0c3b9508320df6", size = 5452914, upload-time = "2025-09-14T22:16:35.277Z" }, { url = "https://files.pythonhosted.org/packages/bb/1f/e9cfd801a3f9190bf3e759c422bbfd2247db9d7f3d54a56ecde70137791a/zstandard-0.25.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9300d02ea7c6506f00e627e287e0492a5eb0371ec1670ae852fefffa6164b072", size = 5559635, upload-time = "2025-09-14T22:16:37.141Z" }, { url = "https://files.pythonhosted.org/packages/21/88/5ba550f797ca953a52d708c8e4f380959e7e3280af029e38fbf47b55916e/zstandard-0.25.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfd06b1c5584b657a2892a6014c2f4c20e0db0208c159148fa78c65f7e0b0277", size = 5048277, upload-time = "2025-09-14T22:16:38.807Z" }, { url = "https://files.pythonhosted.org/packages/46/c0/ca3e533b4fa03112facbe7fbe7779cb1ebec215688e5df576fe5429172e0/zstandard-0.25.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f373da2c1757bb7f1acaf09369cdc1d51d84131e50d5fa9863982fd626466313", size = 5574377, upload-time = "2025-09-14T22:16:40.523Z" }, { url = "https://files.pythonhosted.org/packages/12/9b/3fb626390113f272abd0799fd677ea33d5fc3ec185e62e6be534493c4b60/zstandard-0.25.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c0e5a65158a7946e7a7affa6418878ef97ab66636f13353b8502d7ea03c8097", size = 4961493, upload-time = "2025-09-14T22:16:43.3Z" }, { url = "https://files.pythonhosted.org/packages/cb/d3/23094a6b6a4b1343b27ae68249daa17ae0651fcfec9ed4de09d14b940285/zstandard-0.25.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c8e167d5adf59476fa3e37bee730890e389410c354771a62e3c076c86f9f7778", size = 5269018, upload-time = "2025-09-14T22:16:45.292Z" }, { url = "https://files.pythonhosted.org/packages/8c/a7/bb5a0c1c0f3f4b5e9d5b55198e39de91e04ba7c205cc46fcb0f95f0383c1/zstandard-0.25.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:98750a309eb2f020da61e727de7d7ba3c57c97cf6213f6f6277bb7fb42a8e065", size = 5443672, upload-time = "2025-09-14T22:16:47.076Z" }, { url = "https://files.pythonhosted.org/packages/27/22/503347aa08d073993f25109c36c8d9f029c7d5949198050962cb568dfa5e/zstandard-0.25.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:22a086cff1b6ceca18a8dd6096ec631e430e93a8e70a9ca5efa7561a00f826fa", size = 5822753, upload-time = "2025-09-14T22:16:49.316Z" }, { url = "https://files.pythonhosted.org/packages/e2/be/94267dc6ee64f0f8ba2b2ae7c7a2df934a816baaa7291db9e1aa77394c3c/zstandard-0.25.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:72d35d7aa0bba323965da807a462b0966c91608ef3a48ba761678cb20ce5d8b7", size = 5366047, upload-time = "2025-09-14T22:16:51.328Z" }, { url = "https://files.pythonhosted.org/packages/7b/a3/732893eab0a3a7aecff8b99052fecf9f605cf0fb5fb6d0290e36beee47a4/zstandard-0.25.0-cp311-cp311-win32.whl", hash = "sha256:f5aeea11ded7320a84dcdd62a3d95b5186834224a9e55b92ccae35d21a8b63d4", size = 436484, upload-time = "2025-09-14T22:16:55.005Z" }, { url = "https://files.pythonhosted.org/packages/43/a3/c6155f5c1cce691cb80dfd38627046e50af3ee9ddc5d0b45b9b063bfb8c9/zstandard-0.25.0-cp311-cp311-win_amd64.whl", hash = "sha256:daab68faadb847063d0c56f361a289c4f268706b598afbf9ad113cbe5c38b6b2", size = 506183, upload-time = "2025-09-14T22:16:52.753Z" }, { url = "https://files.pythonhosted.org/packages/8c/3e/8945ab86a0820cc0e0cdbf38086a92868a9172020fdab8a03ac19662b0e5/zstandard-0.25.0-cp311-cp311-win_arm64.whl", hash = "sha256:22a06c5df3751bb7dc67406f5374734ccee8ed37fc5981bf1ad7041831fa1137", size = 462533, upload-time = "2025-09-14T22:16:53.878Z" }, { url = "https://files.pythonhosted.org/packages/82/fc/f26eb6ef91ae723a03e16eddb198abcfce2bc5a42e224d44cc8b6765e57e/zstandard-0.25.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b", size = 795738, upload-time = "2025-09-14T22:16:56.237Z" }, { url = "https://files.pythonhosted.org/packages/aa/1c/d920d64b22f8dd028a8b90e2d756e431a5d86194caa78e3819c7bf53b4b3/zstandard-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00", size = 640436, upload-time = "2025-09-14T22:16:57.774Z" }, { url = "https://files.pythonhosted.org/packages/53/6c/288c3f0bd9fcfe9ca41e2c2fbfd17b2097f6af57b62a81161941f09afa76/zstandard-0.25.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64", size = 5343019, upload-time = "2025-09-14T22:16:59.302Z" }, { url = "https://files.pythonhosted.org/packages/1e/15/efef5a2f204a64bdb5571e6161d49f7ef0fffdbca953a615efbec045f60f/zstandard-0.25.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea", size = 5063012, upload-time = "2025-09-14T22:17:01.156Z" }, { url = "https://files.pythonhosted.org/packages/b7/37/a6ce629ffdb43959e92e87ebdaeebb5ac81c944b6a75c9c47e300f85abdf/zstandard-0.25.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb", size = 5394148, upload-time = "2025-09-14T22:17:03.091Z" }, { url = "https://files.pythonhosted.org/packages/e3/79/2bf870b3abeb5c070fe2d670a5a8d1057a8270f125ef7676d29ea900f496/zstandard-0.25.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a", size = 5451652, upload-time = "2025-09-14T22:17:04.979Z" }, { url = "https://files.pythonhosted.org/packages/53/60/7be26e610767316c028a2cbedb9a3beabdbe33e2182c373f71a1c0b88f36/zstandard-0.25.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902", size = 5546993, upload-time = "2025-09-14T22:17:06.781Z" }, { url = "https://files.pythonhosted.org/packages/85/c7/3483ad9ff0662623f3648479b0380d2de5510abf00990468c286c6b04017/zstandard-0.25.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f", size = 5046806, upload-time = "2025-09-14T22:17:08.415Z" }, { url = "https://files.pythonhosted.org/packages/08/b3/206883dd25b8d1591a1caa44b54c2aad84badccf2f1de9e2d60a446f9a25/zstandard-0.25.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b", size = 5576659, upload-time = "2025-09-14T22:17:10.164Z" }, { url = "https://files.pythonhosted.org/packages/9d/31/76c0779101453e6c117b0ff22565865c54f48f8bd807df2b00c2c404b8e0/zstandard-0.25.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6", size = 4953933, upload-time = "2025-09-14T22:17:11.857Z" }, { url = "https://files.pythonhosted.org/packages/18/e1/97680c664a1bf9a247a280a053d98e251424af51f1b196c6d52f117c9720/zstandard-0.25.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91", size = 5268008, upload-time = "2025-09-14T22:17:13.627Z" }, { url = "https://files.pythonhosted.org/packages/1e/73/316e4010de585ac798e154e88fd81bb16afc5c5cb1a72eeb16dd37e8024a/zstandard-0.25.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708", size = 5433517, upload-time = "2025-09-14T22:17:16.103Z" }, { url = "https://files.pythonhosted.org/packages/5b/60/dd0f8cfa8129c5a0ce3ea6b7f70be5b33d2618013a161e1ff26c2b39787c/zstandard-0.25.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512", size = 5814292, upload-time = "2025-09-14T22:17:17.827Z" }, { url = "https://files.pythonhosted.org/packages/fc/5f/75aafd4b9d11b5407b641b8e41a57864097663699f23e9ad4dbb91dc6bfe/zstandard-0.25.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa", size = 5360237, upload-time = "2025-09-14T22:17:19.954Z" }, { url = "https://files.pythonhosted.org/packages/ff/8d/0309daffea4fcac7981021dbf21cdb2e3427a9e76bafbcdbdf5392ff99a4/zstandard-0.25.0-cp312-cp312-win32.whl", hash = "sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd", size = 436922, upload-time = "2025-09-14T22:17:24.398Z" }, { url = "https://files.pythonhosted.org/packages/79/3b/fa54d9015f945330510cb5d0b0501e8253c127cca7ebe8ba46a965df18c5/zstandard-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01", size = 506276, upload-time = "2025-09-14T22:17:21.429Z" }, { url = "https://files.pythonhosted.org/packages/ea/6b/8b51697e5319b1f9ac71087b0af9a40d8a6288ff8025c36486e0c12abcc4/zstandard-0.25.0-cp312-cp312-win_arm64.whl", hash = "sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9", size = 462679, upload-time = "2025-09-14T22:17:23.147Z" }, { url = "https://files.pythonhosted.org/packages/35/0b/8df9c4ad06af91d39e94fa96cc010a24ac4ef1378d3efab9223cc8593d40/zstandard-0.25.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec996f12524f88e151c339688c3897194821d7f03081ab35d31d1e12ec975e94", size = 795735, upload-time = "2025-09-14T22:17:26.042Z" }, { url = "https://files.pythonhosted.org/packages/3f/06/9ae96a3e5dcfd119377ba33d4c42a7d89da1efabd5cb3e366b156c45ff4d/zstandard-0.25.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1", size = 640440, upload-time = "2025-09-14T22:17:27.366Z" }, { url = "https://files.pythonhosted.org/packages/d9/14/933d27204c2bd404229c69f445862454dcc101cd69ef8c6068f15aaec12c/zstandard-0.25.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:e96594a5537722fdfb79951672a2a63aec5ebfb823e7560586f7484819f2a08f", size = 5343070, upload-time = "2025-09-14T22:17:28.896Z" }, { url = "https://files.pythonhosted.org/packages/6d/db/ddb11011826ed7db9d0e485d13df79b58586bfdec56e5c84a928a9a78c1c/zstandard-0.25.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea", size = 5063001, upload-time = "2025-09-14T22:17:31.044Z" }, { url = "https://files.pythonhosted.org/packages/db/00/87466ea3f99599d02a5238498b87bf84a6348290c19571051839ca943777/zstandard-0.25.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e", size = 5394120, upload-time = "2025-09-14T22:17:32.711Z" }, { url = "https://files.pythonhosted.org/packages/2b/95/fc5531d9c618a679a20ff6c29e2b3ef1d1f4ad66c5e161ae6ff847d102a9/zstandard-0.25.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551", size = 5451230, upload-time = "2025-09-14T22:17:34.41Z" }, { url = "https://files.pythonhosted.org/packages/63/4b/e3678b4e776db00f9f7b2fe58e547e8928ef32727d7a1ff01dea010f3f13/zstandard-0.25.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a", size = 5547173, upload-time = "2025-09-14T22:17:36.084Z" }, { url = "https://files.pythonhosted.org/packages/4e/d5/ba05ed95c6b8ec30bd468dfeab20589f2cf709b5c940483e31d991f2ca58/zstandard-0.25.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611", size = 5046736, upload-time = "2025-09-14T22:17:37.891Z" }, { url = "https://files.pythonhosted.org/packages/50/d5/870aa06b3a76c73eced65c044b92286a3c4e00554005ff51962deef28e28/zstandard-0.25.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3", size = 5576368, upload-time = "2025-09-14T22:17:40.206Z" }, { url = "https://files.pythonhosted.org/packages/5d/35/398dc2ffc89d304d59bc12f0fdd931b4ce455bddf7038a0a67733a25f550/zstandard-0.25.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b", size = 4954022, upload-time = "2025-09-14T22:17:41.879Z" }, { url = "https://files.pythonhosted.org/packages/9a/5c/36ba1e5507d56d2213202ec2b05e8541734af5f2ce378c5d1ceaf4d88dc4/zstandard-0.25.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1673b7199bbe763365b81a4f3252b8e80f44c9e323fc42940dc8843bfeaf9851", size = 5267889, upload-time = "2025-09-14T22:17:43.577Z" }, { url = "https://files.pythonhosted.org/packages/70/e8/2ec6b6fb7358b2ec0113ae202647ca7c0e9d15b61c005ae5225ad0995df5/zstandard-0.25.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250", size = 5433952, upload-time = "2025-09-14T22:17:45.271Z" }, { url = "https://files.pythonhosted.org/packages/7b/01/b5f4d4dbc59ef193e870495c6f1275f5b2928e01ff5a81fecb22a06e22fb/zstandard-0.25.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98", size = 5814054, upload-time = "2025-09-14T22:17:47.08Z" }, { url = "https://files.pythonhosted.org/packages/b2/e5/fbd822d5c6f427cf158316d012c5a12f233473c2f9c5fe5ab1ae5d21f3d8/zstandard-0.25.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf", size = 5360113, upload-time = "2025-09-14T22:17:48.893Z" }, { url = "https://files.pythonhosted.org/packages/8e/e0/69a553d2047f9a2c7347caa225bb3a63b6d7704ad74610cb7823baa08ed7/zstandard-0.25.0-cp313-cp313-win32.whl", hash = "sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09", size = 436936, upload-time = "2025-09-14T22:17:52.658Z" }, { url = "https://files.pythonhosted.org/packages/d9/82/b9c06c870f3bd8767c201f1edbdf9e8dc34be5b0fbc5682c4f80fe948475/zstandard-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5", size = 506232, upload-time = "2025-09-14T22:17:50.402Z" }, { url = "https://files.pythonhosted.org/packages/d4/57/60c3c01243bb81d381c9916e2a6d9e149ab8627c0c7d7abb2d73384b3c0c/zstandard-0.25.0-cp313-cp313-win_arm64.whl", hash = "sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049", size = 462671, upload-time = "2025-09-14T22:17:51.533Z" }, { url = "https://files.pythonhosted.org/packages/3d/5c/f8923b595b55fe49e30612987ad8bf053aef555c14f05bb659dd5dbe3e8a/zstandard-0.25.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3", size = 795887, upload-time = "2025-09-14T22:17:54.198Z" }, { url = "https://files.pythonhosted.org/packages/8d/09/d0a2a14fc3439c5f874042dca72a79c70a532090b7ba0003be73fee37ae2/zstandard-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f", size = 640658, upload-time = "2025-09-14T22:17:55.423Z" }, { url = "https://files.pythonhosted.org/packages/5d/7c/8b6b71b1ddd517f68ffb55e10834388d4f793c49c6b83effaaa05785b0b4/zstandard-0.25.0-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c", size = 5379849, upload-time = "2025-09-14T22:17:57.372Z" }, { url = "https://files.pythonhosted.org/packages/a4/86/a48e56320d0a17189ab7a42645387334fba2200e904ee47fc5a26c1fd8ca/zstandard-0.25.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439", size = 5058095, upload-time = "2025-09-14T22:17:59.498Z" }, { url = "https://files.pythonhosted.org/packages/f8/ad/eb659984ee2c0a779f9d06dbfe45e2dc39d99ff40a319895df2d3d9a48e5/zstandard-0.25.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043", size = 5551751, upload-time = "2025-09-14T22:18:01.618Z" }, { url = "https://files.pythonhosted.org/packages/61/b3/b637faea43677eb7bd42ab204dfb7053bd5c4582bfe6b1baefa80ac0c47b/zstandard-0.25.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859", size = 6364818, upload-time = "2025-09-14T22:18:03.769Z" }, { url = "https://files.pythonhosted.org/packages/31/dc/cc50210e11e465c975462439a492516a73300ab8caa8f5e0902544fd748b/zstandard-0.25.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0", size = 5560402, upload-time = "2025-09-14T22:18:05.954Z" }, { url = "https://files.pythonhosted.org/packages/c9/ae/56523ae9c142f0c08efd5e868a6da613ae76614eca1305259c3bf6a0ed43/zstandard-0.25.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7", size = 4955108, upload-time = "2025-09-14T22:18:07.68Z" }, { url = "https://files.pythonhosted.org/packages/98/cf/c899f2d6df0840d5e384cf4c4121458c72802e8bda19691f3b16619f51e9/zstandard-0.25.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2", size = 5269248, upload-time = "2025-09-14T22:18:09.753Z" }, { url = "https://files.pythonhosted.org/packages/1b/c0/59e912a531d91e1c192d3085fc0f6fb2852753c301a812d856d857ea03c6/zstandard-0.25.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344", size = 5430330, upload-time = "2025-09-14T22:18:11.966Z" }, { url = "https://files.pythonhosted.org/packages/a0/1d/7e31db1240de2df22a58e2ea9a93fc6e38cc29353e660c0272b6735d6669/zstandard-0.25.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c", size = 5811123, upload-time = "2025-09-14T22:18:13.907Z" }, { url = "https://files.pythonhosted.org/packages/f6/49/fac46df5ad353d50535e118d6983069df68ca5908d4d65b8c466150a4ff1/zstandard-0.25.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088", size = 5359591, upload-time = "2025-09-14T22:18:16.465Z" }, { url = "https://files.pythonhosted.org/packages/c2/38/f249a2050ad1eea0bb364046153942e34abba95dd5520af199aed86fbb49/zstandard-0.25.0-cp314-cp314-win32.whl", hash = "sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12", size = 444513, upload-time = "2025-09-14T22:18:20.61Z" }, { url = "https://files.pythonhosted.org/packages/3a/43/241f9615bcf8ba8903b3f0432da069e857fc4fd1783bd26183db53c4804b/zstandard-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2", size = 516118, upload-time = "2025-09-14T22:18:17.849Z" }, { url = "https://files.pythonhosted.org/packages/f0/ef/da163ce2450ed4febf6467d77ccb4cd52c4c30ab45624bad26ca0a27260c/zstandard-0.25.0-cp314-cp314-win_arm64.whl", hash = "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d", size = 476940, upload-time = "2025-09-14T22:18:19.088Z" }, ]