From 453d3e8a67d65dc7e326d5ac5acfff16d54a8020 Mon Sep 17 00:00:00 2001 From: Jonas Daniels Date: Wed, 28 Oct 2026 13:13:33 -0700 Subject: [PATCH 1/6] chore: pin @rocicorp/zero 1.11.3 Co-Authored-By: Claude Opus 5.5 --- ...age.MacBook-Pro-21_local.pid15427.Xwc66hOx | Bin 0 -> 192512 bytes ZERO_VERSION | 2 +- e2e/docker-compose.yml | 2 +- e2e/package-lock.json | 342 ++++++------------ e2e/package.json | 2 +- harness/package-lock.json | 342 ++++++------------ harness/package.json | 2 +- 7 files changed, 240 insertions(+), 452 deletions(-) create mode 100644 .coverage.MacBook-Pro-21_local.pid15427.Xwc66hOx diff --git a/.coverage.MacBook-Pro-21_local.pid15427.Xwc66hOx b/.coverage.MacBook-Pro-21_local.pid15427.Xwc66hOx new file mode 100644 index 0000000000000000000000000000000000000000..ef8900b9854c499b943ba4efdec4d280ddc78c99 GIT binary patch literal 192512 zcmeFa2b>hex;9?b(=%ON9flkvEjcbx5KxdP8ALLI%d#x6gxzI#L4tx45yb$eV@8jd zBVZO2X2pa#U{08h`5eRU_taEZ_h7o`-f#VX_kVuFz3zFQuAb_in(p_lH`H5GCXX+v zD$ZM2Rx4jVr*Paezb#PjekKR<71UPamQdF90wc?(OH73YlhX!6)Q}tIGc9lYjKtsxt4({@cIJ z@BEg*_dV$6R6{O)QTeHrr6ns?7W+Sbx4ht!-7;UJw5(g6_!4*m+FOyv1L{+Pr?Pl; z<%(sPIu#VItSZa!0{r*|z3|6Q)@4THR;}p96`2P`3{S!QlB!A{m-v1Z}Jwoc2{2ei>nwA$AR<5j2g5=MHr20f4 zCDl9PwczXI84rO|NiZl z7gjCK`|Z7p!?^K|;Yj1d8~)Lai(4q@CH_W|b+pVapB&1J{&EO?@n7hmTNX!xTOL=u zCVy4oGDws1!jcNSvXH@21LTPl7Zl>;!ue$@t1{28pqa;0=$_zyWd{LQV~?{sXHLiK zeRNr6WeL=(P+OG))zQ3;iwi4-6sqi)H@~cGS#e>h)D8H=m`Yr;q%tpa55Lt)M+SXV zt`U7KTGcUgPfEE6F+UYAoqE3jmM2KwH{D5XG@`JoxT<7%ah^COb2FI->-fUT zyrPO?G#lOBQ+0AIQC>bihP3MlGKYMn>Ph8MT{1sL*6H~cZD{3_E+Lgvu zdT+24P6d^i2`n#E-_@hqK~L7N8iR*y5~MVyNZAjU81Sqy6jWY8NJROw&zD1|&hjQm zyt9M5q|u)U{y+CGHv+j4$c;d51ac#g8-d&iMj$r=e>eiR z9%>O)=@-2Y356Q=5$OlLd-m>^->YYS@BVqc`V8zjU|_F4Yc;(^tVCs59NV$Sv`QpL z^jK0_xnNmkkM31{dK_O?v9zk9xVW;%+Tx0`{PHzbi_1!TRQ1VUuwX#H!{+zx-J|=e z;?h+;=9iTAKx1)bRmX059rIUb{;tUUEf;@F%LWz|BGaI}qHI;kg5rvfbL-Ghv!-4S zz+v5c_3Pf7;~VMG=SlSW=*Q9TqHq1-7tA%}Mj$r=xe>^XKyCzbBaj<`+z8}GAU6WJ z5y*`|ZUk~8@V{sT>`)7;91%JTHKf815fVq0&q3p%W~S!_2;)))>ufw@R2pvNmdK3ox8XCx-SwyR)q11Q z&d@aNAKIx}H~JV|Obv2fEf31hzrsGw?1d2uDy zniO6DqnB=;|fJ(3$WH({I>I2)z)1r0kN`jaRGh;tOKts zs}NUcSzA}|e^{(Nu2|yR87|ScHh$j9#aNxZpa)h(mS9C;`I=)7LC=TRM$i5L$+dDq4HPv+=d~$D>I^}o?aMyDhq37C_X=+f=dtpQLUMr8Jf8Uwj zi-R|-paCvXD=PlrOF(#{r~&IkQF?UCy!4Wv#sV-WV*Y z#4}f3Sdp2yPUq;oSFH|1W}-LSM8CE9EN4quhQ4b>yZNHTo7>GW&~vT4C4SGCIf@@K zf{ttDNAx?+9*}Tt4TyXP=dUbThJZEhdzOx#Yvl!$J!eUzS)tnPI%_Q{&li zkH`+EpZl{ritlkRvz{}mw6C9vf4e{rkpqVE=bKPLCZ~ zo7Xtl{_n6rkBzI%?)|TjW&gMT>n;))x9#TYu|c)+O?+XSW&gLGug6B!O7DMY|F_EvHzRT(qnyV<>&pi{oibk9y`2NdiDn>u>TLKZRvn(2&4p3L9a|8-~Tu|Bo(6@F#^*Qqsu@H_T@wAOyp-`M}|G(DDIn*)-u|D9v>SjXDw zRoVY`t-blbXa8F>^jNRjxncRt3GV`|C0^Ula+Z`eYhTKG_CKq&x5O7x(*8GU?ML)G z&Ki(NtqsVp?f-DCy@0>5|Mgn#I%_;aQ}tNaT6w5_`yU2+-e2%;i}nAuZK3{ObZfM= zd$&8y`O!Jo>1N++ueHaZA@?sg0=W^$jX-V$awCu%f!qk>Mj$r=xe@qJ9DyV35ZNGB z6Ib>S)@Tm_3#9cpaA1a%ie18%m8>nUELc{uw76hpIg&ey3zwfzv9h#>6(Yrd`ss?Y zm8A;`swzs#E5!$fiNL@4r=KV+Dk?6osw_Ajh2Z^*6?jKQAIn6Lf9`Mk@KRUd1@LYh zTXhsJ6aI`f1@n=TQd(Icl6KJZLX_#RDnROERq^uTQjubbwbm;$M=GjtuYP{2xY}Y7 z_Mi4&U#+kJ+a8n_XYyKu9%W8RWdS;>EGWjh?CkE<{gfSFDy}qNqydcicdq1RwiYZa zTZFRx*%w9?_=0lm0)S&Hmn|qLEyMRI#E&+=uxP2c#w?K$aM*w68sg))2!6zh;;NMu z_!?pev%YH?cFYl7FA(Vf6aG70X9h*|mNn#=9|?PsWL@Fd^2#+uW#xzFCNdh$Ghj+qO-jKRfx(dG0;#^k|#t zSswsQAx6{VV(58m`C8KmWvkY5()(dTj1*`}m9Of3rl7P5U>lc90qFdlddV_CL#H z0siK<_|5k!RGF>+1N+~|gunkg*Z8;X|46YOoA7T9lyYDEPwfBjUv`yj`(H2DV?}?- zcliVRKeSknP5Fy2n6dx0P{=Vw{GZgxNx$3(Mj$r= zxe>^XKyC#7lSe?r|8w^LfATM!`);`r$c;d51ac#g8-d&iXi0QlbVhVybY%4K zXs>9;Xp3n5sOi?Y-?^W<@3=3!d)+I9_Hq| zZQZ7B)D1g7IbS&+Id3}qoX4H}oLimioXec^oYS0yv&JcNik)Mf$<8QekkiNM?6h(k zI+jE2AMDTV_pm3yGxo#wJ@!ufTKh8lJo_{|VXv{v>|*;^d$K*s9%T2iJKL@7hPGu> z>j&#|>pkmL>ly1|>u&27>niJF>nv-NwZU3xEwu`*8P)`AxHZ7)VYRmov7(mF|HZ%H z@9|goUhH^qJHMV^#?R%a@{{;VzJwpgr|{AIaNeD_<_$PAtIcoCPt5)1KJzhix4FZ- z%Dl+jVy4Y?X1Q5x&Ne5RBg_G2ceAb8#B@xJ{lGqB@35EHlk9$WE4!9m%Fbq+*m_pU z7PC2QGCPVL&U&);><|`Zy74dL3*$ZG6=SdQpmDo#y>Xdwu5qeylCjcQViXutjWNbx zqmR+aXkpYdB9WgWUqs%CybyUba#!Sr$hOE?k#uBDWLcyja!h1oq<^Gqq-CUTL=XQE zJ`jF0{A_qn__pvh;fum&gyZ3r;l<%&!xO_p!-s}Dh7SolVWNMdf2hBTT^;Vzcj(*o zzv-vy>-81-0)2))P9LoI(%b2cbsnk-eH;2Dv_G^j^hoH=(Dk89Lt8_s(CX0A&~c&3 zp%J0OLR~^FLk&VaM6~aNjBV0TR}$ z&*J?htXq%850fxjm&N-@=tfz*uY``v;)hCTJ1pKuLT;P!-hdi4v~UfN_Yzn$$W!D^ zBi<9lqy{(QJ;d3jQDelrXO2aH@-xRGHDfznk9YFOI=X?!J9?yop3LJNJW@&5@pyZ`rIN?nNm6|}t>E#tUdu8;+IZwRx{SwL zdt@p-j>q#nGMP^0@m3z0Kqs?!OAt!K!=|x#3lENG@#Y>J!{W_6IGV)|@!%*PZwe^B z-)K64$D4R$1Rc%ejXg55`W_x{%G&W9=VEa%$Z4Xb z*l#(F#}-IZQzcr8yq2^e^F0zHX&x(7q-Fz;&GSC9fy7v>00i$zYMmK7PCzZZP8^;q z53ftH*c=JV*0I>J9^ArWvnAY8#$vN1ECHM;;ldIYo8iG?7Mm_%@j@1xCSf6tPW9kC z7MmjByh0W`M#8x`I@yDBSnOyC=geiXNfOS)(TNgHo5^AmBpg4C#l}lG8gQJ1qX5TB zcog6m3C97BmT=fnEH+BQF*tgZgrmo>*hmRSjb^bC5{|?P!zCOxlEsEeI08q9dPhgF z*bonnGh;^zsD%en93CtW4;jT`gCrb0gvE}KaL`~DJ6ys&gIG+egYW=AsSd&eA7wGA z4#EQlvY1o{;YSCsm{bShet=RPg!}emF{uv1efqMPR0rYSeOOGW1N`8WfZcntSVkGpaDI0d6WRbryXCW3Mjg;_`)({I^Z|~xZ^vRn9RPNIgT;h00PNJ6 z#e^yV?9hqDgdzZJ*MY@^8UTC)9}`MI9Bt2HLInV}#!;aF0P|Y2m=OPft@2o`jf5>* zu~=&f>$YUEJPDx-V?ylXTqnw6p3sK^jCn%evRTX%`rKkMPw1Q6hzX%@(y(d7gv_UA zxW_J`#fXH*&S653!J0>zAq{XpGOh{;;ffJb! z&p?L`WI{Fr4Z zYbInc(7P>}5WwQRR!m4=pk^(Z5WPT6n=v7GftoaBLg)fDYQluX1!~xc6-v~gArrC| z$1tgrf>y6vpJn7M)$!~~DXU`|CxxtzS)7!yI-X%EVs*KDNr@YKq*hP-J_Tkr`qs|OiEL2OCWWWgb03qkQ|r-_Nztk0_h3?TYIs7V;MDNcNV%zX+|Q)g z)H-xzQfg{l_A@CowN71_l$jdDoD`Yb{tiq^Os!2mlLAv~-G)hdskP2yQe0|~a8g=o z5O7jhYOPu_DJwNRaZ*%jEm|@uDYd2tm=u&+i{?zqNv(McCdH(Nr%*~s4NqZfIerHk zF)1Up#?6@&ky_(MOiDqkb3%XuBGtUIw&jXVlGryf#Li3}}^y()r?? zmaPmo1DdonoD68(#IQ4o&gOS8nBwWU%U{1yIy!K{&qfp3jTID?@0V@ zcTOk#ZMk(V{x&;v4E{DfJ&M1LPhEn)4L1$K-})Ox4|S7s@wZNVpcXxzMBTT=HUOPq z`)kqH-M8JZqjyKIi=H3d80{Nv7i|zNj!uK!KPfuWe%yY`ncyUyN8I}E4H-{BhiJ2C zooLwo(f!hW-+k46+Pxf}fIHmt+*R&oH|8#N=fMXs);-ca)a~rHbUt#K^M><_^R@Gw zbGLH=_8Pp|*$&IU$SHM(Ifpqjoo-HRr;$VK1CC{XhdP8Wz}~;w-e_;NPp~hxSJ;c} zx#l;p#P?Zw)^h6#dkX3l4u<95$!=lSvkmKK>nrO+>vim1_^@@Cb(58_&bLmtR$KF} z@v!-aTK%l9Rzv=7+lIL-nQlIr@F!Md6nt(MXHPZ0w9!!|pQ|n#aQaA8sCI zb~Rg>^-Y8Q#J*(jv6o@}?_>#fJiCfr$j)F(Spl2EM#1v$!P>IM%rd?)UN=56_86DI z>c7rN8>L2X?L+M~?P+7H_Mmo~vDTPx9Adb}bVD=7!UxbH@_pn$WPjxO$Rn`*Z;V_X zIX7}jWPPL}vM@3+GAzM{f^n-weGKx+-*jXk_T8(C49BL(hfw zgia0h4>b>wkgI*IU9DZ9ZPwOn<=RyElOn<(f=;cVm9)KL!nURD6r(V1XZROgQCf$ZKI1e>na^POQp0Dkd}+jIoI03Ls-KWk zo9c=Qno|@J{ki#Rv>x@`KM{36t-A22+@P=QG&C9^WUs=bhmofg9yN?S zsqj(5$P)^W8%7>ic-T?oF@?trBabRPdJK6);ZdW>!wQcaMjldl*hsQR;Ss~gg9;BD zK_2k&IC8%lbQsX&KGiT}6uDR7!9&Pyg$E5L_b9w)5V>370fWe03J-jg+^O(@f#eQ_ zA00q$SGeB*a+|_^`;l7}?$ej-Qn+^?vQyz+y~z%Rd-fu?DBQg#xmn@-?&KzgyXBJ` z6>i^++~DJO8Tk!uug*MVHE@Eh&ORSLIjPp(wBbvv?M;k?%5 z3WZzck-sb4vK6^p;kqr!WeP{@l5Gk*QF5umwnHva*s{sR3UiBGq_D}!g&c2;@P3%& z0s)~f!ZqZ4PjLu4oBYjt-7C<6)vkHr}?;)Y*u*1R&uJs$Xc2Y}L9%!FO9ORjhYlG*s#%j1sgOhRIq-7c?#C8U!Y*L?r{pb(YXpb?i>Ye=U4?T zd$yT*+F_^95(xjN*Ua>eg=%Ja$3ivJy^p|FpQhlr-BT4DGj584V|O2;;KV7D6&y46 zXaz@)nWW&TJ0~i5=cow^jv75)!QG?ADL8!MSOrH8AEV&#k)stHF?^JQ!$%yY;E)j` z6+B|^2nG8+HC(~I{e~&{RNtWr_CI2Xf_;V@so!5%&PE12KoFa`JJ_fs&xdtU`R?mtw)4jua_*kylj1v_==rC^6nJr&&Fp@)KP z^1Cb8x=p?h^SUXR*Sf2Mt@66~uw`cjTej+?V2hR=6>NH-gMuxZw^y)vi*^b&Y1&r7 zMoro%c%V^h1sgZd^I@Y_3N~uo(u1&T)%2WU(sY`(2}klh(PH=R8yW|$rrrkaK@ZnD z;E1v?I(B}G-43oSi}r^PW{YmX0cBpa-QUJ|2fU-#WshU$@CM-RJ>F$tjR}*8bnygu zCvp$D+iVcsOQPZqp&u+;d-E`yp$wB|q?$$|_uz;!N1D>r)qBEE;z%Q}*G6sH>d%08 z^LmYv>&f=ei#Xy2y@J(qBbcHi=xNgi)jvDpBfeqbO_Fgam;WyYA@?sg0=W^$jX-V$ zawCu%f!qk>Mj$r=xe>^XKyCzbBk-R)0$Be~!+L!Z{b%&c=m*i)qt8VjjqZ-_j9wGH zG#|Eh3Nlx_5u4X#Qq;g27rkFUyS(w zMq~hF;{V9@A88M?d)V#lrgqd0TR&P~TJIy*|7mNFb%%9>b-8t(wb_bUtB~nG&zfe9 zwT`q7wK`iZt@;+@zwod5NBj-`9Dju0&2Q%0`31j<{U46#KO+Ak1Hk+S@&DJ50dP?K z-z+kxnWK>5-_>kkMor4TVIQ&A*)wbpyPaLnE@S5)yZ=PCf)%q_Yyum~`m)Zf1*^-# z$nF2!c*l6rc-+`+++u7u&NogoV#w?-G3FYRjgiIxBj3m~8W=3{bL1b9_am=Fo{HQb z*%i4ua#7^WNHVe-nf>!3QzN4zM?`u?+C`c~><9^e8~!-_26Fo!3f~dFKD;e_cKGD* z3E}c^QFvN-RCqwRYq&)iS$D|o|3H6Pe_X#?zd^rL-=Zh={@w;dVM_{ z`T=?UuZQ-A?hD-#`g`b{(8kajWcAMtO$rSS^$E2PH4d5DPul0&+sNpDNV_d7`al1V z*+Q$INVeQ-?u;7IweG9>;uC*oI;@`I!H2gm6T zino((2W4Fx97X?i-29;E`#}-*gJSAr^FfjM|1zeog1G3aT|p+mc?#*TiT``Uf)smlruXC?oC}a0|No=d{vYK6WXJsf zH^uq0g8RQ8#{d10e^zk+zl`W-hw+2pepW<3D~$j9vHPsx{{KH>_g?%Is{#C^gRHcL ztaJ#ZS*XV@I}=1?m;9UY|E%zSRw_VN{68xz;1A;eL8Lz`6(B3TpB>W=;{RDe|Nk=5 zpB2de{h)t#Oh1V9XNC8FKYIWBk^ZchepVp=_e1vC;r$?bpB3r1Rdn z{~%5u#PoxZJ<{@9cuybl@|!DIr(QD!qje5Z(2X`#&~}?BXxWVwUgMStEh*X zv~)exxQXMTMvZL`HEd*gs6j*Sq52I>583sZhp6AEgQ6FHWGXVP)wnlXT)BeT&%KiYg0ruJt*tgo(z*Dfr zPN6Qqa=Xx;hP45M>|Sm1YrIMG^R6I{!x^cSNp* zH{kThNyzkH5Sb2Nz`#g1tn#lD(clUAF#JmRiSRw)8^hbMI{)F&?V)Qz7luv?Z3wLh z6@{jSMui51x`tYWq9Lk%qkW*gtUa#Xt=*tqs%_B{+DdJaHcK0)4bpmOtyKoOL9e3M zX)5#FpqJCDG?jU7(9`MVn#w#k=tg?F7JP0o-KeR|bAuMp#hS`IH;`6epsCDrgAS%6 zHI-d%(8K9qO=XuGw14&Un#w0PXg}JYs(f;T_61k@KeaFsJ| z(4OEbXWUEu@#boRb=TC=1%r0SxhkJrupU%pkQ=mX^($26j2pBw?MhY7xIsJNT$Noe zSZAs-$PJnYu5!i&YeiMgxB;9;RnE9U8`DNq<%NUQG@>eJ+(1iXsARg zlW*wT>OqE^=xeG>9)tXYd`0*BZC{Xo&^LYdIr)OV;j_>15UOPv2KkJ9O4YIqgM3Op zqp$jBeo78dwWPry2gv92b-(QZ&QwbiMBAtI6~FDhnznSG&)y;L(dT{kHhG6W=d-tP zg_pC~e)@vX_LH~h(^>3I`l8R?ByZDav)Ehor7X6e?)BNentt?gpFKzR(Z_uDEP0MT z>a%Cav-A<4?Iq99hkb_1BD7Z)dy+okvnR<@^ua9lgx1MtPmsOzp)B^4re0KoJVBnM z5BP15)C{5b`0OF_2))~9d&oodE}uO}o}l+3v!30eYLyc9Z+*tv`Jm*3um$IbeGSt0Eb?e#jeoQG9`mt0Y~Bue%lq~N=+?H5^dY*wSL>SnuYXApIt(> z(d|CFm|Q}y@YzM=3VKZzyO3VxvkS>Z^s+2=0lnI17m$nT-?P|7^y4gcA>HP)b8E`! zMLs*5oJ%kC*;(XldV$Zjk_+giS!@fv*k@bFR;pGK7-S1MlV0Mtok`B3=V!64^vNu? zg{n0N23(wL=oY{26tbC~>9bAfQ>{lZ$R=_!-RifUOg7Qeve-s?md`elQ|K94Y!f{@ zi=n8FXGR+9&!lS6fw68qRf`Uc)$6EQTwttPP1WK8W92HU78e**E2&ysU{qGogy#h@ zD#%7UM_r&|1yw5+j1?91Bz1g6IaR9~jPgpVRwEeY6?BF=r+fw7pz!i?x?bUuZS(|% z7nM+D6B`Q`(Y5M$@j|*r;i6)?TH*Q2wRS!(q$es|IG?Wc@jR`K!t;vg@jjlfwf1o# ztx|Z-wX|H}*>h-_!n0=6QiW&Eqm@3ML02d|VZZH zU37uMM;}9r6rOZ6ov-l3>9oYh6KJu*6DHCEACIRC6&^o{7W#N1UF72l^f-lwjHh!I zK70s0R^k4K(^(21)}PK)xbIXTAbeO_Qn+DbdW6Ca8q&iR zuHS$TRJdM!IzZvN^=N;Eqjl+F3cFF-PhrQUeHFGHdZ@zOrkN!Q$c#5Qm5UKf&8VS0 z)EV!#q{9_{wIdy)@Jn6j(F#A?lTKB5*LXTl;cKVUWeRVbM^`C)W;u;1VN|mQvf^}~ zl|$UAg<3|C?!nstP+ zHhUfIfc^jIK$K!b{Qn=(kD~jdFGin=?tvA5OY~}F0Gt~=Et-TSpR50utN)j)|A*op zqO3u#{$FMokYACj|Cg)(m#hDW0&KbZf0ZaVtN&+a3IWzfes~7h^$jBfkOdIQ0MsK4X8KVXHBa{KiDMvjTcoqy) zg%b$?HNyUHt4$=)@1h5yZ$)2-K8}k2J7N1@9NmigeESnjUN4*-2czrcTx5K zDfa>QHb0WT9yR|LyK~)R+)?fkZZEfk+sv(lSpJXBm(KgntIpHT9_J3{2Iq3r``?Uc z{wimwGtZgkjCGE54n?*9mQH<#A)f!W{gM5K{ha-XeYbtHz1_aRKEqDh$J{5MD5zlM1J$Efu8wDo|s3-$dkuuij1vMQ~Gh~|$+#(!^A_HSZYyqbT--{&v$ zCs66{X8w0n_20x#;AM#APs^(5Zf2orvaNYHl@C=4x}Pd7L>pQ_a5{ zs`=G3!|b2zQ?{Qy#~wni|F!I5b|z~2tzspJ=ucw9km=uvHDfLd8Q&Y97;m5^;Dg4k z#?{7!#_7oOuQC=HvyBPH5Y+GQU^GQkzb5i^qaU2*i7DJ+?( z?@HNC$FQWV@X97nXGvM&l}(z=lCs7tLw($&Eb_{B;vQs`S2iA(kY!%kgk3Bt>%6k@ z6IfCfdS$!Dv!tx_%0@rLlcLnCrZMtbO+HJ?YOieMXqJ@aUfEG2SyI+}Wy2q0Nm=lf zjXa7aWyM!E0w>6luWZP0mXtML+0Y>@DT}_cAwyYGR()kh;^;8>MUNc8lCthA8+;^7 z4v|M68qAWi@+&(6w<$}%vH?f1q^$kQ`aQ{#viK`|vL8#z>aVQt0G5>HUs>4-mX!5h zS@*syDGR`|?$5KNtN_cN@6M941T5JGi@`GN=a7`uU|F5IMpBf6 z#R8f-Mlw?l)?_t2Dayf$y&9e*Pw=Fu2rI}FmTUupl&8XZEGg^58uRA!WS)1*d-MQLiW;%v{5R=)JSob=3i2v_lP5)` zSV5koukxfQ7AweJ`Xo<^da<%)FHee+v4T7$T13@Y*@F8Og<}PIRJ4fNv4ZTOkMg7_ zA1laxbPrF83bKOSMepNDQAJjeTj*UpS>GeK(OY=3o=0w_xAA0MkL;qi@?;&4?4-MR z5(|AY9qgbxdD8XBEp!J@IvznlmM3kGAhm}lEsr3xhbOs5kl4eMrbplv=Sk)fr1kKm z;Spr@@MOdzNb2Fquq3FGxSJ<+k8CGb@MOq44cR_C32$2FMv?5plhiv60b8CVUJDX` zcw&P`koUtA>pgS5BS;70i8UTUHV{v&_6U-J zc;a}EAm4{4R!Jfp@;tH9Z|T7kRUScl4^LEj1bIL_QQ;A!0rA8N|Fq6LQ7(ya_VYxU z*Mb}%o+$MRzQS^kAOnaemPvwqivv8d)N4Wh4^J#ngo2cK1i3&wvDo{}1>_>0Smd=J zFNh}=dM(Jw;E7_7AV-KL7J%RlY56Ws6nV#yZo(7uJ%Vf#o+$JPl1+GGo=1>t!V?7^ zL8=K)9On^an(&0s26&JO5>0qQ=mSBJXTlRgBM5?Y5uOk_0i*_D5uOlQL9`&rgeQbv zkf-4n6`Da1q=WE;&<%pTN#5cKp&bO-Pu}DSp&#UF`*}iW2tkl+!V^MA2!dP_o)B6> z5Tu&$gwPX$AY+6lgr)$gLCl6Hgsu=R$TQ&yp)KTT`&nW<2u7{TOFSX8g*bjD*~$|_ zUkLIPd6FlD#t;MvCOjc@h9Jl<;R&HN1VMTUPYAss2(n9fLTC;_kX*tOLU#y)91)%n z+5@Bpu_c}m`a`rJvxFyv29c-XqC$rVf*cT@5L!f#`^kemA@qnK50LwLLTD0s+5LdOV#%n_atT1F5gj_`!g zGlC#*geQci5d`TUJkd>(8ieS0LTDS&B6cxJ2z?_@!yO2fBM8zvctUBN3sJE#p|s9L zsN|UNv<{Y&E#wJL>ky=i@PyDhq61`$@Pwy#2$DW{Lg*c|h>#&q2+bo-L#hZ*c)ACt zp=(d~5F~Z*gr|E%-Ud&2x(6*=c*4^?1X&_H;prZNBoUtQbPqv}2v2yr2T$TzJmKk{ z8iX);!qYwI8qcVwdvF@M_H++YIdGb%d&owzi6=bWLrx|edBW2@BIAQ6Jl!J_MR>x~ zJp_3oJmKjcbZ`n!c)CY~RC&VFJ?ITxd%6dwoy-zK_uy4pkBPL%gvxLw)fGf%| zy_T?I1xpB>gQKh0v4qe#fU8!sgwQvDD_60E&^CbBTOuKJ4Pa#zO9)K^SYF8zLeBu6 ziGGEa0bEhR;zGxW6IQUe&@h0@%UN9L7r^<2EH1PQV97QX7rF&-Q3;C+%>uY^5sM4G z0*I|%;zFwc78SF&&?$iPidbA|6u@osSX}56z{2?~F0_d_0hbWE1aKx!5Sj#V#&i}J zdPE$Z!Qw)T0M5CV#f1(5oIQudg$4n{Ogk?02jKKsEH1PM;I-&M=nlXcGg(|{4silH z6M6%1B2ExmLmb5kLT3PupUC1uV*pOT=21dlh;t{fxTh_S*~Q{QTj1!?$FR826@ZhD zW^th@0LM>aaiJ#wG1HCLGA)B<2RenX)Y0Q(-s;zA_=9@>}1g+c&)wkL}VbpY7= zP!<=;01$h@#DyvV#9lCQp$Gt9>cZke4FFqIJXmv3*is=DozlhUmQihLhu7(V}-a7`+)5`u(%NVfS6Lpg~$hN+m6MB zzz4*Z9C0D;0kLC8TnKwWY^4wvq8<=?Da3`K2gDW%aUtdbo8x0b$OB>{gt!p#fDIe7 zxDfDwSXZF$As=azh<>Ynm3}@f#S`^1y-=T`kJJx?mDo&ou+Q69q4z>BW@Y$aAH5`c zW;7nHf^T4EbZqp9X!mGdv|dzqzjr@Ie*aVMZuds_Quj>M|F3Y1+^OzJx1Zb5ZQ`2F z&+rAji@g5FPzB&d?EH7Ov(Z@#KfoMR02u7_aPpkGj%I&jzi+u!koB$gf%TI0C|3Jl1H1oJ>jdltaGW*C8e;Xb+F+Hx&cEXy!sdUB z-^s5uL|%%@{zvnnyf@bNHwa$>EBc;r<8X&?-|z_Rc6UHKPJdlnuAQKrs{bp@ zB1WW8n-rReg%sbT0>CTgWJQ+|EHSeDYsEe}tj|UX>>z&5}YNN3JqXD(h`ba=UG5v=F z%1}T5p=`>XJpnaPPyfMyMo>`y0iRa?jEegA`;=-mrgL9FMcw>+14>Xhf45IFmGkcj zXf;ab-yP6jP&@ywfc}i)`F95NCsfb(bC4*kJk3#gM*pcRI3n!{rdBYQ{ZXhsirxc( z<3CU1=`O4+^66N5o7*R#f1^vBe+Kk2I?wq>K>O0Cv3H68*#Y!1cThkF(TCi_13Hx6 z?+)-OHcP1Ed=k*Q*bU<8fI8UL;iG`M*a70hfY!rC63_ay8f!k>i2-d(_hVmG|E@aH z*W58a#Rd+p^G-nj^hKrh@EY&+Pp-b39_760Q|$iWIB!6F93iX74y%69c|B+yLWeo8 z1@uTd#CbKF9_hT|)9MH5kdH+4NQC`hdPn zUU9Aq=u6~f=ca(Z2zpI6eaX2tpf8XYovQ=-Xw5+9%7ETgGsU?qpm&nHoNWQU1N4e) zdZ%+~KyN2^IF|?XHqeW+>Fv%X0lk&n=3EreUF25h!hr51yPUrVbO-49*>tCKK|pUI zJDk4-^k#C4b6!AiA~!qd2J|v=nR8Yat(oVX9nkYo4FAk5TC>Cv+2|BIt{65+#+}oH z#so<^rv)@l63*s;qFlanYCumSG3OMdQQ{wZT63~^M2e+2ntVnk5*XGXgpfW4Se;Q^-xuIRPC=9(A?{G@rcSTouq3*em3gfSTA+^Fi|NLkw#LKejYz@AA9 zg#C=)6%q2RNkD}>^JhmE5%R22&?@9vy?_dNRyUwRp4HE$uR3)CD&$!-phBKC2&j-} zPBwkXaRVyknH^9e&v-zEJc|TW$g^-jg*;=~^iD?)sE}u&fC_nLWYgOnEucc4`4a() z2zlmDcP-RD+GW=SCqtguzhu*$c6C4@&+MNA3VCM#6i~=B`^SJnp4mTS(VBVoKLZMR zW`CPSYnGtCnfKaTYfeVMQ<1q>*#Gd!lpF0Yd@^j9{YgOXM|m{$nL!WRANu6sK`@u>vZj zOGBTENwNKOKw%=-Zz;-Y`|5XWUsQA2mbSM~30hmzwze+iL zi|y5#WwEEUranUyNNeIVgn_iiK0_QxYveNog0zM{L-0qd?=wVyw0b^6_(!YjGsJ(i zIzB@HNQ?Rm@g%K57Tck@BL3e+oA*D7|G(qDFKn^UVo)R3QU)E>VTh{Yfzki2yopp(|#Y$SoTT86D z*3s5*?CjUsYHmd>4YvQs{B`~`e}M18&VCp0)A&iek}u+O_%Vq159WvR&b%eB&zboP zR`7pp?nl)BF>|-M!`zM?{?0Iy<{GmUHU4Ir6JX;XW_B}Mn~hA1Rin<|$LtNn{vTm? zV>SPFb^-SBOR_bv^^4d{RQemn4rAR|Ypm(F7%{#x4j6A4FTm!%*Vu_1fQyZ-#zw5{ zUtugXW@WMf`eJQ=GwkR`Bi}|oio6!ti&}p>BUeVwkDL)nMb<{ju)cp*WMX7^q<=-9$lRFm)@sgLavt9(+_hN# zPwpTOvyC+4t#XcE%Qq75_^sp)o)#`!ancTQD^Cljtt2~GTDonW<;U~1aNCOG+X;?K z$E{PeoTa7X)@ePMrKRK6Y1x{krQ_Ba{2)t9$F1|=V4fC^ThYgPWV@Lbu3Ic9MM#II zh3{50JVW-ewDjIOPD`E^-dl0}3Gxg}OaHCI9iA5cTXFnh@&rpu53a*FOG^)~V=$JM z9$Y74u(b5xI^hUQOAoH2hgn*BaGj8Dq=g69q)y04XB@buQ^V5Ie`~J>l>S@0>>8Gq z{#$!d8B0t5tzEQ;rKSJYo?FDy(tm5uSk2Of=#^4?=3JJZCm|fiY3aYUubIKp(tm4D z29*9=d-BmNo$=pNd+N7EicXvESzB&^qfrMpXrt#i}) z5@PS%bTki6nnhlv^?16y zM_wSW@^m|o>?SYpbX$+yj7)=c8;@L1ZszIM9=U>C&(nDxxfp@IbSsaXk2qbrrAN*o z=ks(6k8H)x=jrAiIRhcHbTf}^BxkVnAt01mu?;-k)H@z08_jeRaa^-DV8^m_W3S;P z?2evpBtNhLZ)HOX%Qmod0|}Slt*kHM(sNn5o_BO9OV^d~+$AhsN5X}8>7o)M(3Ey1 zTu{u?j)X-ESlX6wKHh6fLIj%9TtWnz(x!w3^H7UMLTrwnmZI9ij_7G2s__M}BYHX` ztEq);(9=>_Ti6FZoe|d5Ld+>GWHrv6Ig6#Gq_$?@eubpQ(Wx_8O2}zIh=Y`n(tr>L zDJi3^$;YsikkL2_8IY0^+Cm&DB_*_l?5>oQ(AKz#EF~qhHFg|JNeOL@y^W=Wghmfz z$Fh`=&w#g$HB%Y!47=^97)wd%Yz@R2LOSD&?gM#B$YwlbsM>NAO9|ykO9|PG&ahK;DkGe!g^jFJ zQaW3WaITcj7GA5Al+IS;rYt3;vxP0JQ&Kux^&9b&kj~;Z>(Q<(CFQe)-PBW3K3mvM zJtgI{l$6fg`i`Zfbmre#EG4Bgw_KKz z(wSrGlakVz8>W#G(isM>VWcv`+2l1wO2}rBQEH?zq8YWNSV~G}w%`&oB_y*3ySG^l zPYKbCOgq}L`U9R4!dZ~!v?Wgo@hnI)+MK6^fCj1lfHvbPA)-Y~Q8 zgwPh`VwPjCP(9;#?4z>^vm| zx*#u#&j^tYQj0MoTZ7j{ zh<8CAM>a}I2zZd1f#h+X5+Yu->_G-gN(gyD9;+F|Q$oxO@(6j1r-Yyv^bUVH{EQrxplHcCp0dvTqIcuI{>i;OC8a$hch^keDJkt4xr^M*Q&QS90_OxzNomh8|Gbl@gtQm;2~Px1HTB4? zDIxqNIh&`1_!s1C=%|!X0D^3(spKi40t69h9Vwv%1UUm~9Vwv( zKx!(<89XHvfoM4$86zp73M4s=r-U*PLL~?iBd79| zPzr*aOkzAG)Pf*sB#ESiVh|)n(mW+pgCI$g;whmV1cCFGCxvU%<|1-MoVaHt@xZ& z6KpL$C)ETiD`!ceCh)lxWh^PR1Y5P1C8d^NNGVAQErAn|8IepaP9QLvA#@Xz7T!~4Vc!y0}jyfA!= z#>1DR_I)zEGCU(Z+PU4i8g&Fuan?D@P(^SeRvq+24Z(U&$o>`;1YfcrweLXvzzghC zvCqJAy8yck47dB*9Z@^bvVO7tfgJ{3v7WN-$6AA{t&6NP;d@wZEyMl-Q?1d~Agng% zU>#z)@IHLc5Agl`dF(E57r&8Tj`aqopmtydU&v?kiF_FEhw6bXc|9J%ii0oAcTqp^ z3G-g~9Oolg;nVx8 zhq?m;dQbIHZvTMpsvhqi7SP+O$HH;sfA-euac=K`?yR2R_6+C+^jx=lK+mTaxcLFS zq`JcG7SN07C2rS%UPLc;yJXXgPs$L7`0lHqM}D zyUhc7Dm@kMH^24N>NRfDfNnyoA5XC9iFCc|2M`23(e-_{HceLl&Gmx-HcikZw)60N zo<^rTemKRZQ|V0S^PqJqo#q?}=oC8D@#7~p9ZRP;?+2}8=vc>(CfRf}9pm`%BAbq) zqa8m$WYeSQDCe!&$?D4mu;CU(@g0=>h$Ue(g>R=x6kEId@n8@w|Zt$rJa(+u;sANFs;%~_8I0?nuzgYii7Wm>64c98Rkz~ z!e^L3X>p%n2BoQLMVTp-c9P$QIh3}+XPBR9sVo-P*82<-IW3*VVw$QuhG~_i>X%_& zrL7f_6o`8Gt8hgRkjH;FHIFT!yHOeg~u?7(o|6~%%U_^oD9<_O%)8oJW5kV z#W0c5RN*lgsM>R0DJV>(G*z?=vnEZIj>2S0Q$@%yh0;`^GAwn|RN*nKb<elAO%)r%`ZY}z7{dZKO%)i!vNcT=6~kgR zO%)QuYBfz262lrbO%)HrA~j7F55p=oO%)HrGBvG_`W@SM)Osu2uD#Yv;Wq8Go(eZ? zqp7lDSgWRWSI6ts*YXvvTTknzaGknZW}kDs4XlnP_c=FVRcW2o8P9juRJk*(%hOcZ zH1^P7ZHPK&*LY2pcf)!`O_i9#I!A4~I_Iicnkq4eS-Pgm++lXFsWN()|7)t$9t;aj zmF#0%OEp#E54MoDM)jFqtBC}a%&QD5N;^Tl{lY5LPW2h=S51|W%-GhNs(6;M^)*%T zEaOzrcKV-#D??M|NHe|}O_e{*_;9qRe9^8oM7CFDKpP;`SrO3sXsrrpJp?VwvuS;2 zML_G~EXD3!4_v1ypTqlL~_S+x2x=ZApKslMC!GN3aLPWGOT z>G)?N{@+hK>EQVPuIRO>{dYEM_^*#vMi)otMvsY(LY9B8XoqMs*!yAkNB7G=+52w_ zri+KeSZfa>B^xhozC^nHs>rSjeY-?IR(x!&M4;yrgY39^{9sWIX zT;8ysu^&Vw|Eui_k;DkZWMf5f`my4l)p zU5IRew6)GEM>YT1SP3w~8eny|+M)u01DpH@{uzIVzr>&9_w!q^BH&VfHmU%uM^?aM zJ{MU5qxccL7w^EE@jA%(|Iz#sb^Tw(u77*XJIou*%gyu5&1MXi`BHNpcK#b@4mJB> z@4q~=A#C#+@^-VZa1zsE;G(GPBl)#9)L@X0%NK%#u$th0iBE%Mm@v8E`VP}K8(B`c{cKJ zfU}u5up|R5-x_n|#SO2^TnE<0fpAaDW3MXtSxCgms&&`60UDR zghl@kd+!}4Rh9MsRyb#$eNH918wxr%T{InNa*zy45Cu$#fQpK7Km-&-LC`Tx<*1H2 zjyZ>MbPQv_G&<&padbvU9kYlzj5E~lyK2=saPRLv_kHetpXc8B;|GjAW2p? z(9%|tjBXofCCTRY+Wqi($>a8V`#L~!xPig%b&~vTU;r;g?lxV3Uo&~z!0+*@4KAWl6-4mW}QxwYYkiuB+nXn z;)Oa%jx`WQZ<73KApGGZxz)f`t963BYTya0bb_2};K~zpf_!S=ij_K1ajEgTD|CWQ z>beV;>jazBb;knPq^?_btWK~=U3b*wI#DsHy>-agCfK8{gH4-YkGk&iWjetgbsZXO zB-o>_JM220V2`>E8QTPV)OE<%CfK8{L&i429(5fuwh8vA>uS4nf<5ZG>kih5mHgKC z1+qt7x8Qc2V2`?P{{=e19(5gZvkCU7>yVpGut!~Y`@TBC9(CQk2Xulx>bkk}bb>wV zx*52DJ?gr>=IR7{)OB-L#qpq7VQzw@4n|xq~ zP8`aJ4jYL>7|}~3aWLcfX*#ilaoBjBSj;$ft4=Ip#B#vIL5!ov=)^+CQKNO@Kt?PF zOe|o;a=^p^jKfFh#QuyZ0ZHt~hz1&oeHpue`!Ei9Lnr1lqL)Tu9%FZpPRy-d+pQCO zGrocEVlT#yFLYuKBkDd9dop4TU}82S^eZuoabUYn%w%jEs1q|7p2)2Gr{WX)x|;$X=o%!rnT_UV>G)u3uCq zScU8Q_R|Sg;X3sCO0Wvoq2Wh@Rk-epTAg4Ou8RO!h3illl3*3Cg90X4h3f(#onRHN z!;p|*6~774`Mjbm7jch_AP11pYi&duzIJ z6SDT!bm9Wm-kR=ConY;)8H}T>y)|#)t+V#lbPUxA*4~=8al^9q*0kd!ThsEbjHGRI)an|0N+I~9D+FSEgA021ytqHd4IBRbWZtgg1Zw+puIBRbW#`HLAZ_T$s z9cS&Wv4O0;HQLs3*4`Qn?{U&z+gpPw$%^#aHK>wg1Ax`Q`!3}hV^p}F`DIf)=;98V+#YuxXz|W8hb8v$b zCmrVCdM8dwOuucH*SR^m11sa~~%~rrpf{5XPu6$S+C=)(~-m1=s4?iWZ~gD&N>~LyHLkjrz3mf{$rhv%-GY3 zlTOo1Ut2y&$A=^EdLxr(IPqcC<5zjE*71s1dn02e>o}`*WCU&oR_n-cAggs`7?9OE zG8D*a9q9$KT1R?-tk#ikAggtx3&?66=>)P`M+O5~ts@;kR_jPxhmMn4<82PY(TZGq zBW;6pyoHa(aFq2r(iYQk*6T<+j0uBX6Ab zI?~vzj-Kqj()$1aH#zMj#Afdb@T6 zf0nG)VfqVYwGKOwCRXck=zAS!wGLxgjk8*ZF$IXTT8F=Pu>YUeV;COuTJtw%&OFOp zZ>}&8GY>Fln-eihY&WB(W9%@#HaZzCT^{u+4^)dKfKZi!q07a@Tzf@>m2qf@}V$n?mVNLQo@9)c16G5mS>@2DDh zGQ1^xTlgB(4P>!~V10N+_%P(PW+QJr4B66X*a__jeI5D$s|cP8JrcSrbYtjJtOh(6 z4#EkcBe9NPF7o)JL!EFCB5)9X2!0lPC-^dQ`S)Wb!PUVFkAZ;7oPm2@rm%H$;QXR6DJuT2~U`4 zd?-AAn(?X26O0dp$G%{^Cp>1X@viXbF~;A8M~yb#5gs|-c;Dqw#@oWfMjCIryw&)t z@Q^o*H-vkJ7_SR=_ZY7UcMUWC;&Qj~s&L0U##Z4$9mY$-u|dX*!tF8Rg(~A=$^Wx( zw7>tK!u=Zk^4;Ni$uHjvo`?MMec>_5FW(~`gZ%P4$8(ThzAro}`M;Le42Jyj-QiKl zFW(oQh5Ygz;bF-CS#={6RIPkk*|OF4)s+46&Ea{9mHvT9a>oD#T#$V!{!foxw9m1_` z#_cYbjk|@LT8-O;2W&TfFWi5CajS5&zwtZazER_5;o82&O~N%z#w{+_8aE1uYmDoK z@r_<5jBoT>VJB!@Bdi_cYGF$oS5?2!ewJ}%^&9P1Hm;B>w$~Up2*2BE{GrNlEse{B zalc$D>;#P8x?DCc7q%SZH^Qc6TwMKM`ekGniGw)gjcUI&JlY~yg@8T%SXyFAl4OnCYX<51yg(~U!f z@%V7C@Yt=!VqrWoEE2{e!$HD$WLPLXa+GnP@Q9Jd0^#8!j01#+jW?FKJlxn{7>^J8 z2;=c#zAzpi<_Y8RVPBWKjk&@dUl@A{%*%uRHqHuzpbup43#Us zs5Qn4V+h+@7(>{8!VkL|pDPLc(3$MvZW1w-I)Eu+bzuxZ4Q2{FWh4 z3UE&iM~)9_HqYq z(7^tI*_g?Xz)k=S=nP;uKRI7G?>lce&pCf`?m>0_HRukIN5kt4=ni<4v)GxB`uyFT zUT2`w7fan}M}YV2*X?KRN9?=N{qG9<0y~BI{5tgiJIr2S&#|Z2qwG$*$*!@L{#k#C z9)NG^7ci&4SKq3y))(olK1-jXSL$V`(x0oRV|Rca-HL92wzb3h#`@U$o3#~{`dh5q zF|WVGDp==Q8_^Z;XmkhI$C_b{N6mh_)!z!DKfw3uQ}s7g0X&MC{f+9k=nrs~I$8Zf z9isL@1;7~9iB%9Gu>xXma|*fw#Ng@J#=ng1 z#(T!A#*@bV#;xcGaJ~^YHX0{jkAMZnY-57aYqX*pfC*pVGymJzAK)=`BfQCfDOPKo z?O*R-4p(4b>(Gs`;6o9qZw+oOGyzDHr|tDN zASaYw&KR!MX?C?6LSZXSuD0!M2wCZhsckou?KFAXK5xDE#~CLZ9a38lO}r`!Hz|Ah74{xa}@>TaU;M|ua}%O+1!<& zJjY3s(amQba?)gV)6bmlJ=IB**-gQz-qW2l+1(UurZZ%C^O?;~nk;V$HqjY2z3FE* zIcYY%>#+qV&amlS@7+iNo8ENUX-=9=ZwfX#X*Rv|v(wpaCy&hpN1(qwznyPkm#?P)T;DVRxT z$ol3pGo3V<-xN$ikN7m%-<5z~nho%J?^OC3Ho)n!sZN>=@cJE(dZ##PHo)t>EflZ; zj$p@oPMQtydT$%ngr+M7IR2j&v_wy{1745K>2Zb~a5~fKq}c(l-|-%uVF$b(%O>%i zvIAa^Ws?ZV0mmD~N=XD{fm84jR!XMH1gGFtIzu)%pLx|ulMznAD|Cjea6a>jlO{8q zf|uzG+u`(TFFR?n!|BXcI>UxIo!RQ7$q>hx@^CssmN;GZ5SA9E$rPvHK?=wgr{Er} zT1t@>0Vhr7I0aki4B6v+W{Z<1gPel<=?q!qeCB>9O(r=7_t6=$$@$EE zPMVBz1m!t&hOBZra}xz*mQ!#UdT^)7E~nrMvWpk^!k-2>9oa1k6)?Ns)`rrw8?nE z@j7iVt^oS0_-mc^F=Cy6+RKOnlC-Ca7wXg*jHo3^oz8d!aB~%}(5X#~=q;T(jqy<6 z#wuQ;QyUnU0#B_TU8+;3FkW-0POWELc$ZF{T*aGpY8~T}g*vsidUT0Som9oeI&~uB zVqCC>aS?EJ^@2q@wTkg(96fQ7vpiFe(pS-;&Gz>uDLqJ<3#;T zAdM6F-e%6wsmeg%t)KINPSHq#qo^fG(Le!2ElG-o2_Uv;NzotyoH0wMXovu!mLx?3 z1Rb5JQ#3pPr$3=nG&lgKPS+_K8h{gUEe#C7$y0TTh6Nz{jihK$pbIAI6b%W$Cno3= z4G6$-KpGB!qsHkJ4F-fGb&7@pAQJW|8VG>HaFm7tVDB)UqCo&Sq*teC2mqp%Bt-)N z5Va&Jl7ApF`Y95BAliwfNcw?jCz2xJ2cj%4MY0bZ)ZwJ6qVIVLBAKd+zUQU#a3@t2 zeMr<_oK#ix5xk*OB>MRAhCw<-k`F}nNQwj>h+KP$Rx+5 zS*J+Sz25%y-{=$xI*{HK$vH5JcST|jM9E5uq#THnl@tj%5Iu8JB;!C7?WIV>f%FDR z!h!S#Nx*?&yg`z0AWBwJB;G(&iKIxnfv6Hmk#GZ1C6Xf92BJzNMWSs3trRJ?*Y1!1 zmgE}sj+@JNsv_3jsOQYH?G$OX*B3p*bEZy_RO6D<&(J9nYP$M#og$eAp1oP8NTh)q zfh5tubsKex1R8isMyE)gf$LAvDH3P8V7*R}Gy_oymm*;Xt^$%Q15YgJ6p1nrm2fGN zWFRWxQY6T9?P{GOIR;|MbBe?mxEx4Q3_Ko^MM6wRkJl-ZVIZkjMTEUkRKlfLgQH82 z)hX8C=uube6l-u4m2fH6;3z8LQmnyI^e#!U21n7mB*hvWMemXnYj6~ma4FVcn72B` z8XP?k$Qm3y_!gaF4UVFBNs2W%irytD*5D|59i>=;a zIEu}vlB~f|6woDEgJF>CBx`UKwRB0=;OOKhb&@qWI%%>_vIa*dPSQ!%;3)Jj$r>C* zzojH=aP-OXI>{Ow9SLL&j*fjnCs~7|sK!gO21ilRlw=K#qM|9u8XSc{CRu}{FO1Yl z*5D}iuu8H9M^S>8WDSl&L6fY(QB>n4S%agf#!IpWN1>oe*5D`Msbw2HwryVvi3%y zvPst7XrDTrWbKVYu9B?1QOH%2wKqyN#;m>3KoI$V&-eaqp2)XYGapBl_-gYg%oOLE z)6CIkr`d@9aemZ@e`dU6ylgyyx#I7QtBniMJ?;!+t#O=jh_SCR13Lu_!ECV(z2koV zZ)E>7;a|c(_yv~M?uRv^#&1BE^t<0Jvs><7B~Qo!9=VaXb(gK4)zZG8v6*miC%(#a_+{u zfyCKKoXz8Td84g46AjcFFyB z+s$wa{Q5ink^YN*2CD^rudl>ffpg&zEZ2vif8aDd61xTT(-u|-w3W3D?${p9+U-%}<0wVe{jvo7fOCKdQQk z4Q2B~x#Ep(^XKYvwTgRSzVAkG49s`k=tl3&=6kzDH<^F0ri#dpFhAINYR4k;A8rIc z!F=0|;1rl|x)Iy~^9?tGBVfMfMsNYlSKJ6Tfcdf;!D2A~vP-nte0`T_llkf{(P`$^ zU80TVOFN?-kDAZ85ljQ~h3YQ_W0di;@c)!-ko5X62K{H0Y`EVT%pCJMH-ZCXzPL-Y z)_i`K=p^%5H-e#KKIKL*am**&2+ofwD>N%^kNM;-=VqFZyAg~W^HDc~;bZ=JmuRZ_ zr=1b*v&Y;B{*C#d8^NlQ&t0O| z%)57qUN!IBC3?mD!!FUw=54z~Tg_W{M&;q=UTy>@$dnbf6)VWR**ypQ$Gp*vVELHW zyAf<2^IA88wPRlGMzA)`zwHt|WWKda^q_guF3|(#4ZB2J%}1; zk<5$TbLf6$%KGJsLu5*7zhV)YlG?9WNv5RsDLUIMRh1CVkeL_WaGF`^CXF5lzy63T%*}HB(rZjf$=4rxgel3v`a_qRlESY)^cquA`}Io}nv#02 zU%b#fQC?@!LUWDq!X>7p^XnHbHcxVoFEUrVe3vQd|N4bDo06ccpSRdNUapyYs3}QI zcs=F{_xN4rO5r&Vn8yn5ImbLkc=n#=(ZaK4n@0)HoNNBVlG=yUV@f(7 zR*xy^e0V*kr10VJn3BSW#bX{KuQOqWDG6ElJ*K4d;qsW0&WFuoN;)4tk16SV7(M1f zd7WV+O-bj&>M1&qofO>PLaQ^}34B>wLO-a?m#4#mR4;ROjR6T4QQ&RHqaZE|u!^knG z$m@h7=H%*AS$)`?RDCL|FPoC6hh=6;@d9i)bD8|)U31Ol!Z$B8B~f30&7tNga{P)T z%uT|m&^kl-*DK7kgfBSWJXaWN?xa;q#T7QC{Y%AUHl?LZ#Q`_36iFek-u#_JlwvUN zb0g$3%tzb^`6ct6>gOF$HYZd+FHi~)U{sk>fB>h;l%fMzRp!|0`-E3zN&y1QDpLv& zU{RSPQykT%$V@z(@n|v!|O5!%khny%{JjvGNx1`z^XEv z<@madW~Qnf|KU`b zwQ~H}hJW z9rFKQ{{P6(V(k34M`$<9;s;_cfI!HDy)iyT4gV|H4dB7x9q0paX|NQGS7-8v1osQh z!j1sLgM-kQFN~c4e++EHT>f=*=6fV?H+BWMB5*+<6*wcXF0dT4`32S9e4W@CpeCT4 zpPetA4=|s9!Fdcj1K#Rf?Of<&oHL!1o#ogaU_WOjvj06!Gr9oyF`xg${;U0h{U`e_ z`v&_G^yWJgng8QZ$v@woZjZJH+x68-{txsUn9V<^Z`apoSf5z=zXtmO9HjTsll5@j zj`_T{enu7lyVwih3GDy(JL?MTJnL8125Ti|^aoh8km(;{wV*qnfjt2KQ}X<$pyU5h z{>A?J$n@`q`u~Ccs6POA;5*-^zIS}D_?|+4#yhYA;8I`77l%J^qVHJTO7c(V*x+q~ z+m>U4w+Rajb8PT7AxV*AgSTnWV4Y)ww+YJwb8PT7!A#4s!P|rdHaRwUo3P3?#|Cc` zR=MWb;BA7LmScmr31(W34c;a!ZOyU4+k|B|IW~BkV8`Xy;B7*lAjbx8Q_TRKWrMc~ zhJTg~-X=I`SvGi^uv{+725(a!sIzSFHeoSMmJQw}82(u{c$=`IFUtmR6Bh1d$>6oU zO_r6d_`7yf+0K%^>qDR8i_1=y{9T`~(OdLh>}1K|rQj@Y(aDm>OF`cq-#S@xc_}#B zdzOjm@a$K zJJQLL7ffeHVDI)Uxxo|+-SLW(B|n%hd&N7{$&w>XXL_-BdzL(53c9_$PL^C@3c9@A zPL_OO3Ocd%dX}7F3I=;Soh*676!fRxHMzqS(4Oj9@`owthi%leMS%$vLI~TVgp`@{TFM4p>f>++zx`Ig*nl{}{oJ zZ@nQWOAa!fvAu(xEP2Qj1ikf6mRw{Cu+f#1B_EkDbDS(W$q07*;I*ABdC3T{n>F@X znNqz>mGS>fuAbT9RZezNHTaijhm)OH4X|CMlbuivu&bYw9bXNwr=OGE%?*BXvg4}3 zcOC>|B`61+>=+5qraL>j8vID_U{p2u!Skb&9mzr2#@_bX5!Ex_Q!u<5P}A@1ujwBrhEy*Ldl2+g&wNe4knU=*y^MROs~UXn+3sXJtHBqZ&z;xn>i>CLB&h9se0yBbQ;e#R)d$yqn&I+HK6L1Y`vU8^-6X?HK1+lv;C_9ZCjs> zR)goUZGE;M2j$V8=bdcd>Y3-zgFIVT4W96P;$&;9!87P5o~@|{PkWwmvVE$-li0&P zO9KhrV$>TvOG62Q@&xn-&(dH*XRg3r>{%L4D7YLYCRrL#D7X}RJY;D|q2LnKn`CKF zsr*cNxs#<~g@UWfCpcLeSSYxhUX6wp3NE8pqrrs&+U7k=!wUt!rI)4wh638zJxfCj z1+=q!mIfILE~Y;W8fGZC2&;WFG|(U@FZW#JWN4_NGw0#+WN5IV;6kh&&d_i}!3Ee6 zCqn}c1=RIBLqiV#%=u1+1|0{-8N!%E?s6AKJ3r$yCQ5+S%RCRK_1~qvy;s?M!9( z0iS)2lc|nAl@>4=8hyOJ#xv0ZCPRY{_zch4IzwZR*W0-H44t8o2YAjVJ453RuGoa# zz%w-L;4nJFWN6RI@AgbioliLqiGh z%ENVr1`^=GKpIAXdmXGZG>8DF?4>g_ga9W@(HR;*fG9l4(C`7oMmHH6Jb=iyW@zXD z4j-d4G;k0O(-|5zfFrl+3=JAU^ghYZkO3SvLT6?&BHNmw;ew71*BKftfZaGsLj_@% z&d@*sMB+6=!vqkC*9;93z^)#hp&HQBmj|)&Cq}V#1mwOh65nB?8wky07T2D3=IW9^m)qA zKmf$EVupqRV11*`&>#TB17e1T03ZrUGBf}H(dQ{c@()DAlMIPJ5alfyl73)Kt(76+ zw}DoMRNreimUV_CA7!xh_>d&{z`Ymg%pk_4xPZhSM-N`AGbHsu6e(m#=z)ut=nTm` zaKT)iA(01SE0qjMJP=!{WJutFC_KoJyaUm$Cqv>6#8xU9l6K(yeRPI|9XM~k&XBAF z=kBdDB4KgI#KvWxK zNVI{wjn^5HY~Z-vbcO^QI2L!h!8itg3MAGzItoWgs)4Zl4KyJH)#@NU~7v`lNY-2-E> zgJ3g!0u$>5wxM6(tIku-1J3Qvwa&$Oh+vmssk6T`%bDN|#TtQrj*T4zzp_6-ufXT9 zM&K^{M*C8`XrGJy16QI`-~xM=Jsy1mo9&40#omD*=-2gA*g5cb`f^>=XX}&EA8-lQ z1x$fUFi_WN)B3^s#Ci*z0UyGOfUB(wuv_2;>v(IawXd~1dIEM}Er6|lR@>A&>LsiM zxJzA+Zh&dE8T$k-Qw!8AwHx+BXjCEe0sP8*A3Fp-Vcu)rY+maBhyOLK?Z3}|i~lmz z@Sla1{l{Wvzqfxf`t`N>YyF1rd*8>X;C~i#`rCY0A>;omtm$9jJJh$2Z@O<(by5%O zsOof9=3`2d-IWQMk`z;A5~d`@RGEb-NrYFXVM-F(m3f$ww0LDArX;;xnJ%dp-2V^r zG9@YV%EU}b>bo*CQ<6ZZsIQU)dSz~=BrRTp{(Q1(Y z$m)$o52ndV5)O?X%#)R5z#Bc7C@aZ;Q}mOP40xjlb6_Po??w+M!AdgHjoA9?2PGNl zMr?QWlafp}Mc*sQhg0;OlDsw|_{x%1Z}ebZtRyi)(YH!+*Nq-bhLvQN8$Fl}E6H6q zdN2c4lDls7U<#}x&)n$2R9Q(5nxbt=^4g6aOo^4`pc_4y1}n)yH+nD+R#(cOwg(eo zB{}Fu+9gX#p1F~>$x@P$Zp4C*HVItt2Dei0!dnQj&vi z#1^10D#%zu^aNPyXJdAUlu=P;X9@m-?Jm88HblUwyG_Z()o>fBwTOO7K)6SpCwCFy~d9-3XIjwaJYz>s6<@ z5vI!O^j)I!)kZhM#8;i_Mwt1kQ``u%U3Ib>r92t6){QX5RVTR-row97E>S|ASWSA- z%vP;(gEP-mtKI9+>QtRj&4Pr`Rg&h$ zY*!sym1<0Em885eaaD)Q@zv)kNt|MCt0c{hxvM%WGiC@fDRj<5G zU0*dsxVBD7;ulj^C5hk08YPL}#X{;bGjQfu4@{*_wqMlik91~-DCreyn* zibJS!?m1YIN{W3N$pBSSYSc*Ps*-Z4#xg8jB}G?_W%$5KinSWaJXTUTRx!Ypl%`eu zboH40e`M(^DcP&!DAZrwb4YBcciafs5cQrLA%&toa3kbh)W>dwWR3c)`U`7VT2_*8 zhhv~5-ww+_waYb&mZ*Wk3+5`xx5G71t#W+-1C-?6;TtH)y~8+AO>)gX`>ICa`THoz ztHV1`l81+Rpd=3uUqf}s^?T1#l9Pv(pd=3u13^jd9S(w$oIET9C3$#w2ntzSn!@^E zA}Gnp!$nY%ONWi1BtH*-LiLmDr%qRrtA~@I0&;xfBqjNIcnM1K^Dq;X;C;CqR zuk-)2{~z@KUx?kKreQ0$ZtPfAhYJ3GqkrE=p}&S+!VUoUhi(g98~RNsj~f0Bn8_dY zf6D&{QOEy7@bloi*pKg-;KSGl;HKc^sN_!uPe&KOUj&y155Sy$a&Tm2CxBY?@%vZc ztH6hWw*oI>R)1gM_kn8y7o(d0Z0yZ&FFpEA{+=fXDd`yYs!{yn>ni zgU%h!4bG*g=%;ReYp_4xq1Xvvjxz-{{hdyeQ-dA)ezw2F?EXzu0X$~ki>m&s?ThRz z_UJnW^ZRA?LD&gkx;@tJv0LrFwvAo-zR@4+zv->|3B3i?0N3eDbOF`<8}({^G-mqy z=oxyv9*T;9{yM0=*7w%u=_eU)wQK;8ex=HJY% z@Bp@$x0~0QmzV|fTyrDVcpPmmG50ZNnB(E}w441+SZ~JnSmp6|bOd{_uMy86%7iqrr$62KoYQ^S|eR-T#dLLI3UO7kHsRg_Q&+`j7T6 z^3U~8M`wT@f2+SA>IJ<2+dcr7!(B+?rt|0m8`2$pzSIRaq&vRsqYG?EcOd&-U_-hC z>sJbFNOxelU4aeh4lK7Tup!-nUYP|pq&t+>1vaEROr;BKNOu^fF0dip;Ww-T8Pc}5 z!*3NTeze_DwhClN+ulK-iW_YYD(eE9(Xqat>jIn6vCsSJ0-MpXx^23^W^`;@on0U^ z+UJdV&M!L!a-)5|7;P%%6v&UJ08g=YfgEYNDt@+8AWND~oa>1@1u~^6pdC^RWJ^;( zJERtdR|DE1wLsQ1ouM643uI1HKuz`wy$HO%7#SRg9Y}dV`p!+ z3*=AZ;?17(b)n)=dt;|=)&;hxV;d8?z!r53{q_rNQOB?vy1*873~QtdY*EMhf1(R) zQOEjyrVDIQ$3E+)3v5xxj>C_!MIAfgVqIX1I(GaCy1*8746C6FY*ELs?zzAgb?g_* zb%8DF7&bsHutgoiuBQdIsAES0*`kgeeVHz>MIFOls0FsDV^~C3V2e6-*^#=y7Ikdt z)w;kIb?mUEy1*873@Zl<5&E5YV^}#@V2e7o6hFomb!G~SV2e5if4;yLbqxM|fi3D7{P_Y~)G;g!EU-l#n*(HvIyUbfU0{nk z1}DD27Ih2@0}E_X$FMLk&lYv;o;fKL5(JX_Q; zIPrP5sACUL*Lk+6W8;8qQO71ft@CVA$0knJdA6uyu;24+QO97v=h>o;!G6!PMID3v zo@a|X_VhTNXNx*E`Z=9n&A-0qM(aFV)UnaHfGz4+500+n3x>a@^K4Pah7Q;H6?_z) zdww}17DnfP!HDcq{y4_hdUXC+#&+N_jDz3S`J)+8Xp}#S5gDWWk=1KsI=_q&&Byac zFn-cs=MQIW`B3K%V{B^C`K63-;`4_xe%PS%hcGta6%MXqqs}j3jDD)~iy1$S>ii-` zGyu*YRJ{QI+d@Y8^Z5fAu}nF?fDwtN`~i&E_&vWrBTAL>`!PZ+^ZPQQR4Km?qZ739 z^MSTE=2-c8bkL5Kb$)K;bo*G~-c_8Y^Q^}0)5hvNt8x1jAggivWFV_?JMuDlR^#@u zlXRZdxP2myvKqHf0J0jlPX)3Xw@;a>^Q^}0lc(rBt8x2yTtI5<^|sSpR*_?GJKbfZ z$2dxNSw)b&?YrSxR^;}vxZzll+sERDV?}PqLeo4ea{JJcI!}s>SLnu_LxK$Kd0pp8 zj_D}w5fWqI>)kp}S`2Ilk`eZ7F!0kdB45ZtI6c{)bpRJ<5Kzufq z-}Y8q!1CLUYPCGeZ#$~h@)h~@wxe1t&+6Mg79XC~w;iiM^Q^w@Nay5PecO?Y%(MEo zW37Fj)wdm)ojj{=JKZ^~zU_49u==*sox|$ejud5{)wg{te!r}~?R{!>p4GP%iB;(%Uu=zkQb8HY|nBv-GxMDQupl zw+$KDJWFre{F8N_rMC_1`SUEjZCKBrXX$ODzZsU^Hu|_My>0YyS$f+-VLMOK>+`mG z&cS;p?X|saAuC^zUK_hQ_ZAAlv94l|@0K6D0Z)-iSW#w(98bwy#){h7194l{YKOie_E4C%bvGTU!Q|4HC zTk$D#ti0HvTjyALTk$D#th}vwqRO%IwxT(Bj+M6+^^7@I-d5YOa-_Vrx7D_C72&n9 zwNj3Cw`B;Bb+={EmpaF~+cFUKo2sG{2p0%TWaveS$A7%@p)NyTX4JPSa)0Ste9inZK2Q0x{I~II>)-(LZ6Ivw}n0# z>uw7wHFK=HEojY)-(j9he%b+?%+bXj+sF>vNscblPvIo91~G{(=d?lw2$8)n^Y zejl&Py4y^}x~#j+^f$q}+uZP>&av(`V+_r)?lw2z$5?lpsaThFx0(KESa+Li@U^h+ zHb=MX9P4f~NZUQvg$TXnyPcGx=oX&=p3o8*V{A-7gS`| z+cXTf3+XP74#VA05ngW--6O2LO?3BGl-JwTGnD%O_wjA_V4t{)(J5{-=7!761?DVs zH?te7|3gOE_{w-6v%)8gd(rv-QX_AiiIxAy7>kU(FdrO>b^kSnKk{|tACcE0Pe$&G z+!DDgQiz-tSr<7L9sl=+YcMP_FcOUf!sYNc;g7;^g^BYXyqv6}TaILU37dVQ??(88|Z7ft3RPo$UX0&L!v( zc(!wjbG&o7vjBSqPI87jF{i&1u*>#0_DA+x@CP2X|7hQ0Ux^NZNqe(>qJ50L1nUHL zhdaQl{!RUp-lpHxuc89rfvPu?4zyJ6Ct1*L5`_J&N^&jUy z1ik%cpyGcBX7F`>%lETyyYGG9>)6NdA=LX{@4E!u{m%7m@SWgW=3D673w{1pi)j_B zsk63L4W?M18tWzDa3AYM;b7Q$K^PCH&sP^5;R*G*>Vl(K*?Ly4*jA^-S*N+TIN~(H z6gxBaY1Dc{uK%RJ^>^V98?3(yzuj)VEBsoI^^x#%qpj`2Pmi;{5`K8P^^@>Db1bpz zXuc*EAk7rThNKy$SeP_P6)Th`&0^DH;;p-@uZgE@>nY)>W34|4Pnl{x;__JQN#V&; ztcP75Yds-6X{`0Q@Wjd1gD#J?9uuB0(R#q;vDTx)<0n{dc>tB(tG|oYOy&0fQ{I7{ z+5ZE}JKS$?to4xaZeuMe?Pwo6&3amnkDY4$&gCiAEiO;Cu6KEo^=FqST9>*!!MfPx z@z#AV?`GZWa@l%Dc;tBN9^s)Qtv?F)yl&kk+&$E~+vOhXPT`KXt=olT9oB8a?cLTN zT#i}47jA90ZWeB7wQdq_Zn16@Zfdq}5N>F)t`n|ru&xyzP;Xr$+<$;|wQ#h*b(L_x zsCA`q-+tB=!gYPE%Y|#}tjmP^)LOq4j`XoE5k^z9-v~nkFA@#}tqX-QmHKs64>6az zpsI(MOr0-RK*w$se!bheqk1)FS?5)+29_w3hBo(=8O&MLv>MH{tU~o?F;TLT!sF&! zMd6-tR$jQH$I1y0?69)J^#iSpaJ1e^3-^s$DdF0_RzesBv2kIH3cnJ@Z|7WL{C3U} zMoZYUtDg(=t~0BjtF3JPvU&w(Ue+1Htx66jS!;#y2~QHnCp=LYpKy&ZKH+L%e8N@2fsl2AFuwnl z!kB>_U;Qm$3bvy9TWBp?%jJrX2UsUp851(=7s7+Sw2l)VILJEIaW;kJR+(ZWq_ z)=|RvOF2?_K!deRxORYbm~hPy>j;89Fh1=PVSL)f!uYg{ zgfT5UsQSq9zI{?ipq6>GBY3 zcj3YBTT_Gw4Yno;#|Bvwh1+A+1mU)JYrJr4k2THZ7HhIFG;yrU&DK=m<~D0Lms_pj zF1J`?gd3WzkuHB|Nj(B4W7Y^cUjLDGfN*rXHB=Z{*((gK93l)g=n<}|w}!b~Yjq2U zYphOT{Cx}-4u-4_VLUGm64s6t6UKC`y?V!BJ~ps=$26C%Ho0PZjnyUmQN6W57|)2K zgx_zr#tGvYak}vHqpUeq#>~!Y6`nrHY8IY0$!ZjyI?1XRo-)ZAAUtVztHtH%R+G!q ztOl2-TK!#~VntmpTQ$O?CRzQ2hmErOx;)g9XHZP}tXetVGt}xM+}&eEggd*fu<+nc zDc!hLE~SvcHB?GO%z)xU)ULG>?Tyy2gPZAbl6Slj9+VN0tYt9MV6rGBX1Jxyix zeRUkAg}UlH_u&{Tb=9|Sgr&Ob8#lu8T=kV3VSTRp(v7e{S8aDAtk6|oxDl4tB zHM;6^H^RDH^{E?SVXpebjj$M3edI=1jjKL%Bdowx|8OHL!By|O5tiMmcOBnX-df*i zPh@Fi|H!P!gvikU4`%DHBeVYyR`y?y&i?;bwfg;ofj~L%P2eMB^@4yu7&Nnbnhf4h)oKKv;Vm<#O$mU<;T;L>-%U|Uza}LBjeIly# z+nhS&@qe;E$G&`9?LXW1ppXA$sL?+MIs6s&QhPsp26pA^Mi2jp?a^Q9_w>u?-+vc! z_ZMPEz72W>`tr>~h5iW4&73Z3$kF%ly{EibRewa}50Qr=-}v_R zE%%*`we)}V-Guq|df%auzeJvmeCQkPKOu6he*!x6&5oRi8F{VmN52`lD3Xqx?wf9Y zjVk|F%s-oV`(E}v1{dcdGg-~>FE;0y)6Fqvm)UIAn#%a6@dY~mzlQw&gT^0>>x|$0 z->v5Gf9QYH|GfWC{=3oj|1y8c|11ASFN$!m4gh}2^Z-!rSgEu?FFS|kOd*Q8a@Lp^9 zrJe5~>&b>++$Aiph1(?Wu)G#$pN}l}Sa{3s3#>jV{Fxi9Ssnh=4OXuSf8qwKR);@! zgY#B}KXQY`H-S%7z3*|MN8nirQbu@Hnd2H)wXw~wl*3rdU~tabGHM$5w&m4nCw7ZriXLsLhnPRoNERfNccSV!m^Ee~;26Cw{{ zR1_i)VpJ6(4`NgnA`f3w7b3$v3Pbd?>M+pZ*Hf#*Ku1~2;~5p7$Ri(Bs>nk@2TUq? zyzF?ZL(7A2$K#Xqk=0j3HA$rr66R&VN$!1#Cisc)vpd7Qrx1Q-XNVnr!%y!Fvz}b| zshwd4_wMOn^3D+NIpqFw0v^2Q(8Qf* z@t#8yb_vTHLVL)cRbbt!&@wkzyDoHu8=SN@bhsO=SshyH2CGjB9kw%A6FSrlR;>;l zvNJd@bfgWwh(_?BtTw8Y*6Fd;$Jt74 zogRaiA=isa@box0h@Y)TyVs%APLFaU)Y|EhZiHexJ;IGpZKsF35z6iKFgHTIogPa6 z0iJ#;1$Q#(rHZ>Da)OHMWX?(TcQON}@;aH_QhlAwZm9xKX24WoC$n2Bo0EAX)y>JA zlM3f#4oQ`BGXJE~IhjvV^_|RDsg6$OrBpH}b4#k3lldf7)ye#m3g~2RNwsn^|D>`w znKx45oXjn$v`*%nR3s;JNUD;PIV6?I$sCgEg>o{7q)IuNLsH$H%uA_kPUe$T zAt&=jDv^_UBh|>sypam(WS&U{b25iSZJZ@@MHI(bGFL=(oF#Kbl+sx;w?wI&CG$p9 z#aTa8AAV34XMHb>9LsmY$g6xSyxVx|8{u)&tsh<9&H7q+^f>Dam!Gq~6o%XMx$w~8 z)@Qg$KWFeISgv{Xc}^FugC_-eY~}a?Fwk)L6@h);n^% zsl|F*7<2f)2{*J`Z@JuPy(!$k!Ft{0PjtI5-1XO7{?z)5Fx;b8UH(iD6o!-Zip!tt ziNbK;Uv_z$7FX;)Fbj!^ldjB~szMiBbzXQobb_Z+>sE!oaD%n$!rR>7q_yGC-C+Lw zF!dqlpSfdx_#HR6bAI@3H<-00{I{LK{o%jv3}%Jja)bM4h2Nz5|B=2`|8@QUtjNU3 z@W`M@eFPg-hkp!j3%?hB9gnS#gzrW!|BCPh;S~D!uMZy|J|g_zssI14`u`p3*8dL< zHU|3yP4x2L9{5M#FM;Rb6x@SV{Z|Dp3}galqMQHmfg=J7;T22^jKRA87Ptl4`8WFc ze+0kaCFgNxi}MHPdgr%J5gq-h{{I-~U}s-+0G!|qLw0|F6GBh_AK)9jYrkecV?T`5 z{WsZ{+vg*{zX|#MW9>uW9n7*P+QaQZ_5eF%`}7a`b7c5mLxz6~I{ROxFMxw^nqGz7 z{|-cce+zO=f5Ai`;S9zzsZVNKJ}gY zSiPm5SC6PWvC{t%l|yEKojOh(tmdQEf3zB`>e1ofH2-OCH~)dm{`2Oe<~`{0f0cQm znL$T~_2%*B5#~bd{5P%Q1T?`3(8j-wuZ@q4zZx$Yk7GxN+l*`B1>}u$j19&r<0xaX zG2hq&dph(Q1C9Ph$ng7r@_+7s7ySdC@jr}R18(wP?myq3^l!qx0Z03n`1kSeUaj`8 zMXkU;ecOER`Cj!sjaq>_eK-0p^PPvi15U#&hJQ|p3gCRcUhKv8wo{@KI0|0IHbW&Y zg6mzdQkS?0u6LgWy2M3rz5C49B`$*NMdOkZ7s2(;!BH-P>&4zEB`$*Now=t|q9QnY z<$FCZ>k^m2_3k#)DNz|59ly(SuY*mzIk?4hmo9NBTyN)YPKiq4=%Q;qx9AcV!}Sj8 z)Fmp0^Ll$@gX|Jj!}+|u9(3<3Q9T@8{u|G=y2KT6z0FO!#1(P9%`si#in!jUW~W3I zadb5`g(^`^9D?$3o{M#ftKxd=@uOT7*NgrgC9aC=#g0lPu8QkLZBvP>;(9TcE^$>{ zFN%UoTou=gQn(UV#r2}}uf$bxz2S&cqN+Ih&(HE)Y?Y`k&i3|(tx}~d&hAClkgDQr zFEU9bDvI;kDDo>+isHOI6M{4>qaaCLoYPm{W71slEpu|;iJxCsus4C9( z_E=V_QWR(RlPmDit9$1Zi%bny8A?RiL2te`$Tk!tKz!rzS1SGit9$1Zi%bnx>2%L;;Ojr z2)WY8IRxk%_ncj#N;#jm%kwzi z4VB8#RgcrVp<+1-oW1E@+^ zf--a{#6fva&wWlQSUqzu1%YaCZTS$aKa&8PE0%0GxX~$TH@LwmS=Hbs&$UiTRf8KT zFss206d2XudJ6p2;9Af1PRYkX`4Bqet)3}iKgE)#8eD`k#WURCLZ^6oHMkJH>WZ7I zK>=Ge7B@+NB~HcDs=@hqwIZv07h2jOV3ns^=RBuKDo?++f~VvZN$2U;yM`_!rKdA# zx{S1*f^+cA6-n(Y0lv8+={*JKdeTmj6rV3U*D12*ca=9`5?5r+?^=7CE|TVZy$z7KS?Rl`%+p0y z`YtRaD6-Ob4IitEtn^*IZ|Nc{eOK=*y2wi3^-8ZUveI{T4A(_cdi;Mde~YB^z`?kn zBJ?;qSQkm@aTH!sgAd^qu3M(M4AJ&T->)k(Is^HP}T`dat+h z8T?8sQt#~?2CQg3aH}qo)Z>B?!*r3AzH`_JU1X*2M8muyD}5(wu#2qpooJ0$WTo%y zen%Hs={q~Rb&-|6lfDU7`cC?tv(k4C#E-GkcedjwD}5(ggB4lnJF$&ek(Iu)|5v)m zO5fSHzb>-UclO1vl$E}de$lM-oe}&PD}5*S11qx9chYZ{lpdcABS^6#_1;d56-Cl| z9K~2sB&7#ptSFMs12I+EMi;Dq?HNxp$}H;{M(N8l()H*g@nY!Yr@H-3*K z+jJD)C5bj+moAcI13Pg62{s+=L`yHmE*vGXrVDT_Ni}dVj*?K*wfGApnFbERQ4(oj z3`i197Yx!x5@^DhE|NS0TRU}u#2JWs`T|Ka5LNaC5@sMu>kA~yz`i(2qD+Xzt0c+5 z1|SJC5Nj?9B*%pPb%Defh?PYJl42m9ISVAjK>RruNQQy5{FOu)h;sJ=NigC2xrcy1?=~*be9d%kN<1Pzxl#=s2vcLPdS;!FVYaUrKNlD&p(yKw*4=wYLL> z@dei24iv^0j;s7%9jJycu=aMKM!vw>ODVMiYj4M79A)jL0I6z{*RhwgM|JWzGt$yp(h+ zu<}w4t-#7l>9+zaZwIn(1y3aq@yxl#YWpZynljDCjg|F{1u{|EjzcCP6*9>DDIYU9^N(%59I zHjXqF8go$fKg<}2O!IHdtocjy3_KQV$>(8yIm+xX2bck42UZOH!+70z+HSPN=z8}p z_PTphKc^qocOa*JF?u(ip-2BG5JvHop+X}xc~W<6zXv3_q|g>~== z?02`yT4o(+?P*Q4hFWb_9oE4Aq&`RQyRF#q?jCisx=a<-IavR`0=4e@sTpb{y4Sp?ZZ zRsrc|a~n~|!F{Q{+7)*}QNdlu9UXTOSKM{fany0fWyZC>_he*5wa(o8-RJr0_j|s3 zFZ{#%W@JTGM@F1CBjUt4*H|g*57s7Yg*6*q|3j_5R)y8xV(|I@0x|wyG9NSVgvWo* zJkLB8UjFmVDdtGCpW&~A;|01flC{nIA(4_Akt~Qz~%szd&+ViO}d-vgLH>kp_>&?{`RE61NA6Gj_ z6=ro0uC|~m%&KaxHkvBT%4)85sz9o^+MKE|%RlBj^e^aE&Tm!3>eE-W)1-GF(N>eX z-lB~rJ@$!~npF1?Ei|dB7BNjKt3|6KsS>Ux~ylb*fAzclI5Q~a|?_K9ybK~sIB37YC_P0&=|6-kx&hbCyMuQWkZsfF)~Fq%p& zd{>0gRBGY7B8;X|3*QxCG}YgA1!$@tG}%4GQ65PZp%J^qwW=gEY=pQ*lcB@JR!xQs z6<2G5M$|nqghtdoF@#3cJu!qv)SWMczN7nF2o0i#Xb26WhiC{5qWfs5vP$Uj8bX8U z#~4C`=#dpdgXobJLW5kde<&K{I!(|Zx>JPEAbRwN(0BBZ38F!it3yQ)4WhgoDuQSb z<=s#bM1#y!RR_@^Cuo8OS*ZycWQ8VZkmZ`7L6&HO20306G{|C2&>%~Tq)IH(1P!A6 zD=LC$5aq{F5k!M1KaPqZ8btYVR0Po=%5|e6hz41xYeR!*!xcn>tWqTw8YH3#8bn)K z3k?#|C1{YKCTI|~Axbm4yL|IVQ@T49#_a75g)ynysc|>^c7-v4+o3RKaJOpQ+P+0$ zyuEE#7&Et<6~=^ao5n5dn-s?6?FNN0d%Ip?Oy90k81uJl6~+Ya8ig@~+o~|8a91k~ zFQ}`0pQS3xzS8%C!Yu9zbq8jTH!6%7>TL?+Rd|=em=WKjFs9ndNue?le-|m7q^eX# zYU9oXO=@b9rdgHrs&U3?(zBN{R+D#oI!7xKMs4aDR)$fVdWMx@)TW+cWf-+t`ZK7_ z)C9E!6bZ%O70Q94GE^HEO4lkwHMK(NT4e}!Q|Ve|2zFEHT4m_no`#g=S>~l1(w12O0cl!*D%k9kyV{U)C!kFCu zPGL;xPt~|!H)#B!y;fn&>Q7P_)A~&sx3@PcjEVgQg)y^VuQ1jLPSLony-wrL?UNP8 zl>S78F{fXnFede@6-LghRle1zwCxjotATm_N_7WT4jL84n#E}fW5p(^Fcy7seiy5V zj2MBG?f&~`WW;W#pC-dbIEQF5beMCnCPRigeKqN~2PeQ)1qb(Y#%R*_U__BqC4Kuk zqcrK$7y0E>N$)<+2uwxw_iFJ{ zq|yAld4Awe#`Z`o^lk9DfNjVGQM1#?403r=Wcig?f91R->l%~!vl3w8y2r^ zs9V0OVbRKlx_D#Xx(%z>u4r7lcyUABNsHI6u3fXKap~$+b&Y*%7cDyc(0&UJ=~LIR zcA-}`7av`}cx|2k-$Wqonf6@FkjnvZf8#b+4xMm5yqY8gRk5Ya|cPU6R_Dt!iAnWbJ%v|AxAT#`$YkE^ch3ujn>ExOrsh zzhZ8&iMmb+ZZ1tU=ZL}w=ca-Q;mY!+m+0w>EG?uzPLED+Llb>fBh9Oo^zKi!r#zlW^i-A zl5`&Z1&i0N_3TdTmYu2n>w$SU8bJf?h5@p7nCHa~LIcnD~Ub3vAaqT9w zPRkj=&3#K*xBAg7N@fr|Q(U)riG?@42@)KTzKBzU&^YNQ?1d- z!Oc~rsa3tYIwc$Vey>>GfFZMH{#x(FRZI_Vt}o3GmVJ(DM?>(^Sy@?cGb_tVZ)N>= zJG*3~MB5O&IifitC3}dv;${sJE?I-fF}PsevXzS#(;HW)WJXoi+{_{cOI&rc_OOl# zZthi*PVK4K>a{eqO}sMqVx^e>Hv6Tt|KB0J#)w@T8|=R6u8(|ytn~5lpWF`7J8|^y zE1b{ojmY|q;z0f|c9~nTU2F!T++P=7V^1pO+?JO~a;ztGzYi+LEp7(F4ZM$M3EwX{TxS^3$tiudV*f zrnpK~>e)xL_tusrdzJrhosUfoZmuoKE3y97=o-|8KT{O14JlTPXQovu(JNE#UiKK&zv_-XbX3N~ZdT ze_vcLxz_!6tWr1MZT&A3qGp?wrEVmxssHV}iKVj_a!fa~7YE?{ztPj!X2iC|rnx`j z9RH~3r^wBBi1UWC*(t}&|0L0cKgyT!D7&5=ZGUF}!R`@xA<__W5o>;W=o@5|iwB<# zZVa}wc3M--uiz(M6L>1HG!Q|hrTsUvXVBPucq!ID)W|CC|Ih7#x`&sr?%x`oIo0@r zp(Xr+-x%fvRjB{S64tM~dvQq3uEci?F5!3l+W0T5K=ntLuzLL`G#jxCyZ@JcNpW-g zxN>~Sz*77rdaQ_JpyK}j!UIuxDNPW*gPcd^s;zEX$DerszNC~o)@)eKXkV&! z*RA94{~y;5HJ8?=sYXGY%x{a@OVTCv@BZ!n|J*kCijqkDufIa?{~y~LU(m0lJyXp_ z7q>#y6G~e3f7<__*%Dt#U(Kaa){Nk+ck2EW?>pT<#peMx@sulE0EGSpkj zzTDi(+NirE>ZVPJzyCiyf|^S*N_5R~|Npo!DlW-G)D<^xkWi@_M2^AY{{OK-)LfDQ zC~I!Ej(V(B+Nzti#}pG)_bNrFZnpnFB~YqLoBdLn|3{*0jo1ybDLDVW#qAw^26X?w z{VlM+1@^bV{ubEZ0{dHFe+%qyf&DG8zXkTU!2TBa|J(x8>oJWh$rS%LE^rHbqvDb- zUjGLdxM_9xlF}^K{K^GxaUAuQWQEh;lhB)gzjA?FP>ZiA$@a=`zozH{H@ODiQIf5# zq6^&gUZ}HVo#xl`|3KVaVIW4{r?IzVdt$p|4@ z{^Gpjyo}U34Gm=TDN5O>O*1DvR1ihqjF#ruey_l$U0 z+$nBGrof~)N1P@a#8NR^Oh)v)0iutn76*w|BEo;-f9D_bef&j4(7TWC;9GfypU*e* z^?U`Ni=2T+@&UY#cjIk2W52M!vya&u?0NPWV(8t%wz3R6pKWIA*$Os~O=U;1!HA;Q zgLP)DSj7GbxdT78_t`HZd*FTcZT59`-oD5_3mF7g*$WZvaGX8N?q}E7U6Dml*hb{r z$Y;m_uon>z{}{PFazo_u$iR@7B1c4eM=By6kOv@$sE1#MKMcPfelGkl zvI*XZOaK>$&kU~*uL#cxPYRC;AC8QI<>3PmC(jK1GxT}r{m`qSXF?B$?hM@=x-yhR zPQcR;sc}hYW@sF80`?76h7Jrl!2+7EP0csC_pVp;E}*nJVhaDspNSBB^0EVN}dWl!c!EcmP(#6p64kF zR7>P(;~Ac!P_=a1lXM#et0nSC;7OjMaJ599pb`pLD@&f>DGFIj6xNo=jmCDKq`xwm0S?G zoF^&3Es-sO3wV-3+|q4ZjHh{$g4_~0&q(nkg}Ehi4#JKnDbTIF?Hrz@P`4n3jm8$9 zq+quoXpnn&lEU2*Ss&QKk`(YZVAiiUw%AE8mXauCkLd z=577bC3aHAyscl6vy(FBZ9Q^6C1uRpdZc_x%9ywHOP1M58S}P2hmXmaxAn;Pl$0@V z>*wLBjCor>Z?2t`F>mV^U2P|2%-i|}i|nL~d0Rh!ft{2wZ|mpIx05pFZT;2wn2dQ_ zKX)EaQp{WW^JWEbT}HjFpD~vwDe5ghQ%TCG|xb+D0l$0@W>np45q>O=Ej|k{V83VVzyuwZ%i8=y-dSsWEQxl9T{&B(9PU(AANVWH}(i$0A9RfB@@VU`Y~yfLV8%ahaVY`yU9@Z9L6RlJy5% zx6w|L?FU@D&Q6l$2VA|@PLkaRT(;UylGO)Xw2URm=A*WG#tu74CLfpQV{DMY2b_li zLFOLts(E&jj6L9NKr;1!$IZ5rWat5B9cL%W%mYrJWhcqV10FryPLhcS9CNgtBm)mP zbc~%O^G8FgHJ7#|~(4hU6EN`qbp zvpva9l0nDy!-w#sH0O23-NyZPlFT`-A96TLdd56ZXY4>9vvAoo>tKEF=1FPG>x|pT z;7VJLyNug-Qrhym!Xz|Rf^0deLZ*TQS#rQ$`&h!WnQTC09Y~PD21I{Okhum#irEAiYe2`f6P~FK)S>4lq@}LoB9b6W z%>s2ilJE>Qt1H+EX{X~GF0d2QPRAQ?Rodxz;|4n+?R31M(N0J^9lxN#PDnc)Ujn$$ z``Y-NC3ZsE>G+(jc0$_e_`ErGLfYy0oOyOa+UfXQT$Oe@K4-3-kajx06<4L5j?V;? zb~-+Fr=1`>9SFpcydoj3bbJz?7FlUr-7(2dkc|c$H`7i?3mqSit7M_+>UcXr_8D;J zI6EP&bG$zwS!Z0W{oGEFZ3gUFYbT^-j`zgVB+HDeJ$l*+X_w<&a8=snc=s-LLfYkc zm+p2#+U0ncayudIa=aUEkajs<4k+z%940y;?Q*;;u1dQc@6ydqNV^>G($!9oUB(x; z>2D{@gq=U4pDJAoUT? z90%gmN5~%IDk5$sq&1Esvrs}><9Nij6Ve*T!;wgWtT77!COl)z;srY)ZE@`sKxvC> zr`=*Fq%E#JW}2Olwzw9#5);xE*WNP4PDoo^I~Gvd;##B}Nl05KO>eQcnekjA*S_v>~-8spm9-gZJ7 zK5oshP;wi~*Z zw8gbuaD%kPwbWgtEv|hZok-f^+D?DA6Vevf{<))_khZwCBW{qkxVB>_J0Wdx?E$z! z+TvQoRZK`*T#Fn}32BRKkqRXtZEu# z_a!3ZzsMg!R=;Z?f0x=nMsAFJjd}Om_V4YD_6qxX`ytHB2ibMDi9CEQBOgU}M_!5C z9mz$`jjTkrf!UGq$lrIheO&P7P;2Yqpa?DwejB_zcyaK|;K#^l@D%37qk|oShXs3B zKUwcsUsx|7g8o+PEX;`4S;vQh!Fz*^=K1Egp*f*@%+cm+=I_k4=3?_H^Ipt{4>Nm& zZVEkVcEH>`A37y;Ug(6-1kA>}h3ZWa-eG=hHks4HQ<2f&jBrDE5oY!M!dLMv{8YY{ zACJt0#&Pa*r)9_M&X zjb{q|oW(_YVPT!Ks7NVc^ui*gl+X(_Ev!RI==qw`l-il6X`!F-v@=%+-+#_{fpspj zLVs4-jP+&RHACVt)WcIZW2X}@QoN2iwVL7`>^7%HQ@o$u?er?rZN?6#XOV66 zXvfuI^p1myl~C{Kq^Z|CIubX~>FI1LQhGWYHT9m(22H)Ev%W~_>8vYKdOD3oN>8VuNa=B`)zo`fCu(YJ zMf(vv-aL>_I5Wk~s< zzQD#>%T>4WW4(v$v^Fs9V#2ZD5aV}mjQuHM8T9KkBiC2phJxRP$r07Xvucp+K#LJpe zPZE2I6g^42RHW!h;>99GPZBR^N?k`huPKdl@m!Ij>xgHI6ysbxQ=}N@;^`vAI2V7? zl%BhIN>fq+@uV8sp(#7WZcQdn5l?8cW3qT$e~MHA9Ak$l9Bawy zHEzM`6n0{)R$<|=8il!Fy%c7g^;9^*SP#D)kao1%Z-*WQR;BLvdyHuT40`?MLiL=2 zUJp2@Na-nRTNCt#+*QR=8gf?_DUGqqiOOaBK|3i^dk3U-oVi2JLrwui;AUWAW}t2rYB#dG;FkS4|>D(Tvcj$&rZK_Sl+YKZw;0= z0?UdwlervFr1b2LC{lWMhZiY5yZ+7TPH|X~(zEMVr1b0#DN=fNeT$Ty-NDW2cA?+S zEQ6k%e*dx{t=9r~9g9(6iI8W0pbBPQQ*>qs1nl$eC`$dXp$KNYbJUjkQk>c6$cbn6l{Ouyev*T|SDV`mFvq^z69SHj|zm z*V<;%v*TLZ%qctg^XhS#lc(_KG}$qkYc)0L+40@_PornYwN9J#?6`g*FzMOxeZ`wG z?zomolg1r?M!%gQ4Kpj(4B3`hnPy10%(`gyobeLt2LJy+dvll(`v&{}Z^d4WJs!I! zwmo)LEE(GpI|aM{i()fkV`D>N2gj;oonkFwLHA$S`+v{fi#&M`AWz;^?xmRXugAXs zOn0n1#68%pb~|9!|4Z~MQ@L*D7pwb4bwm!H;b7JV?UW&lVCY}i z&3`YnH}q8KfzWNCYeU)4`N+E17+M-SE;Jsi2Yo`_L+wI#@Q2{%!S{lDu|{!UaC`6y ztXZ59Yz!VBoDn<!@AzeTNheqSnI9j)*S5QkFxq> z<)@p~&f?}T=0C6=@uvB_`KWmp)-J9vFE!6LPcm1UbIgh8F3tXl4AxEY)>0kv%vffS z&ph*#!Ro0jBeBk40o7wH#7{BBQ{!BQ&zRUq>f%V06p!+rO#3|3P;+@~+gU^&&pz5B2X)>A!P*PCUq zpz7g0bu5DwRS#EIvkaD0J=|j-%V15_!__@l28*g5uEIT7RrPRX70X~*)x-NLSqAH> z9xnfwWw5a7VazNtSXuRO*KRC>rBx4?b!8c>t$O(5GM2&Os)tAHW*MxmdU)6fmcjC> zhv7Pr!TPF)k-sB@1y*WN`Sz2+8mnX^pqb8!kzi)9(kjbHO*2?*m5e4E8LYQTM&g;l zlB>r&&B$QYRhE&GX0Y%o87XH5Yp;@duS8gWRb|^n1}m^0^MojaMOaxzs+qw$tYoCH z87#&6Wf0g5R%0b2LCs)6Rx;Al4Ax{l=4o99%d)Zza>X)OnZ>_qy!o&UmS#OXc?!#5 zZPvrk&I}f3<>yK7GFYFLpQo8c21~S(kyK}}N-G&Dbp{Kyl95nnuvY6a*~771E6cnd zjul&3=Jjwa+TveT@bITt2CKFne(G?R!Md%7>-VrUR&G7q_6wFi&BuM}SsF{X-t{lq zvNYCiJ>2pumd4_(hwJ;W^hrMcswGQr^6?&gcB7Aby~ffTeEeE3mR|4U@^@HzosZus zXX!={Q^G2iZtyWiVtTEQF%r`!`WPcIy~f8FiRslo#z;)B@^A%GuCeq9KE_B)uk4iSVNK7yA zF-Br~zK<~y)AM|ck(i$AV~oV~93NvOrf2&YBQbrPk1-O{vwVz^n4an5AwyaESP#do z!N4Df*8F+ICUKCXJ7r6>9r zmM1;I#})W{$NPBEe3l;PEmtFS$c$zC)~l(!+m_m1ePA=a1NlE-NO!P=ghOz>T2Wbm*+#>5i z%0if|2k8o7vL2)=gvol4rVu9UL5f0{tOw}{VX_{iCWOg)kd_c8>p@CFn5+lsh<86m zlT?uki>!xKkqV2fhg6XYi>wE!2;EQCgEWLNSr1YW!el*2KM0fcAoU)rgx57qOI7jkvjBAxlZs!2JtYN~%WOgat@R)rgz004b>&aT69GB~>GC z!UCkEYQ#-gfRt2?xCsl8lBy9mVF6N7HR9&T-7F1^aD5a@N!5s(!*N}z zM%)}WoTa2{#65RWsTy$;)-@$nBW}XFrle}bO<32IRE@X^>zb0P5jSB)Q&KhJCah>m zsz%&|6-`Oih$F8sOG(v;o3NrOsTy(f{R)vH)xaq5X1O9oy5TXhJSkERkCEj`%~6aj zPm0vTFWV_nq#qt5%abAnA?8`06lsW8MwTZ50e4@}x*nWEm_^iZsPzWO-7gDjp-tlOkOq=2@N;DT`M|mM1kH4DVN% z;j)y}mTF9JSxRb4wF%3UB5mf(K#EKiE`#bac7Qlu~*Bg>N_jqw;+o)oDJ zG0*a(NN2n>A$Yxi5q>}XO88IV2N7-Xx^OOhL3nd`U3gh|HoV?P!t1>@+%?=5Cl69#JM?34 zow?lnJ~+#qjm-6X%!$GA=I6mP%;CX{f=k^~-4or#?y>GTcbMDH?d5iHTf33yk2sm| zVe~b`Ja{O&GkQa`39%2)#JPkO(Yetn(b3T(qIJ>oX#1$Zsf4ecPn>P%}dND%zMmjm_eTv`#APS z?Ah2uIA3vH?6g=mb{3vFX7+6H}quamQW(JDRfw9a%g0zSMWvlCi{ea;{?RF@RfL5 z>=93j`^64{Ih8n1{0`?KmWWwmf*3CPiC&_MXpPelKk~ov5BY2S8U7I8$#39I{33oP z-@sSAGc}_M3dKg@sFLb;q%(igo)yPP2zjSdz$Qx3#93#{%6*Pc1;lf z>$Q8IA#=_D>{mecnt!B8MJ4}GllLq52l{(@z9!z*1aFaVX>#QKVxK1WA1U6nKQTCd zThESaPrt^<8F{t{H_`44Sb;6`7tU&oGmIQNmWp7B$m52oT&-}qVJfRBd~TT9pM=v5 zQ#n>^rmSQtCu`04DNN;Ltrqj zDV3o1rtTBB1`ZZ8G#M~ZOxI-h05MIIjpvA|nw+yy9HU9E*TfV}Uh5?$Yf|;Tn4}5v zwW~DMajT+Is5I4aYtek6(p1N-`CElbQysS!ED|bBHA)r;m8Lpw;ax)IXN_B1=L?mV zHg3(jUZ}LRaqIe7VzmAR)3=FHnrxdcR9f1&HQ^31LYLfuA+5<#yM;;$8@I+BB~*^u zxHW8y7@}`O9$1y5mS$Um_93Zxz3G;`rJ(2?vqjUQqvkvY5znMzm=r$e?? z-7|0yyGr5SuQQdh93F=3a&^77hN%?Q@G)d6MKwGNSx((kJ&>t%)o?RpDqS_43o#j> zv5&^;)}5sk?o!Q?8h2v}g**M3U7~QuF6<(WJ290)8s3EL0(HIp0qlH*v3+}4%Ec$uB3*`3C{Y_(?FjXT*Y&8{=Hvz40VjqBJ_tXF>xSC;=D zn|1&9N-uj;Gjg*JU~rNQd9L|EO>Vl1{jBeUZ#?@a6+x5e90c#`kLWndt#Y<#vT8Nn zT6|N=R_7emM`~KOX0!b#SJR>;J5J$P3pPt(H^yfAeWb=^GyFbMQ()6oYZsP48yEWb z^eV7v{yl(G{aSDmg11)q=;^$b!efr+EfpU5Fn1LmK9WZj9yXji3J)E_TWCCl#}pni zj0=s2a;7lOK138g{W%w&1A1>Jf6LxaQ|`aWrb@$XL}UxS<7D1xCeVt;T}EN3krAd!k$yOybJr2 z!kB{iDJdy$>pkzVv{?=u7u2jJ|ZA!f;@^S7E5bJqm{->~6om#l!3_ zzrV!`>`ryZ=WW;{3hx}p_A0z%5_?_Yt#jDh3SZE`{_JBMcwl!ZJna^?QyD9)j0sB< zywI7NtjP&7k>2Ye7RQ8|k{@|YCu5lN+GqjgLC&UH_M*kK4Jo+B| z1fGgM5WOvWEzSp=4^M%{=+fwM(TUL!(Zk>?P!??ywVj`kjqpR~73WEu64>Tkj?9Fc zod!e%nC=|u9O2Z$cetfvVeb2hcmuHj9>mP|YMc}Jz1Sd@Vct7V3>NjGT(rfk_j~?V z{x*LBCk1x$>u~=69DXuC0jK{b@!|XsUWN4!!G30cWAC$<*%Rz;b`xu27qH*46WKzX z`X9{>XT4bGW?SI<;rhAemoud0 zE$cKxlohVlfMh73t92Pdl<=9FQDBl8njta?SL+ut9?4W)hJYme7|kdQ$z;tChlEek z4ADpU1kETQ$Oz345rhxd3~@mCP|Yah#~{rR^Meo63;{lPf5p7uNY`sd(T%PuGK4p} zQZovcbggEHmc*~oj6xUXG^21u8O?)<#V<-~Mlp*n)ePIT{1VM5 zX3*K1Q81rVHKPDNYVA)(@KI}jGK7y>`;%dNHt5e$K%NH8C?d~V%_wqDV>5ORKhYoc z6nclB;E!&^-r*}1UUed0q40_vU#jr(6?}=p%a-%w6<)fEFV}bpU#9SqWqh&5OZh^D z7hTQgDZF42pQ|t?mva=Jw}h+R7KG>FYPSX9ceq-Xqo6yp)cpvwbF9LMr!!4qgw2_v zFrwp3RJghqAFps#H6N!i_R_{GjIFez6)vyfM=9K`oFA!h*KT}_!ew3gXob6!@lgsR zG|otc5dmkI!XnCtD2%W--Z3cH<9d#5XR^!;QIz-*>W-`D^5YcFE#ZsQL-hhJtlAilJg#um<+TbsQC_1kPPg|`mAZeG~Ei9*y1w@Bb^JmqyQyo)le$Xn)h9M@0uk`$Q|F9ind3biQ*wL#)3& z&Ti*E=T>K{lXlK^PIJ~e$0O3;cxRY%sM8Y%fm=CY@q_qMe1JHAPl@})cKH8aEY1*( zhygHDj2FYi!J<+eAUNXteaYX0@BiccE`B4=BevgZd<|cK$bO^vVZ4Ty@iyFs=l@si zBlbFbmOadNu^ZWC>|*%-Z)7VG*Y6lMh7ExCe|L5ObC_X&V}A<&e~RjNkG{IMDSQVLJkF|%`2iw(l2iuAK0{{OH5YKOSy;Y zb`cvV@IMc~1JD0Q!@I)QhqK{x;rG8PJTE*sJR*E3y#70cv89f@il2tw3_TnABYgh1 zB7WZ%#0yvxS`?ZQ8VjHQgG1G!PN5d?2>6%w`hOsJTX1VI89W=g7?%f+3yu#C3HAwg z54J=6z8|d5t@jYW?o)6JD{Gx^oo+Q+OReLqiPi{2@T)<@hc=dN{)7yUADOS4 z&zcXLyUZKS%MiowEOWiN)SPLKH3yl!MNWDXMMMDm5+WzPiNNdt6FKQkLd>$!ZU7SIq5%C&X1kJ za?*bY*N?KUB_l0Zad( zaz63_mXrQN!1uG9^dBnc_m5;b=|5D?9{`sAL*;zn<18orhsycm16fY`50&%YFR`5T zA1dcB^=3KgKUB`E-efuHKUB`&tYSIoKUB`Ue86(jf2f>)&_(3Pf5>c?uwmboGWtWifu8@5xH(YD{L~huv}NL0N#@Zk6^hn z9}gPLa$VH*AuQL~$Bz$Uxr2P%XAjGD@-ck1avgoV2kqFw$M9{-9q42Dw&f1+@s7zX z*WSlt@NC-oc;pzCYwKfpHRjr=>!Vn%wU0;OdMh6f$7fsmc*t;;YvJRe_{}kOeJIYF z0%LUWU)~RN6}hO-0-b*OOynF@_F15l$O%YT`hyiZiybgEzEf{Ni#zsOlW^LztyrYa+EzFa^t@2MM#(NoX@)A2ZFZ4j^mMWd6{Dw**Q)T39nVqZ3$k#GE2MlLfA~?&Eo$X_s zWyv1rW1MBl&hjzNvSera7-w0s$NCs&S+X;H44=*HbRXj^OLm%%;lG)k>SLT`$sXfl zc=BdRd+0a7lQ&DsLl|dSvZOnNah4@ZsssPc2z)=Yq&bA)`KKNWH0uz$Y|2$ou^j z5qK+SrQTFT;1imadQ%aBPiR)^O+^G-l3A%Y6%qJ^W~JU#MBo#em3mVVflp{w>PIsW%l7c&ulo-c&?z@+?bw15aEpq>adu;&{v(yQDcD^Tw`J z9o*@SU8y=?-q@9@1LlogsXEnRId-M$pv)V)Qgy(*u`5*v%p1E>b-Xf+U8y>#g^sUh zrRt!}8_5;wmwBUAst%RWXqBo1=8aaVI$+*tCDoz7NRC$09q;e+MypgFieJOBol%03 z8>Z1Jm4~9&h%70O_j&S+&XV?cpC`}gEUAyjywOVf<1ufvk^*_m8?B^4#N=ot74piw z(Mmexm3gC8Dp6IKMyph!sxXaKsYF#_8m&@^s=_o{r4m(zX|zfustVI+B_+bh3e#vM z9U@Gll~jl@jaJei!Zcb*fe6!RCH)~xqgAR;RhUMrRG+Fajn=StPi2@!YskkmT1k5_ zTEjG2NqGp4CrY-#z@SN+R*hHoarR3A&gO( zA*CUVQJEo~A&gO(A(bJF@6C|L5XSdrNMQ)WIUqy&LKvRi8B!PTI_f8FA&gO+A!Q+q zQJf)N@$Sc&QBoDc7{wXV6v7zA8B!F&7{wXV6T%q98B!C%7{wXV62ch88B!9$7{wXV z5yBX`8L1)_VT|02RFR4>&YEPTiqwV%Kgu%4d-s!%Cd(}L@${Qnh7<&+ui&A{GNd1b zC+uPwQV+rtC$J1@hj)D<%aC#qj_+j|(hb6Uah7+Ek9XmIsT#E*obJj<)u;`mRF)mBm`e!tVDt z{r@i30G`0k{xfjaK|C~-w5Y0rv?GS+BmlONqi+f z60eJA5dmE|cq6NV6Njf|^}t+-twzjNQz5Gj^7FcK|1qc8&MKdmq# z>MO_j>S)YXj`P)sfv%k8s}Tf!x9?zHE$k=!UCru(tz6%$KRM7=uKU&dDs1KUU%j`+ z-lx6^VeLOq__m4mC#rK4PQkPLyJl<9iT|nDEDQzZy;NNojUgM=Wg{{0TWQwc*v;B& z))$f04npU^Ka-Y75xz{3KK0niRHVKS+?6%igISIuyoL@9d+QbD)L2JM;(%+g-}PGWTe$nM;&;qitMR()Vav2$WwqSe<_xE9Igp|wXF$$bxTd~t6fd-s~t`7t7DqrS4T9#uV$Ly zR|`$>t6M9A&`RPAP5RV}(=@5?BTg=oJ>qmtZ~}CrCgtyl^_sN(Li|pXmS2feH2JEf zI7yQ)+KN*(d8b^g(*);9H)yh_UTo6jsl&x)MG&xnKc>l7E%~EhMNY{196m{HTiew(nP;jn~=gP)AjKkNqI+ z|J!D_;{N}Ai2ip)?BduNvBuc(i2Zj|Yyk59mBrd%?*D_g|4)(s?sK;z@?X;3;(PT^ zL%jd~ZjIX+KK*9&8|?nS9(@`f{kKN1!ruQ`(e?1>pZRYy|F@jyore+k?^^8oZ*fj? zR$|yp#Ge1bP9<{t@&6F>PtEz~Ag|vj(O<+x8M68Xb;Q5_yzAeScj7KL*f;D`_7;1Q zJ&tUC+Y#?C$+oal*c!Ho&0u5M5ajZ!W}R3I7DUXy&+Yf@z4lY~1IQR~t)0f6|4H^r zdk$j$4a1IqC1M$HMEv_Q@?K<5)?GCAca$FE}|k0?`_(k(rEvB9EAuF*xDUmgvB$u&BZofJ4pG|4qOk8Ki7a*Ym*b~u~l z8XZ{cKpW8{*XTUfS~STuI@C$KpG|U&4rQ$ZD3fb+U~PW+Ni@kdI$$v_|K(3l1ZR zCR(NQ7!L^1MC)`OBQJ?2xl)JE+4K!^r4Ed|B%0((9T<5@G|81Z{I&3s5KXij@NHq0T?9ax>qN$H!9~BM~P4((?2N{1B zO}$mw>&Ba+sZOz1jn_m|T(LI`wW6t3u{Y?>8pU2W-VjZ_6no8hT{QJn>=olx(bS{J zP-nGbdyQ8_Ql2RH@3IEc6#m6{@Vi@uX<#uFCE)?h{SritRLZiKcFf-EG_> znz|}>Utx-9DpL$E|DuWZiM*$Bk8!VP>a5sZSbJ`wy&|vdE~-WD7UB8bY1}26I;yf= zSnF=;pcw6+H62)F_$>!0c01O(o7yXOn{m5nYNr^E6pN;|J}XSYT6a?$Rd#h@o@i>V z8P=tnTB$qd5o@W+GKJ-$sfA`nMl{9Loy(03Z*qwR@Hd=whG>ea8_zV(5>1X`XBcOS zCZX7719x)8PB%95CI%KjM?S+ioi*7+P~)a8tSRE-Et^ME*3*e*RYEX(x%we*Tv3q`f5X zPHqa8KN*aE<*jmEDT4m{GuYWMSh{re*V!6 zv-|=u_-qOIhvnz{82(}Tc|L}JSYGZrl?(WX<>&a<;UAWlyH4c-{$Y8!>r^ffK`l?J z=GBSoq-liVAC@OYBMkqrJn0!>_=n|5&AjW!vOH-SVfcsTNy!MqKP*o==HZ>!u5(d2sBn(pjystsRpf`iOuR+3~cY(aGK>|scQRZt9-T8*d`x+z+dKbuh8U*8F(7Qn1 z7a?N!2494PLGJ>2UxbABE|B*{NEq}kkS7s>RTc$$7s!(e5i{srAWu5vG1BckDUruW z{qv+n9wYV7lN$M-yI15%k32?i4S7-|kI}n8o;1m0r2ctdm4rd+pYv5oz?Wv1$oZ-y zJYSleuS&#F=Btu0=v^R3s^ry4?*ci}C6Cd&K#r7&nD;J_BW?1^=v^R3>LeM|Kj-U{ zD9j_~>yt3(T_ES{6BynFa=t!^0^S8!j`RtdD4^UqQYXSt?i^_oVJLKtl!-7DI!C%h z7z&*uRU!<9&iR@o0?|NJACapz|w8&>|L~fp9G#km09#NS$Q^}Dc zd3VxmBuAR$F`B95NR>QBvymL>lE+$M_LC!J@>mS>j~r=}e0BOlh1G` zDU`FOb zBZ#rTD|~!7AHE=bI_BH)a3I_QISxNW?7SyJCn1vliqKu5bFeNk;Wu#xzC@gX*PUmv zHnGdO(YegI*g4DDh}DUCSfU%_3~+io-LXF5IEMH}e2T1uFN(*-Jvftal}L&$;uNt) zED|%sSTO{r5~@Wf(E=+J|KgwHT*6-d6n}u<#;@gBoJ=^KH}J)L20sc1VB$EL(257y zcUYr%6Xz2C$Zlg>u|jb+P9-d7$FcEPpXkH7>uLFZu)hWNx4{2SE#N0`i8{uc#y(eN zG9y~3bX6uZ(0wR`*x zw^+N!HY09+)p*6dw@C4h1fMQl`ik*}d$*=|FM0*1zI5q}g?jf+O$+tLi|#IeK|AV9 zo#(2!JyECK*X|vfWZw+^OykP3FvXZ`EY>4EGjIX3TN7Ycg}TyG@g0 zXSz3QGUHhHCQYW#aBtLP<}CLHO{PtEuh(SiH1|49j+yFStI3pO+-o!$KgHdu$)qXn z)tXG1>RzSE3jxyBBNnZn=ArChb0RFD#OF z?gg5(`Pw~Slh$qA^ECOaoqMh(9lmzY(d57mt_n~Wb=tObRe-vv)BZsBY<*j+uif8k z(zd;OmL@G*xo2w9x~;1=grZK1uU)n25_MX(cGVtG)QPol)lLscOIPh1MIE>=s?D9K z^R?@$BkeJGo?mC}_4oW@#?8hy>*Z$bI_p`@u!V2!(F|Mo)=QcpJfZbsGq%lop&7f` zQn{RB*tEaVdcIlN4c2o-R#yMgUVx+7GG`q;S#JXQI%=xYR@Hi-PLJY~XtOs?`dB%m-JyZk> z6kBhcXWi}ZOU2f#x9(DS?ix#l5{pfpYwc3kkDhAnRCx5!)*T8DA8p;P@POgg4uuaM zVBMl{?=owR6ZmswXR+^x*IOyRC&R$k$)ov@H7>U<*SMS2q;Xd(r*P*oE35E9ovn<*9S^e73U}yer4(-8 z!AdIJw!M{5n76eqRoLd%B??Du>tf%3AQrJM^8E*51?xi95esY45!F7Z+oE7y;D6SQ zerKJhu!vgcD$HA0=W8sia}ks~Sxe@DZ|GV6R^?P;4cibZDMx!BWyj-wU#EPi# zg7LD|=>McBvg*BHsW=8v;|b$wOC55LVyWQ?OU28GVpHHAOGVv?VzPgyrJ`pLyTek^ zGommccUa1NMKLkI&Z4bu8hz1|jEr@r{~MzlPO>&De8L9nRD~CwV4bY+poP|18b4}n zRJiLP>m-FcceOSt9PMm1D9oeQi3;OMtyUOMYL&v_h;@R(p|G`5;b6#Gp)hjNFZWwJ zYFW$t){YjerRt7H2U#0@>})JpOB7yxlXbkpOIBNp6<)Z+TBPvoh1NoaXUw)#3_RJ;f0u*0nh>iVIFS>qKxWSBKs<3p`+ znEwZ&=F!IgEN5Kk$Ius{cSA3Q9t+(Wx)HhS&kvo3)94FA$Am^9`g~8EMR!9+@E^gC zg0E8kIo6g%?C~wX7x0u{}wZOn$tfLz5`hRvf=Z>r-e^MuDt2kKOY$G9WD>I3$xhQ z&Eo4{gZzcRkFAd_fd|8pvHr1M$XysUH(MRdMx1Yd4g20R%?HgT)+N>z)}hu})&^@f zvfxd$hFej~z>fIE=HJaL&7-X<>l5of>vii-);2^n{HOa@#6Nh&{S#ifZ+EYAbM6K1 zW_O*t45uF^yQAGB+&Z`1ZSRU`A^LUn6P$l|A^KSKZmfM=8BIk05Is4%I=V1AJ$iI> zaI|l7#!7?9zvX{HbiB7w^Jb^9asB{EdCcUSrR&huBV>f@oqFu`}5Qwt~&YIf&8h2v*0+ z|Cgs&{%pYZcAZ`2dSg3-2D{30#&!meaFt7p?L0on-KalBEAco_E{n(XO~5R|%#pN)dMT9-}P;jYqT@)Y+3 zO?FInR~A2oRe5)XE*m<;U9QQP$K7R`j2h`K)nw!-cS(_qagWzz#7K9sCc__h7iltN zsJpO8hPw;&Z=e-uq*$SM=N2g%2mw0OUrlTD?rdLlY)Hx7rV0mS(+abx zLjKsst;U0<3i(6q7Bi(kcZ+eWnbhn$lqEFVYFuYts#(g|YN~KRwvjTD=Eb@!X{5}H zG`pnGU@C>Sv8!~6sX|%Vw5w#QfLJzmm6E0kY-Q8hzPU@+ve$UqyrUU=*}Pq|myNxq zdS|wc7mb(A9lGqfLVr`eGuy_WjOWZ-blFqJljaRYRv2jBtl5*sQ>F^bWE)Q!yUiPQ z*>05G)Qml8{y*)VYj9Q7701us`zAT(u5-fE00F`)JmhTz6qJwv2_Ybl@P0paI)H$n z2o~j`Q?+({AR>DAW@^VeQ|l;Gky=s5!PHXQB1X_c5h)@?Ku`qQ+V-Yv?S0k>AMD3| zi(mZzvi7<6a_&w3cklf_>-;>SS{%tuntbnF?|}SeQhV2{mE5w)_uj_$f##L;4FYMS zd@dRImiM;gzE8flr+$*$k<>7mEKNeV1tE1p7(kHRm(2HY3qtZ#lXi=uPW-up5w%gr z1j@^zZV8l)h>9AZJnE3Zu#wTg1}KaAB~Usn8k9guUDP*${v}bL1nT-ny%VS{jRqv} zN=?**KgxFNF1ns;=}E8P-DBfF2P31~uMaZ*aKV4CU$uq80RE9i7 z_R}LP@cu4b8BrVZ6xmOgm*ILaSIS0&JYV+HW#!nDmMgy(-G^elc7r9Tw8t zY5#JiOX{$bEkpkjTx@2j>mTxBfS;}{4S4~olspN={$zN6;NNi#yO4o=FQF^mck;Ja z(Pdei&}Hw6R&#j!oz={)u=!A>1gGzfK4@3gzyEKK72t-&{qXNS=y@{SK+o z{MD&%KUKxLPhT~Pb?-i^l69{VRh{VGs)BWoqiQ(o?mbjF>u%ju8SAdy)G*e?z0`c=nm=z)@@IzL97dl)PO{{RoAn|X;%sBf>!D} z)-4MZH?j(ww^aT3=S`cdzO0)xRef0JH&MM=NBOE3>o8J1SwpC8yV-P0fpcB|ubel9%E)8$>Tr zDLEGom?-QePjJmUESjrs2_5tfE55h^ytllAsyM0b^xjfk5_-wosX8b0Jg)q7O2~N6 ztBwgh;~CWKfKdx2YDam#k1N6TMV5XMNi=)s*$Ex2Yzq7vHM#Sua|wBGwBR zsgU)8g$h{DU!c;g=gn6E>$&rk&w9>WmB)Ja9F@y@)@+r-dgd&ZVm)J~Qmm)XP?Ghu zB}%ZKJY9LLCry_1tZOF8EbECi@;|I6OqBm-J${1x7wd83<#(*djFbOlT|GwrBhl6J zN}_kmzq1}vEx%>mcZj^mx@TYcHS4ZDC zZO+KktXs8_U$8D{C;yh{R`L|<76tNi*3DYTzp`%9Ode+)HIaO&3WSk77XR)-C_jxS z)qAzGV5(iR5R9_HmJf_r}OLOUsyxF{Bxo&%Ok7;Y(V_} z9kss-ad4XiDaI3ZzZ$*hh6coGO>|uY;+!Pvbv0(Ak_N;{Pt>2kAYh0n>X|^zr0CiN zs%oRI2~<`@#R*hYMO_-860gF?`9I(_XG#CR|CRr5-Xw)x{ZHo~$^RqnKkdx_8K(b# zgwwYr`LpxK<(K7`4q)fMUC|5J&Hu;IdhGi*KdQk!_CZk(?D`jm zS?uJ0B0Pva|6UJY#IHP$;x+%P!o_$oOI0`oulO&*YfmKZt)If{{ojW-;bq)e--1{B zuYn(6x4*Ga3jLuo;2$U4RX>wHn%}%-gE~z8@5WAl&jvpU?!(OgLhSQb8Qd83#KeC-cKN&LpTrEpKL3sH?eSNFiT^@B z-}mw^<(?Z{DuF7xK2png84JW@4AW;kaheGp{JGd0sB|`8%I`BKPCm_jBLI zRf`ueh45(Z=G-;8OLFI8ufNLNA(%(#jO!NZoGfM%PGPsd{W*IvkMLs7j+`xc@4tI; zmg5S`)ST*^(wvfssl}Ty%FKOb; zW{7!76K6I<%u5=|Y+}+ew0TKGsZB?imo${yB(ix)L&;5Bqr9Y{?4~8kOBzaVTB5w9 zq5P&L%1atba9X0gq@fI_CCW=0N^x4EyrijQqP(Q3V4}RF8OcO>Ni!lQo0l}hE#Ve0 z?nHwrPpG~dm@+2ZhXyl@=?G@+OleHC3k_x{*KiXW%*{-=2My*XCftGsGlYq&f^Uy2Ki9xo0}Z8t)Ei zI>mY->W(}H$9ITz&A7l6#d^ZTz_e#w6PR|f9zP*4ZDT!dd|(P=jdz(eZDKtZf9qPu zdNiJ&RJvSB(zLHLR-x(=yh0S4z_&)|I0ILupO=x9_bC3}rP*7k?HQN@|kE zJ5d_SX_Ee|OJFFaNxI-nU?`($bwOY#p-H+WzK@j8B#k%FGL+6FeFn!+Hj{KAj-g~G z={ALdp4%t5)-HMJhnP^-l)R)z#e}kUxl8+R5rKBOOFzVfvUa&kKg5KxrraeRfeB^p zQkQ;+cl*}PM^IM<+WCkdVnSIvAMry>C~M~dSo%6!0GeFI(gpD~Xz~?Hm&GN}OecEK{`k^=QKzkiG!e>(;$67HRm+;xQ{?Pr$IU( zJ_PNY25GgqozvjRO7SLW=QK!T#V*j!X>jE3 zY&B?d8hVei)nYej@*4Y&y_OvVn%u_L#)#KIli%3dtJzyXljGRhE#g(s$kNkdJ81GCOWVZL zpvi?SJw?w>K4fXDcnUN*k)l^f1-Pq3m(%#KWM; zqipS7aUW=MshI8sO+IDmfh?X0aw<#riw8iHS6SLD?gve7WoeVx44VAP(nhffG&z=~ z4Pql`@+?dDi4CC1wJfa{_PSE6DmR52`d~4EdU+W#V?oIA_D(j-_H5WXRcUZHZV48S*wu z>*$(aayO#vc7fLpI)B6C=4bJ`Lg#Ntiv?bR===?7t-z~;oxdS15{n^2{$?L@p;!bN zayUy1#6rlB$61;$7C?qv&OYCB^q9`)@KI)G_dtex&W@ZeW^>WYZWr&dX^}wpCR9~v>M;{3^|{r zYJpcFlJ{8}Evg|y?q{h=jE0Q!KYT5C#RFvKb8VEUf{gP&tW}Cpka7Npv<^Q@8Rvg^ z=J$w7$Rz%UkAw4nFKDv$d-DIu)UQ!Ao`J?Q(0B$K&p_iDXgmXrXQ1&6G@gORGthVj z8qYxE8E8BM|A#XW7+R~KJxT0$V`!yl60pD+5ET6QuE;PlnbeNI$v!zfXEo*#H0l literal 0 HcmV?d00001 diff --git a/ZERO_VERSION b/ZERO_VERSION index 81c871d..0a5af26 100644 --- a/ZERO_VERSION +++ b/ZERO_VERSION @@ -1 +1 @@ -1.10.0 +1.11.3 diff --git a/e2e/docker-compose.yml b/e2e/docker-compose.yml index a0cbda7..2001e66 100644 --- a/e2e/docker-compose.yml +++ b/e2e/docker-compose.yml @@ -15,7 +15,7 @@ services: retries: 60 zero-ts: &zero - image: rocicorp/zero:1.10.0 + image: rocicorp/zero:1.11.3 depends_on: postgres: condition: service_healthy diff --git a/e2e/package-lock.json b/e2e/package-lock.json index d3b2a89..c3f9bde 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -6,17 +6,17 @@ "": { "name": "zero-server-e2e", "dependencies": { - "@rocicorp/zero": "1.10.0", + "@rocicorp/zero": "1.11.3", "postgres": "3.4.9" } }, "node_modules/@badrap/valita": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/@badrap/valita/-/valita-0.3.11.tgz", - "integrity": "sha512-oak0W8bycFjnrLeVCVvZqkOWTGh74wCPKUxGLJyhRukRs+V/hQdfZp1eDcQE4Gf3UrtJWfR/Ou4Xe0DZqJZ2FA==", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@badrap/valita/-/valita-0.5.5.tgz", + "integrity": "sha512-+Hp6PfoOyD6hpONZOiJTIP3xiqPPLMtMzDjJ522Hoba1ynqKKnt7bW82L5+G8klLoUHmcuxfCNpND93KyiA7CQ==", "license": "MIT", "engines": { - "node": ">= 16" + "node": ">= 22" } }, "node_modules/@databases/escape-identifier": { @@ -403,9 +403,9 @@ } }, "node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", + "version": "0.223.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.223.0.tgz", + "integrity": "sha512-Oepm6etVPFCn0VWB1t833iOTgz1eYBDUPxcrOR/XvwzD1+Ag9VzSPSrrKAFNJ/haatj+8CXbG47pZXvdNQKDJw==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/api": "^1.3.0" @@ -800,18 +800,6 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/instrumentation-bunyan/node_modules/@opentelemetry/api-logs": { - "version": "0.223.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.223.0.tgz", - "integrity": "sha512-Oepm6etVPFCn0VWB1t833iOTgz1eYBDUPxcrOR/XvwzD1+Ag9VzSPSrrKAFNJ/haatj+8CXbG47pZXvdNQKDJw==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/@opentelemetry/instrumentation-cassandra-driver": { "version": "0.68.0", "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-cassandra-driver/-/instrumentation-cassandra-driver-0.68.0.tgz", @@ -1235,18 +1223,6 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/instrumentation-openai/node_modules/@opentelemetry/api-logs": { - "version": "0.223.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.223.0.tgz", - "integrity": "sha512-Oepm6etVPFCn0VWB1t833iOTgz1eYBDUPxcrOR/XvwzD1+Ag9VzSPSrrKAFNJ/haatj+8CXbG47pZXvdNQKDJw==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/@opentelemetry/instrumentation-oracledb": { "version": "0.48.0", "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-oracledb/-/instrumentation-oracledb-0.48.0.tgz", @@ -1302,18 +1278,6 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/instrumentation-pino/node_modules/@opentelemetry/api-logs": { - "version": "0.223.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.223.0.tgz", - "integrity": "sha512-Oepm6etVPFCn0VWB1t833iOTgz1eYBDUPxcrOR/XvwzD1+Ag9VzSPSrrKAFNJ/haatj+8CXbG47pZXvdNQKDJw==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/@opentelemetry/instrumentation-redis": { "version": "0.71.0", "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis/-/instrumentation-redis-0.71.0.tgz", @@ -1381,18 +1345,6 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/instrumentation-runtime-node/node_modules/@opentelemetry/api-logs": { - "version": "0.223.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.223.0.tgz", - "integrity": "sha512-Oepm6etVPFCn0VWB1t833iOTgz1eYBDUPxcrOR/XvwzD1+Ag9VzSPSrrKAFNJ/haatj+8CXbG47pZXvdNQKDJw==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/@opentelemetry/instrumentation-socket.io": { "version": "0.70.0", "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-socket.io/-/instrumentation-socket.io-0.70.0.tgz", @@ -1457,30 +1409,6 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/instrumentation-winston/node_modules/@opentelemetry/api-logs": { - "version": "0.223.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.223.0.tgz", - "integrity": "sha512-Oepm6etVPFCn0VWB1t833iOTgz1eYBDUPxcrOR/XvwzD1+Ag9VzSPSrrKAFNJ/haatj+8CXbG47pZXvdNQKDJw==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/instrumentation/node_modules/@opentelemetry/api-logs": { - "version": "0.223.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.223.0.tgz", - "integrity": "sha512-Oepm6etVPFCn0VWB1t833iOTgz1eYBDUPxcrOR/XvwzD1+Ag9VzSPSrrKAFNJ/haatj+8CXbG47pZXvdNQKDJw==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/@opentelemetry/otlp-exporter-base": { "version": "0.223.0", "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.223.0.tgz", @@ -1535,18 +1463,6 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/api-logs": { - "version": "0.223.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.223.0.tgz", - "integrity": "sha512-Oepm6etVPFCn0VWB1t833iOTgz1eYBDUPxcrOR/XvwzD1+Ag9VzSPSrrKAFNJ/haatj+8CXbG47pZXvdNQKDJw==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/@opentelemetry/propagator-aws-xray": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-aws-xray/-/propagator-aws-xray-2.2.0.tgz", @@ -1715,18 +1631,6 @@ "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, - "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/api-logs": { - "version": "0.223.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.223.0.tgz", - "integrity": "sha512-Oepm6etVPFCn0VWB1t833iOTgz1eYBDUPxcrOR/XvwzD1+Ag9VzSPSrrKAFNJ/haatj+8CXbG47pZXvdNQKDJw==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/@opentelemetry/sdk-metrics": { "version": "2.12.0", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.12.0.tgz", @@ -1784,18 +1688,6 @@ "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, - "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/api-logs": { - "version": "0.223.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.223.0.tgz", - "integrity": "sha512-Oepm6etVPFCn0VWB1t833iOTgz1eYBDUPxcrOR/XvwzD1+Ag9VzSPSrrKAFNJ/haatj+8CXbG47pZXvdNQKDJw==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/@opentelemetry/sdk-trace": { "version": "2.12.0", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.12.0.tgz", @@ -1873,9 +1765,9 @@ } }, "node_modules/@oxfmt/binding-android-arm-eabi": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.45.0.tgz", - "integrity": "sha512-A/UMxFob1fefCuMeGxQBulGfFE38g2Gm23ynr3u6b+b7fY7/ajGbNsa3ikMIkGMLJW/TRoQaMoP1kME7S+815w==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.65.0.tgz", + "integrity": "sha512-M10Gs1SSpTNI6ahGx3M/OlIdUF4hkaP6OgUb+MS79t/Pgflk3r1nW5gPFqsZGUAXg0H1AfANT9AvLdBSTIhZKg==", "cpu": [ "arm" ], @@ -1889,9 +1781,9 @@ } }, "node_modules/@oxfmt/binding-android-arm64": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.45.0.tgz", - "integrity": "sha512-L63z4uZmHjgvvqvMJD7mwff8aSBkM0+X4uFr6l6U5t6+Qc9DCLVZWIunJ7Gm4fn4zHPdSq6FFQnhu9yqqobxIg==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.65.0.tgz", + "integrity": "sha512-6DXH5sftNlaHpWJG50hFMF+Qxtq5D2TmahvcDPxWNcGIf8qrC9Y0YgHYcYZ2hlWzaccKXh/f3GcssH8vtkl4JA==", "cpu": [ "arm64" ], @@ -1905,9 +1797,9 @@ } }, "node_modules/@oxfmt/binding-darwin-arm64": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.45.0.tgz", - "integrity": "sha512-UV34dd623FzqT+outIGndsCA/RBB+qgB3XVQhgmmJ9PJwa37NzPC9qzgKeOhPKxVk2HW+JKldQrVL54zs4Noww==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.65.0.tgz", + "integrity": "sha512-K9m7lr53pcOLETNsC88sWes/GWHUGjZyHx95UhYcSXy0r30haLdeXlSufSenEAtoLaW753WN8/l4M7GYcRt6cg==", "cpu": [ "arm64" ], @@ -1921,9 +1813,9 @@ } }, "node_modules/@oxfmt/binding-darwin-x64": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.45.0.tgz", - "integrity": "sha512-pMNJv0CMa1pDefVPeNbuQxibh8ITpWDFEhMC/IBB9Zlu76EbgzYwrzI4Cb11mqX2+rIYN70UTrh3z06TM59ptQ==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.65.0.tgz", + "integrity": "sha512-sTNwIx1gre3MyiHOPLu7IGW4UyMScYL4DTmJT01p4vzB0En+OJUQz6KuH8t0PpsClRSaMuY3b0QmtoPItfO8Lg==", "cpu": [ "x64" ], @@ -1937,9 +1829,9 @@ } }, "node_modules/@oxfmt/binding-freebsd-x64": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.45.0.tgz", - "integrity": "sha512-xTcRoxbbo61sW2+ZRPeH+vp/o9G8gkdhiVumFU+TpneiPm14c79l6GFlxPXlCE9bNWikigbsrvJw46zCVAQFfg==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.65.0.tgz", + "integrity": "sha512-lYZMVIiIpnjGu5hJb2jxA8NYQ/e0OTGuaiAf4dqlGPNnPmUTu23FZRMltmjro/KkQm1uE4NT4n5yJ2zWmKcpfA==", "cpu": [ "x64" ], @@ -1953,9 +1845,9 @@ } }, "node_modules/@oxfmt/binding-linux-arm-gnueabihf": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.45.0.tgz", - "integrity": "sha512-hWL8Hdni+3U1mPFx1UtWeGp3tNb6EhBAUHRMbKUxVkOp3WwoJbpVO2bfUVbS4PfpledviXXNHSTl1veTa6FhkQ==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.65.0.tgz", + "integrity": "sha512-gIdXFAt/bURnjxuoedDEWdZ0PEWEmdDcm8qdpoFYYvW3QMk/5D4vUaH4mlMeRpeTdST4izUgHVO6RawQ4QulJw==", "cpu": [ "arm" ], @@ -1969,9 +1861,9 @@ } }, "node_modules/@oxfmt/binding-linux-arm-musleabihf": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.45.0.tgz", - "integrity": "sha512-6Blt/0OBT7vvfQpqYuYbpbFLPqSiaYpEJzUUWhinPEuADypDbtV1+LdjM0vYBNGPvnj85ex7lTerEX6JGcPt9w==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.65.0.tgz", + "integrity": "sha512-jJVyADto7gA2AaX5qAjAexrxx9PJQaKWOe8PICE7yKMbjBRyOHcmj9TtVJ+MZYDUQ3hodU0AcoTj0jFQ1W4C6Q==", "cpu": [ "arm" ], @@ -1985,9 +1877,9 @@ } }, "node_modules/@oxfmt/binding-linux-arm64-gnu": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.45.0.tgz", - "integrity": "sha512-jLjoLfe+hGfjhA8hNBSdw85yCA8ePKq7ME4T+g6P9caQXvmt6IhE2X7iVjnVdkmYUWEzZrxlh4p6RkDmAMJY/A==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.65.0.tgz", + "integrity": "sha512-p3RFkB+u7u+8up99b/NEcI1hdpLDiGgJYNwDorB60n7eH+eKposAKuMBxx+NqB3b+sJP4CZmYDh9G7X62tUsKg==", "cpu": [ "arm64" ], @@ -2004,9 +1896,9 @@ } }, "node_modules/@oxfmt/binding-linux-arm64-musl": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.45.0.tgz", - "integrity": "sha512-XQKXZIKYJC3GQJ8FnD3iMntpw69Wd9kDDK/Xt79p6xnFYlGGxSNv2vIBvRTDg5CKByWFWWZLCRDOXoP/m6YN4g==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.65.0.tgz", + "integrity": "sha512-5Prb0uFzJHr+OUD/qS/TmU526wD+PaHDsm3KoRiUXbMIDpTSErjeQYkK3OQeshAvD/PuLa9WGEi9WPajjdOZJg==", "cpu": [ "arm64" ], @@ -2023,9 +1915,9 @@ } }, "node_modules/@oxfmt/binding-linux-ppc64-gnu": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.45.0.tgz", - "integrity": "sha512-+g5RiG+xOkdrCWkKodv407nTvMq4vYM18Uox2MhZBm/YoqFxxJpWKsloskFFG5NU13HGPw1wzYjjOVcyd9moCA==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.65.0.tgz", + "integrity": "sha512-S8svxTp81obnF3admN9yd+u2rOYXtyzThLGBTg1PY6TPtGcC09BaaXLQD+TBSMa7yvqhCDZ8DFri+S/yG60qCg==", "cpu": [ "ppc64" ], @@ -2042,9 +1934,9 @@ } }, "node_modules/@oxfmt/binding-linux-riscv64-gnu": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.45.0.tgz", - "integrity": "sha512-V7dXKoSyEbWAkkSF4JJNtF+NJZDmJoSarSoP30WCsB3X636Rehd3CvxBj49FIJxEBFWhvcUjGSHVeU8Erck1bQ==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.65.0.tgz", + "integrity": "sha512-WtXBr75G/h2qOHy8SiGtC1R6aS3jt4mE52v1D8AtwMXIgoOmSNP9lKvbSaTRoL0e5wsMPoi6T72QWDYPu+S+nA==", "cpu": [ "riscv64" ], @@ -2061,9 +1953,9 @@ } }, "node_modules/@oxfmt/binding-linux-riscv64-musl": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.45.0.tgz", - "integrity": "sha512-Vdelft1sAEYojVGgcODEFXSWYQYlIvoyIGWebKCuUibd1tvS1TjTx413xG2ZLuHpYj45CkN/ztMLMX6jrgqpgg==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.65.0.tgz", + "integrity": "sha512-YwSLVvpaz4o/nv/miiPEBJz+eJ+VmbgNIrao6RccK9ce+L5EA8wP+ZD0uFeq6wKOza6zoWv/dR0sj6lip6R3EA==", "cpu": [ "riscv64" ], @@ -2080,9 +1972,9 @@ } }, "node_modules/@oxfmt/binding-linux-s390x-gnu": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.45.0.tgz", - "integrity": "sha512-RR7xKgNpqwENnK0aYCGYg0JycY2n93J0reNjHyes+I9Gq52dH95x+CBlnlAQHCPfz6FGnKA9HirgUl14WO6o7w==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.65.0.tgz", + "integrity": "sha512-XQTPqgvyrgkKcFq+Tp2eK6JS7sqqJ+nRmy2Fav4j3I+i4dJoPJm7YwEdoeSDX9xkqj9jZ/lWfF3bXUWztIrn6A==", "cpu": [ "s390x" ], @@ -2099,9 +1991,9 @@ } }, "node_modules/@oxfmt/binding-linux-x64-gnu": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.45.0.tgz", - "integrity": "sha512-U/QQ0+BQNSHxjuXR/utvXnQ50Vu5kUuqEomZvQ1/3mhgbBiMc2WU9q5kZ5WwLp3gnFIx9ibkveoRSe2EZubkqg==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.65.0.tgz", + "integrity": "sha512-cjZlx6S/VkeCNWCbwZriTnLnZeTcV3DEyeRGSw/2wwLP9viq+C0bJ4bC1k/ZLkFxDcB1lUgSasPkYGP1bdraOg==", "cpu": [ "x64" ], @@ -2118,9 +2010,9 @@ } }, "node_modules/@oxfmt/binding-linux-x64-musl": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.45.0.tgz", - "integrity": "sha512-o5TLOUCF0RWQjsIS06yVC+kFgp092/yLe6qBGSUvtnmTVw9gxjpdQSXc3VN5Cnive4K11HNstEZF8ROKHfDFSw==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.65.0.tgz", + "integrity": "sha512-2azCjxdLtK4zCcIOU1dlXlU0xxfbPi6EjwWx7Ac7teWPidIIDOcIhudup83xNCKYhtqeVd/gaVDOxbUq4syXWA==", "cpu": [ "x64" ], @@ -2137,9 +2029,9 @@ } }, "node_modules/@oxfmt/binding-openharmony-arm64": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.45.0.tgz", - "integrity": "sha512-RnGcV3HgPuOjsGx/k9oyRNKmOp+NBLGzZTdPDYbc19r7NGeYPplnUU/BfU35bX2Y/O4ejvHxcfkvW2WoYL/gsg==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.65.0.tgz", + "integrity": "sha512-KXQ7xi1e/voP0IQaw6fG6XY4Z5+Llf1XmRSZS1t7pVFCecFJ0iXaboKmVwjFtp5MLlT5iWQrJ2U1C3GJdZ2u+Q==", "cpu": [ "arm64" ], @@ -2153,9 +2045,9 @@ } }, "node_modules/@oxfmt/binding-win32-arm64-msvc": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.45.0.tgz", - "integrity": "sha512-v3Vj7iKKsUFwt9w5hsqIIoErKVoENC6LoqfDlteOQ5QMDCXihlqLoxpmviUhXnNncg4zV6U9BPwlBbwa+qm4wg==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.65.0.tgz", + "integrity": "sha512-2FbbjG5jEqLSLKVJwBap84uJfpn5Y5A53KEO0aUNr+zeiRB9nyPUIFMcSbZVMFLitfBytFWRNngozXYjb6Rsbw==", "cpu": [ "arm64" ], @@ -2169,9 +2061,9 @@ } }, "node_modules/@oxfmt/binding-win32-ia32-msvc": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.45.0.tgz", - "integrity": "sha512-N8yotPBX6ph0H3toF4AEpdCeVPrdcSetj+8eGiZGsrLsng3bs/Q5HPu4bbSxip5GBPx5hGbGHrZwH4+rcrjhHA==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.65.0.tgz", + "integrity": "sha512-LJ+ZacAPSjegDOnSLyA1TMWAhdDrsK4el3REdr1oL2UtVBCMhO2II/Sb3cEW6mF2MfLhl8hDNCSvc7KSbgk3LQ==", "cpu": [ "ia32" ], @@ -2185,9 +2077,9 @@ } }, "node_modules/@oxfmt/binding-win32-x64-msvc": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.45.0.tgz", - "integrity": "sha512-w5MMTRCK1dpQeRA+HHqXQXyN33DlG/N2LOYxJmaT4fJjcmZrbNnqw7SmIk7I2/a2493PPLZ+2E/Ar6t2iKVMug==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.65.0.tgz", + "integrity": "sha512-higu9cWEO6XXFzATD1jf0mCK34rNfN2H9JrJie7QB1IhleVpTh0QlLH9Ip2C1H/Nd5n0v5pvRtC+5R0uE4HpVg==", "cpu": [ "x64" ], @@ -2312,12 +2204,12 @@ } }, "node_modules/@rocicorp/zero": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@rocicorp/zero/-/zero-1.10.0.tgz", - "integrity": "sha512-u+0mrWEP0IBAQY4lbtnhNjpyjee5zSTNxpeY9j14apME1PYshmhdTvbNwmyaoumvBt9MrQGCEr5y7W6utj+Vjg==", + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@rocicorp/zero/-/zero-1.11.3.tgz", + "integrity": "sha512-xP3yPX5q2zauLaZyPICoV7j/16PsyTixTyv85Kj/aFUT48JIyOHrAjsthHlpZhe46XbSlXHzDGSuqIjuwVJmHQ==", "license": "Apache-2.0", "dependencies": { - "@badrap/valita": "0.3.11", + "@badrap/valita": "0.5.5", "@databases/escape-identifier": "^1.0.3", "@databases/sql": "^3.3.0", "@dotenvx/dotenvx": "^1.39.0", @@ -2325,20 +2217,20 @@ "@fastify/cors": "^10.0.0", "@fastify/websocket": "^11.0.0", "@google-cloud/precise-date": "^4.0.0", - "@opentelemetry/api": "^1.9.0", - "@opentelemetry/api-logs": "^0.218.0", - "@opentelemetry/auto-instrumentations-node": "^0.76.0", - "@opentelemetry/exporter-metrics-otlp-http": "^0.218.0", - "@opentelemetry/resources": "^2.7.1", - "@opentelemetry/sdk-metrics": "^2.7.1", - "@opentelemetry/sdk-node": "^0.218.0", - "@opentelemetry/sdk-trace-node": "^2.7.1", - "@opentelemetry/semantic-conventions": "^1.41.1", + "@opentelemetry/api": "1.9.1", + "@opentelemetry/api-logs": "0.223.0", + "@opentelemetry/auto-instrumentations-node": "0.81.2", + "@opentelemetry/exporter-metrics-otlp-http": "0.223.0", + "@opentelemetry/resources": "2.12.0", + "@opentelemetry/sdk-metrics": "2.12.0", + "@opentelemetry/sdk-node": "0.223.0", + "@opentelemetry/sdk-trace-node": "2.12.0", + "@opentelemetry/semantic-conventions": "1.43.0", "@postgresql-typed/oids": "^0.2.0", "@rocicorp/lock": "^1.0.4", "@rocicorp/logger": "^6.1.0", "@rocicorp/resolver": "^1.0.2", - "@rocicorp/zero-sqlite3": "^1.1.4", + "@rocicorp/zero-sqlite3": "^1.1.5", "@standard-schema/spec": "^1.0.0", "@types/basic-auth": "^1.1.8", "@types/ws": "^8.5.12", @@ -2348,24 +2240,23 @@ "cloudevents": "^10.0.0", "command-line-args": "^6.0.1", "command-line-usage": "^7.0.3", - "compare-utf8": "^0.2.0", + "compare-utf8": "^0.2.3", "defu": "^6.1.4", "eventemitter3": "^5.0.1", - "fastify": "^5.0.0", + "fastify": "^5.12.7", "is-in-subnet": "^4.0.1", "jose": "^5.9.3", - "js-xxhash": "^4.0.0", "json-custom-numbers": "^3.1.1", "kasi": "^1.1.0", - "nanoid": "^5.1.2", - "oxfmt": "^0.45.0", + "nanoid": "^5.1.16", + "oxfmt": "^0.65.0", "parse-prometheus-text-format": "^1.1.1", "pg-format": "npm:pg-format-fix@^1.0.5", "postgres": "3.4.7", "semver": "^7.5.4", "url-pattern": "^1.0.3", "urlpattern-polyfill": "^10.1.0", - "ws": "^8.18.1" + "ws": "^8.22.0" }, "bin": { "analyze-query": "out/zero/src/analyze-query.js", @@ -4031,15 +3922,6 @@ "url": "https://github.com/sponsors/panva" } }, - "node_modules/js-xxhash": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-xxhash/-/js-xxhash-4.0.0.tgz", - "integrity": "sha512-3Q2eIqG6s1KEBBmkj9tGM9lef8LJbuRyTVBdI3GpTnrvtytunjLPO0wqABp5qhtMzfA32jYn1FlnIV7GH1RAHQ==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/json-bigint": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", @@ -4368,9 +4250,9 @@ } }, "node_modules/oxfmt": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.45.0.tgz", - "integrity": "sha512-0o/COoN9fY50bjVeM7PQsNgbhndKurBIeTIcspW033OumksjJJmIVDKjAk5HMwU/GHTxSOdGDdhJ6BRzGPmsHg==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.65.0.tgz", + "integrity": "sha512-SgS5VgnP42T0zl3zWD+xoH8FCqg1SAFnSRoOT/qeoa6gxcYIqrDMOmcXIg/EWSN92Du4ogB4riuKhKd6Y4CGhw==", "license": "MIT", "dependencies": { "tinypool": "2.1.0" @@ -4385,25 +4267,37 @@ "url": "https://github.com/sponsors/Boshen" }, "optionalDependencies": { - "@oxfmt/binding-android-arm-eabi": "0.45.0", - "@oxfmt/binding-android-arm64": "0.45.0", - "@oxfmt/binding-darwin-arm64": "0.45.0", - "@oxfmt/binding-darwin-x64": "0.45.0", - "@oxfmt/binding-freebsd-x64": "0.45.0", - "@oxfmt/binding-linux-arm-gnueabihf": "0.45.0", - "@oxfmt/binding-linux-arm-musleabihf": "0.45.0", - "@oxfmt/binding-linux-arm64-gnu": "0.45.0", - "@oxfmt/binding-linux-arm64-musl": "0.45.0", - "@oxfmt/binding-linux-ppc64-gnu": "0.45.0", - "@oxfmt/binding-linux-riscv64-gnu": "0.45.0", - "@oxfmt/binding-linux-riscv64-musl": "0.45.0", - "@oxfmt/binding-linux-s390x-gnu": "0.45.0", - "@oxfmt/binding-linux-x64-gnu": "0.45.0", - "@oxfmt/binding-linux-x64-musl": "0.45.0", - "@oxfmt/binding-openharmony-arm64": "0.45.0", - "@oxfmt/binding-win32-arm64-msvc": "0.45.0", - "@oxfmt/binding-win32-ia32-msvc": "0.45.0", - "@oxfmt/binding-win32-x64-msvc": "0.45.0" + "@oxfmt/binding-android-arm-eabi": "0.65.0", + "@oxfmt/binding-android-arm64": "0.65.0", + "@oxfmt/binding-darwin-arm64": "0.65.0", + "@oxfmt/binding-darwin-x64": "0.65.0", + "@oxfmt/binding-freebsd-x64": "0.65.0", + "@oxfmt/binding-linux-arm-gnueabihf": "0.65.0", + "@oxfmt/binding-linux-arm-musleabihf": "0.65.0", + "@oxfmt/binding-linux-arm64-gnu": "0.65.0", + "@oxfmt/binding-linux-arm64-musl": "0.65.0", + "@oxfmt/binding-linux-ppc64-gnu": "0.65.0", + "@oxfmt/binding-linux-riscv64-gnu": "0.65.0", + "@oxfmt/binding-linux-riscv64-musl": "0.65.0", + "@oxfmt/binding-linux-s390x-gnu": "0.65.0", + "@oxfmt/binding-linux-x64-gnu": "0.65.0", + "@oxfmt/binding-linux-x64-musl": "0.65.0", + "@oxfmt/binding-openharmony-arm64": "0.65.0", + "@oxfmt/binding-win32-arm64-msvc": "0.65.0", + "@oxfmt/binding-win32-ia32-msvc": "0.65.0", + "@oxfmt/binding-win32-x64-msvc": "0.65.0" + }, + "peerDependencies": { + "svelte": "^5.0.0", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + }, + "vite-plus": { + "optional": true + } } }, "node_modules/p-limit": { diff --git a/e2e/package.json b/e2e/package.json index cd53b9c..89d15b0 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -3,7 +3,7 @@ "private": true, "type": "module", "dependencies": { - "@rocicorp/zero": "1.10.0", + "@rocicorp/zero": "1.11.3", "postgres": "3.4.9" }, "overrides": { diff --git a/harness/package-lock.json b/harness/package-lock.json index bf18ad2..afdd63c 100644 --- a/harness/package-lock.json +++ b/harness/package-lock.json @@ -6,7 +6,7 @@ "": { "name": "zero-server-harness", "dependencies": { - "@rocicorp/zero": "1.10.0", + "@rocicorp/zero": "1.11.3", "postgres": "3.4.9" }, "devDependencies": { @@ -16,12 +16,12 @@ } }, "node_modules/@badrap/valita": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/@badrap/valita/-/valita-0.3.11.tgz", - "integrity": "sha512-oak0W8bycFjnrLeVCVvZqkOWTGh74wCPKUxGLJyhRukRs+V/hQdfZp1eDcQE4Gf3UrtJWfR/Ou4Xe0DZqJZ2FA==", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@badrap/valita/-/valita-0.5.5.tgz", + "integrity": "sha512-+Hp6PfoOyD6hpONZOiJTIP3xiqPPLMtMzDjJ522Hoba1ynqKKnt7bW82L5+G8klLoUHmcuxfCNpND93KyiA7CQ==", "license": "MIT", "engines": { - "node": ">= 16" + "node": ">= 22" } }, "node_modules/@bcoe/v8-coverage": { @@ -482,9 +482,9 @@ } }, "node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", + "version": "0.223.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.223.0.tgz", + "integrity": "sha512-Oepm6etVPFCn0VWB1t833iOTgz1eYBDUPxcrOR/XvwzD1+Ag9VzSPSrrKAFNJ/haatj+8CXbG47pZXvdNQKDJw==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/api": "^1.3.0" @@ -879,18 +879,6 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/instrumentation-bunyan/node_modules/@opentelemetry/api-logs": { - "version": "0.223.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.223.0.tgz", - "integrity": "sha512-Oepm6etVPFCn0VWB1t833iOTgz1eYBDUPxcrOR/XvwzD1+Ag9VzSPSrrKAFNJ/haatj+8CXbG47pZXvdNQKDJw==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/@opentelemetry/instrumentation-cassandra-driver": { "version": "0.68.0", "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-cassandra-driver/-/instrumentation-cassandra-driver-0.68.0.tgz", @@ -1314,18 +1302,6 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/instrumentation-openai/node_modules/@opentelemetry/api-logs": { - "version": "0.223.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.223.0.tgz", - "integrity": "sha512-Oepm6etVPFCn0VWB1t833iOTgz1eYBDUPxcrOR/XvwzD1+Ag9VzSPSrrKAFNJ/haatj+8CXbG47pZXvdNQKDJw==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/@opentelemetry/instrumentation-oracledb": { "version": "0.48.0", "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-oracledb/-/instrumentation-oracledb-0.48.0.tgz", @@ -1381,18 +1357,6 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/instrumentation-pino/node_modules/@opentelemetry/api-logs": { - "version": "0.223.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.223.0.tgz", - "integrity": "sha512-Oepm6etVPFCn0VWB1t833iOTgz1eYBDUPxcrOR/XvwzD1+Ag9VzSPSrrKAFNJ/haatj+8CXbG47pZXvdNQKDJw==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/@opentelemetry/instrumentation-redis": { "version": "0.71.0", "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis/-/instrumentation-redis-0.71.0.tgz", @@ -1460,18 +1424,6 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/instrumentation-runtime-node/node_modules/@opentelemetry/api-logs": { - "version": "0.223.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.223.0.tgz", - "integrity": "sha512-Oepm6etVPFCn0VWB1t833iOTgz1eYBDUPxcrOR/XvwzD1+Ag9VzSPSrrKAFNJ/haatj+8CXbG47pZXvdNQKDJw==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/@opentelemetry/instrumentation-socket.io": { "version": "0.70.0", "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-socket.io/-/instrumentation-socket.io-0.70.0.tgz", @@ -1536,30 +1488,6 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/instrumentation-winston/node_modules/@opentelemetry/api-logs": { - "version": "0.223.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.223.0.tgz", - "integrity": "sha512-Oepm6etVPFCn0VWB1t833iOTgz1eYBDUPxcrOR/XvwzD1+Ag9VzSPSrrKAFNJ/haatj+8CXbG47pZXvdNQKDJw==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/instrumentation/node_modules/@opentelemetry/api-logs": { - "version": "0.223.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.223.0.tgz", - "integrity": "sha512-Oepm6etVPFCn0VWB1t833iOTgz1eYBDUPxcrOR/XvwzD1+Ag9VzSPSrrKAFNJ/haatj+8CXbG47pZXvdNQKDJw==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/@opentelemetry/otlp-exporter-base": { "version": "0.223.0", "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.223.0.tgz", @@ -1614,18 +1542,6 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/api-logs": { - "version": "0.223.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.223.0.tgz", - "integrity": "sha512-Oepm6etVPFCn0VWB1t833iOTgz1eYBDUPxcrOR/XvwzD1+Ag9VzSPSrrKAFNJ/haatj+8CXbG47pZXvdNQKDJw==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/@opentelemetry/propagator-aws-xray": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-aws-xray/-/propagator-aws-xray-2.2.0.tgz", @@ -1794,18 +1710,6 @@ "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, - "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/api-logs": { - "version": "0.223.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.223.0.tgz", - "integrity": "sha512-Oepm6etVPFCn0VWB1t833iOTgz1eYBDUPxcrOR/XvwzD1+Ag9VzSPSrrKAFNJ/haatj+8CXbG47pZXvdNQKDJw==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/@opentelemetry/sdk-metrics": { "version": "2.12.0", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.12.0.tgz", @@ -1863,18 +1767,6 @@ "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, - "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/api-logs": { - "version": "0.223.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.223.0.tgz", - "integrity": "sha512-Oepm6etVPFCn0VWB1t833iOTgz1eYBDUPxcrOR/XvwzD1+Ag9VzSPSrrKAFNJ/haatj+8CXbG47pZXvdNQKDJw==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/@opentelemetry/sdk-trace": { "version": "2.12.0", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.12.0.tgz", @@ -1952,9 +1844,9 @@ } }, "node_modules/@oxfmt/binding-android-arm-eabi": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.45.0.tgz", - "integrity": "sha512-A/UMxFob1fefCuMeGxQBulGfFE38g2Gm23ynr3u6b+b7fY7/ajGbNsa3ikMIkGMLJW/TRoQaMoP1kME7S+815w==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.65.0.tgz", + "integrity": "sha512-M10Gs1SSpTNI6ahGx3M/OlIdUF4hkaP6OgUb+MS79t/Pgflk3r1nW5gPFqsZGUAXg0H1AfANT9AvLdBSTIhZKg==", "cpu": [ "arm" ], @@ -1968,9 +1860,9 @@ } }, "node_modules/@oxfmt/binding-android-arm64": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.45.0.tgz", - "integrity": "sha512-L63z4uZmHjgvvqvMJD7mwff8aSBkM0+X4uFr6l6U5t6+Qc9DCLVZWIunJ7Gm4fn4zHPdSq6FFQnhu9yqqobxIg==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.65.0.tgz", + "integrity": "sha512-6DXH5sftNlaHpWJG50hFMF+Qxtq5D2TmahvcDPxWNcGIf8qrC9Y0YgHYcYZ2hlWzaccKXh/f3GcssH8vtkl4JA==", "cpu": [ "arm64" ], @@ -1984,9 +1876,9 @@ } }, "node_modules/@oxfmt/binding-darwin-arm64": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.45.0.tgz", - "integrity": "sha512-UV34dd623FzqT+outIGndsCA/RBB+qgB3XVQhgmmJ9PJwa37NzPC9qzgKeOhPKxVk2HW+JKldQrVL54zs4Noww==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.65.0.tgz", + "integrity": "sha512-K9m7lr53pcOLETNsC88sWes/GWHUGjZyHx95UhYcSXy0r30haLdeXlSufSenEAtoLaW753WN8/l4M7GYcRt6cg==", "cpu": [ "arm64" ], @@ -2000,9 +1892,9 @@ } }, "node_modules/@oxfmt/binding-darwin-x64": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.45.0.tgz", - "integrity": "sha512-pMNJv0CMa1pDefVPeNbuQxibh8ITpWDFEhMC/IBB9Zlu76EbgzYwrzI4Cb11mqX2+rIYN70UTrh3z06TM59ptQ==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.65.0.tgz", + "integrity": "sha512-sTNwIx1gre3MyiHOPLu7IGW4UyMScYL4DTmJT01p4vzB0En+OJUQz6KuH8t0PpsClRSaMuY3b0QmtoPItfO8Lg==", "cpu": [ "x64" ], @@ -2016,9 +1908,9 @@ } }, "node_modules/@oxfmt/binding-freebsd-x64": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.45.0.tgz", - "integrity": "sha512-xTcRoxbbo61sW2+ZRPeH+vp/o9G8gkdhiVumFU+TpneiPm14c79l6GFlxPXlCE9bNWikigbsrvJw46zCVAQFfg==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.65.0.tgz", + "integrity": "sha512-lYZMVIiIpnjGu5hJb2jxA8NYQ/e0OTGuaiAf4dqlGPNnPmUTu23FZRMltmjro/KkQm1uE4NT4n5yJ2zWmKcpfA==", "cpu": [ "x64" ], @@ -2032,9 +1924,9 @@ } }, "node_modules/@oxfmt/binding-linux-arm-gnueabihf": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.45.0.tgz", - "integrity": "sha512-hWL8Hdni+3U1mPFx1UtWeGp3tNb6EhBAUHRMbKUxVkOp3WwoJbpVO2bfUVbS4PfpledviXXNHSTl1veTa6FhkQ==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.65.0.tgz", + "integrity": "sha512-gIdXFAt/bURnjxuoedDEWdZ0PEWEmdDcm8qdpoFYYvW3QMk/5D4vUaH4mlMeRpeTdST4izUgHVO6RawQ4QulJw==", "cpu": [ "arm" ], @@ -2048,9 +1940,9 @@ } }, "node_modules/@oxfmt/binding-linux-arm-musleabihf": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.45.0.tgz", - "integrity": "sha512-6Blt/0OBT7vvfQpqYuYbpbFLPqSiaYpEJzUUWhinPEuADypDbtV1+LdjM0vYBNGPvnj85ex7lTerEX6JGcPt9w==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.65.0.tgz", + "integrity": "sha512-jJVyADto7gA2AaX5qAjAexrxx9PJQaKWOe8PICE7yKMbjBRyOHcmj9TtVJ+MZYDUQ3hodU0AcoTj0jFQ1W4C6Q==", "cpu": [ "arm" ], @@ -2064,9 +1956,9 @@ } }, "node_modules/@oxfmt/binding-linux-arm64-gnu": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.45.0.tgz", - "integrity": "sha512-jLjoLfe+hGfjhA8hNBSdw85yCA8ePKq7ME4T+g6P9caQXvmt6IhE2X7iVjnVdkmYUWEzZrxlh4p6RkDmAMJY/A==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.65.0.tgz", + "integrity": "sha512-p3RFkB+u7u+8up99b/NEcI1hdpLDiGgJYNwDorB60n7eH+eKposAKuMBxx+NqB3b+sJP4CZmYDh9G7X62tUsKg==", "cpu": [ "arm64" ], @@ -2083,9 +1975,9 @@ } }, "node_modules/@oxfmt/binding-linux-arm64-musl": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.45.0.tgz", - "integrity": "sha512-XQKXZIKYJC3GQJ8FnD3iMntpw69Wd9kDDK/Xt79p6xnFYlGGxSNv2vIBvRTDg5CKByWFWWZLCRDOXoP/m6YN4g==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.65.0.tgz", + "integrity": "sha512-5Prb0uFzJHr+OUD/qS/TmU526wD+PaHDsm3KoRiUXbMIDpTSErjeQYkK3OQeshAvD/PuLa9WGEi9WPajjdOZJg==", "cpu": [ "arm64" ], @@ -2102,9 +1994,9 @@ } }, "node_modules/@oxfmt/binding-linux-ppc64-gnu": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.45.0.tgz", - "integrity": "sha512-+g5RiG+xOkdrCWkKodv407nTvMq4vYM18Uox2MhZBm/YoqFxxJpWKsloskFFG5NU13HGPw1wzYjjOVcyd9moCA==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.65.0.tgz", + "integrity": "sha512-S8svxTp81obnF3admN9yd+u2rOYXtyzThLGBTg1PY6TPtGcC09BaaXLQD+TBSMa7yvqhCDZ8DFri+S/yG60qCg==", "cpu": [ "ppc64" ], @@ -2121,9 +2013,9 @@ } }, "node_modules/@oxfmt/binding-linux-riscv64-gnu": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.45.0.tgz", - "integrity": "sha512-V7dXKoSyEbWAkkSF4JJNtF+NJZDmJoSarSoP30WCsB3X636Rehd3CvxBj49FIJxEBFWhvcUjGSHVeU8Erck1bQ==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.65.0.tgz", + "integrity": "sha512-WtXBr75G/h2qOHy8SiGtC1R6aS3jt4mE52v1D8AtwMXIgoOmSNP9lKvbSaTRoL0e5wsMPoi6T72QWDYPu+S+nA==", "cpu": [ "riscv64" ], @@ -2140,9 +2032,9 @@ } }, "node_modules/@oxfmt/binding-linux-riscv64-musl": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.45.0.tgz", - "integrity": "sha512-Vdelft1sAEYojVGgcODEFXSWYQYlIvoyIGWebKCuUibd1tvS1TjTx413xG2ZLuHpYj45CkN/ztMLMX6jrgqpgg==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.65.0.tgz", + "integrity": "sha512-YwSLVvpaz4o/nv/miiPEBJz+eJ+VmbgNIrao6RccK9ce+L5EA8wP+ZD0uFeq6wKOza6zoWv/dR0sj6lip6R3EA==", "cpu": [ "riscv64" ], @@ -2159,9 +2051,9 @@ } }, "node_modules/@oxfmt/binding-linux-s390x-gnu": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.45.0.tgz", - "integrity": "sha512-RR7xKgNpqwENnK0aYCGYg0JycY2n93J0reNjHyes+I9Gq52dH95x+CBlnlAQHCPfz6FGnKA9HirgUl14WO6o7w==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.65.0.tgz", + "integrity": "sha512-XQTPqgvyrgkKcFq+Tp2eK6JS7sqqJ+nRmy2Fav4j3I+i4dJoPJm7YwEdoeSDX9xkqj9jZ/lWfF3bXUWztIrn6A==", "cpu": [ "s390x" ], @@ -2178,9 +2070,9 @@ } }, "node_modules/@oxfmt/binding-linux-x64-gnu": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.45.0.tgz", - "integrity": "sha512-U/QQ0+BQNSHxjuXR/utvXnQ50Vu5kUuqEomZvQ1/3mhgbBiMc2WU9q5kZ5WwLp3gnFIx9ibkveoRSe2EZubkqg==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.65.0.tgz", + "integrity": "sha512-cjZlx6S/VkeCNWCbwZriTnLnZeTcV3DEyeRGSw/2wwLP9viq+C0bJ4bC1k/ZLkFxDcB1lUgSasPkYGP1bdraOg==", "cpu": [ "x64" ], @@ -2197,9 +2089,9 @@ } }, "node_modules/@oxfmt/binding-linux-x64-musl": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.45.0.tgz", - "integrity": "sha512-o5TLOUCF0RWQjsIS06yVC+kFgp092/yLe6qBGSUvtnmTVw9gxjpdQSXc3VN5Cnive4K11HNstEZF8ROKHfDFSw==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.65.0.tgz", + "integrity": "sha512-2azCjxdLtK4zCcIOU1dlXlU0xxfbPi6EjwWx7Ac7teWPidIIDOcIhudup83xNCKYhtqeVd/gaVDOxbUq4syXWA==", "cpu": [ "x64" ], @@ -2216,9 +2108,9 @@ } }, "node_modules/@oxfmt/binding-openharmony-arm64": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.45.0.tgz", - "integrity": "sha512-RnGcV3HgPuOjsGx/k9oyRNKmOp+NBLGzZTdPDYbc19r7NGeYPplnUU/BfU35bX2Y/O4ejvHxcfkvW2WoYL/gsg==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.65.0.tgz", + "integrity": "sha512-KXQ7xi1e/voP0IQaw6fG6XY4Z5+Llf1XmRSZS1t7pVFCecFJ0iXaboKmVwjFtp5MLlT5iWQrJ2U1C3GJdZ2u+Q==", "cpu": [ "arm64" ], @@ -2232,9 +2124,9 @@ } }, "node_modules/@oxfmt/binding-win32-arm64-msvc": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.45.0.tgz", - "integrity": "sha512-v3Vj7iKKsUFwt9w5hsqIIoErKVoENC6LoqfDlteOQ5QMDCXihlqLoxpmviUhXnNncg4zV6U9BPwlBbwa+qm4wg==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.65.0.tgz", + "integrity": "sha512-2FbbjG5jEqLSLKVJwBap84uJfpn5Y5A53KEO0aUNr+zeiRB9nyPUIFMcSbZVMFLitfBytFWRNngozXYjb6Rsbw==", "cpu": [ "arm64" ], @@ -2248,9 +2140,9 @@ } }, "node_modules/@oxfmt/binding-win32-ia32-msvc": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.45.0.tgz", - "integrity": "sha512-N8yotPBX6ph0H3toF4AEpdCeVPrdcSetj+8eGiZGsrLsng3bs/Q5HPu4bbSxip5GBPx5hGbGHrZwH4+rcrjhHA==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.65.0.tgz", + "integrity": "sha512-LJ+ZacAPSjegDOnSLyA1TMWAhdDrsK4el3REdr1oL2UtVBCMhO2II/Sb3cEW6mF2MfLhl8hDNCSvc7KSbgk3LQ==", "cpu": [ "ia32" ], @@ -2264,9 +2156,9 @@ } }, "node_modules/@oxfmt/binding-win32-x64-msvc": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.45.0.tgz", - "integrity": "sha512-w5MMTRCK1dpQeRA+HHqXQXyN33DlG/N2LOYxJmaT4fJjcmZrbNnqw7SmIk7I2/a2493PPLZ+2E/Ar6t2iKVMug==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.65.0.tgz", + "integrity": "sha512-higu9cWEO6XXFzATD1jf0mCK34rNfN2H9JrJie7QB1IhleVpTh0QlLH9Ip2C1H/Nd5n0v5pvRtC+5R0uE4HpVg==", "cpu": [ "x64" ], @@ -2391,12 +2283,12 @@ } }, "node_modules/@rocicorp/zero": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@rocicorp/zero/-/zero-1.10.0.tgz", - "integrity": "sha512-u+0mrWEP0IBAQY4lbtnhNjpyjee5zSTNxpeY9j14apME1PYshmhdTvbNwmyaoumvBt9MrQGCEr5y7W6utj+Vjg==", + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@rocicorp/zero/-/zero-1.11.3.tgz", + "integrity": "sha512-xP3yPX5q2zauLaZyPICoV7j/16PsyTixTyv85Kj/aFUT48JIyOHrAjsthHlpZhe46XbSlXHzDGSuqIjuwVJmHQ==", "license": "Apache-2.0", "dependencies": { - "@badrap/valita": "0.3.11", + "@badrap/valita": "0.5.5", "@databases/escape-identifier": "^1.0.3", "@databases/sql": "^3.3.0", "@dotenvx/dotenvx": "^1.39.0", @@ -2404,20 +2296,20 @@ "@fastify/cors": "^10.0.0", "@fastify/websocket": "^11.0.0", "@google-cloud/precise-date": "^4.0.0", - "@opentelemetry/api": "^1.9.0", - "@opentelemetry/api-logs": "^0.218.0", - "@opentelemetry/auto-instrumentations-node": "^0.76.0", - "@opentelemetry/exporter-metrics-otlp-http": "^0.218.0", - "@opentelemetry/resources": "^2.7.1", - "@opentelemetry/sdk-metrics": "^2.7.1", - "@opentelemetry/sdk-node": "^0.218.0", - "@opentelemetry/sdk-trace-node": "^2.7.1", - "@opentelemetry/semantic-conventions": "^1.41.1", + "@opentelemetry/api": "1.9.1", + "@opentelemetry/api-logs": "0.223.0", + "@opentelemetry/auto-instrumentations-node": "0.81.2", + "@opentelemetry/exporter-metrics-otlp-http": "0.223.0", + "@opentelemetry/resources": "2.12.0", + "@opentelemetry/sdk-metrics": "2.12.0", + "@opentelemetry/sdk-node": "0.223.0", + "@opentelemetry/sdk-trace-node": "2.12.0", + "@opentelemetry/semantic-conventions": "1.43.0", "@postgresql-typed/oids": "^0.2.0", "@rocicorp/lock": "^1.0.4", "@rocicorp/logger": "^6.1.0", "@rocicorp/resolver": "^1.0.2", - "@rocicorp/zero-sqlite3": "^1.1.4", + "@rocicorp/zero-sqlite3": "^1.1.5", "@standard-schema/spec": "^1.0.0", "@types/basic-auth": "^1.1.8", "@types/ws": "^8.5.12", @@ -2427,24 +2319,23 @@ "cloudevents": "^10.0.0", "command-line-args": "^6.0.1", "command-line-usage": "^7.0.3", - "compare-utf8": "^0.2.0", + "compare-utf8": "^0.2.3", "defu": "^6.1.4", "eventemitter3": "^5.0.1", - "fastify": "^5.0.0", + "fastify": "^5.12.7", "is-in-subnet": "^4.0.1", "jose": "^5.9.3", - "js-xxhash": "^4.0.0", "json-custom-numbers": "^3.1.1", "kasi": "^1.1.0", - "nanoid": "^5.1.2", - "oxfmt": "^0.45.0", + "nanoid": "^5.1.16", + "oxfmt": "^0.65.0", "parse-prometheus-text-format": "^1.1.1", "pg-format": "npm:pg-format-fix@^1.0.5", "postgres": "3.4.7", "semver": "^7.5.4", "url-pattern": "^1.0.3", "urlpattern-polyfill": "^10.1.0", - "ws": "^8.18.1" + "ws": "^8.22.0" }, "bin": { "analyze-query": "out/zero/src/analyze-query.js", @@ -4566,15 +4457,6 @@ "url": "https://github.com/sponsors/panva" } }, - "node_modules/js-xxhash": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-xxhash/-/js-xxhash-4.0.0.tgz", - "integrity": "sha512-3Q2eIqG6s1KEBBmkj9tGM9lef8LJbuRyTVBdI3GpTnrvtytunjLPO0wqABp5qhtMzfA32jYn1FlnIV7GH1RAHQ==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/json-bigint": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", @@ -4942,9 +4824,9 @@ } }, "node_modules/oxfmt": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.45.0.tgz", - "integrity": "sha512-0o/COoN9fY50bjVeM7PQsNgbhndKurBIeTIcspW033OumksjJJmIVDKjAk5HMwU/GHTxSOdGDdhJ6BRzGPmsHg==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.65.0.tgz", + "integrity": "sha512-SgS5VgnP42T0zl3zWD+xoH8FCqg1SAFnSRoOT/qeoa6gxcYIqrDMOmcXIg/EWSN92Du4ogB4riuKhKd6Y4CGhw==", "license": "MIT", "dependencies": { "tinypool": "2.1.0" @@ -4959,25 +4841,37 @@ "url": "https://github.com/sponsors/Boshen" }, "optionalDependencies": { - "@oxfmt/binding-android-arm-eabi": "0.45.0", - "@oxfmt/binding-android-arm64": "0.45.0", - "@oxfmt/binding-darwin-arm64": "0.45.0", - "@oxfmt/binding-darwin-x64": "0.45.0", - "@oxfmt/binding-freebsd-x64": "0.45.0", - "@oxfmt/binding-linux-arm-gnueabihf": "0.45.0", - "@oxfmt/binding-linux-arm-musleabihf": "0.45.0", - "@oxfmt/binding-linux-arm64-gnu": "0.45.0", - "@oxfmt/binding-linux-arm64-musl": "0.45.0", - "@oxfmt/binding-linux-ppc64-gnu": "0.45.0", - "@oxfmt/binding-linux-riscv64-gnu": "0.45.0", - "@oxfmt/binding-linux-riscv64-musl": "0.45.0", - "@oxfmt/binding-linux-s390x-gnu": "0.45.0", - "@oxfmt/binding-linux-x64-gnu": "0.45.0", - "@oxfmt/binding-linux-x64-musl": "0.45.0", - "@oxfmt/binding-openharmony-arm64": "0.45.0", - "@oxfmt/binding-win32-arm64-msvc": "0.45.0", - "@oxfmt/binding-win32-ia32-msvc": "0.45.0", - "@oxfmt/binding-win32-x64-msvc": "0.45.0" + "@oxfmt/binding-android-arm-eabi": "0.65.0", + "@oxfmt/binding-android-arm64": "0.65.0", + "@oxfmt/binding-darwin-arm64": "0.65.0", + "@oxfmt/binding-darwin-x64": "0.65.0", + "@oxfmt/binding-freebsd-x64": "0.65.0", + "@oxfmt/binding-linux-arm-gnueabihf": "0.65.0", + "@oxfmt/binding-linux-arm-musleabihf": "0.65.0", + "@oxfmt/binding-linux-arm64-gnu": "0.65.0", + "@oxfmt/binding-linux-arm64-musl": "0.65.0", + "@oxfmt/binding-linux-ppc64-gnu": "0.65.0", + "@oxfmt/binding-linux-riscv64-gnu": "0.65.0", + "@oxfmt/binding-linux-riscv64-musl": "0.65.0", + "@oxfmt/binding-linux-s390x-gnu": "0.65.0", + "@oxfmt/binding-linux-x64-gnu": "0.65.0", + "@oxfmt/binding-linux-x64-musl": "0.65.0", + "@oxfmt/binding-openharmony-arm64": "0.65.0", + "@oxfmt/binding-win32-arm64-msvc": "0.65.0", + "@oxfmt/binding-win32-ia32-msvc": "0.65.0", + "@oxfmt/binding-win32-x64-msvc": "0.65.0" + }, + "peerDependencies": { + "svelte": "^5.0.0", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + }, + "vite-plus": { + "optional": true + } } }, "node_modules/p-limit": { diff --git a/harness/package.json b/harness/package.json index 704e447..ba8ed45 100644 --- a/harness/package.json +++ b/harness/package.json @@ -3,7 +3,7 @@ "private": true, "type": "module", "dependencies": { - "@rocicorp/zero": "1.10.0", + "@rocicorp/zero": "1.11.3", "postgres": "3.4.9" }, "overrides": { From 0af91a9bb815b346c81d7887db7e00410f065444 Mon Sep 17 00:00:00 2001 From: Jonas Daniels Date: Wed, 28 Oct 2026 13:26:19 -0700 Subject: [PATCH 2/6] feat: build normalized ASTs and compile LIKE and char(n) as @rocicorp/zero 1.11.3 does Co-Authored-By: Claude Opus 5.5 --- ...age.MacBook-Pro-21_local.pid15427.Xwc66hOx | Bin 192512 -> 0 bytes bench/fixtures/query_server_schema_rows.json | 26 +++ bench/fixtures/schema.json | 10 + harness/query.ts | 14 +- harness/schema.ts | 2 + src/zero_server/protocol/ast.py | 221 +++++++++++------- src/zero_server/query/builder.py | 16 +- src/zero_server/schema.py | 4 +- src/zero_server/server/pg_types.py | 9 +- src/zero_server/server/server_schema.py | 3 + src/zero_server/z2s/compiler.py | 14 +- src/zero_server/z2s/sql.py | 17 +- tests/compat_scope.py | 51 ++-- tests/unit/test_protocol.py | 16 -- tests/wire/seed.py | 7 +- tests/wire/test_ast.py | 12 +- tests/wire/test_query.py | 19 ++ tests/wire/test_sql.py | 7 + 18 files changed, 301 insertions(+), 147 deletions(-) delete mode 100644 .coverage.MacBook-Pro-21_local.pid15427.Xwc66hOx diff --git a/.coverage.MacBook-Pro-21_local.pid15427.Xwc66hOx b/.coverage.MacBook-Pro-21_local.pid15427.Xwc66hOx deleted file mode 100644 index ef8900b9854c499b943ba4efdec4d280ddc78c99..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 192512 zcmeFa2b>hex;9?b(=%ON9flkvEjcbx5KxdP8ALLI%d#x6gxzI#L4tx45yb$eV@8jd zBVZO2X2pa#U{08h`5eRU_taEZ_h7o`-f#VX_kVuFz3zFQuAb_in(p_lH`H5GCXX+v zD$ZM2Rx4jVr*Paezb#PjekKR<71UPamQdF90wc?(OH73YlhX!6)Q}tIGc9lYjKtsxt4({@cIJ z@BEg*_dV$6R6{O)QTeHrr6ns?7W+Sbx4ht!-7;UJw5(g6_!4*m+FOyv1L{+Pr?Pl; z<%(sPIu#VItSZa!0{r*|z3|6Q)@4THR;}p96`2P`3{S!QlB!A{m-v1Z}Jwoc2{2ei>nwA$AR<5j2g5=MHr20f4 zCDl9PwczXI84rO|NiZl z7gjCK`|Z7p!?^K|;Yj1d8~)Lai(4q@CH_W|b+pVapB&1J{&EO?@n7hmTNX!xTOL=u zCVy4oGDws1!jcNSvXH@21LTPl7Zl>;!ue$@t1{28pqa;0=$_zyWd{LQV~?{sXHLiK zeRNr6WeL=(P+OG))zQ3;iwi4-6sqi)H@~cGS#e>h)D8H=m`Yr;q%tpa55Lt)M+SXV zt`U7KTGcUgPfEE6F+UYAoqE3jmM2KwH{D5XG@`JoxT<7%ah^COb2FI->-fUT zyrPO?G#lOBQ+0AIQC>bihP3MlGKYMn>Ph8MT{1sL*6H~cZD{3_E+Lgvu zdT+24P6d^i2`n#E-_@hqK~L7N8iR*y5~MVyNZAjU81Sqy6jWY8NJROw&zD1|&hjQm zyt9M5q|u)U{y+CGHv+j4$c;d51ac#g8-d&iMj$r=e>eiR z9%>O)=@-2Y356Q=5$OlLd-m>^->YYS@BVqc`V8zjU|_F4Yc;(^tVCs59NV$Sv`QpL z^jK0_xnNmkkM31{dK_O?v9zk9xVW;%+Tx0`{PHzbi_1!TRQ1VUuwX#H!{+zx-J|=e z;?h+;=9iTAKx1)bRmX059rIUb{;tUUEf;@F%LWz|BGaI}qHI;kg5rvfbL-Ghv!-4S zz+v5c_3Pf7;~VMG=SlSW=*Q9TqHq1-7tA%}Mj$r=xe>^XKyCzbBaj<`+z8}GAU6WJ z5y*`|ZUk~8@V{sT>`)7;91%JTHKf815fVq0&q3p%W~S!_2;)))>ufw@R2pvNmdK3ox8XCx-SwyR)q11Q z&d@aNAKIx}H~JV|Obv2fEf31hzrsGw?1d2uDy zniO6DqnB=;|fJ(3$WH({I>I2)z)1r0kN`jaRGh;tOKts zs}NUcSzA}|e^{(Nu2|yR87|ScHh$j9#aNxZpa)h(mS9C;`I=)7LC=TRM$i5L$+dDq4HPv+=d~$D>I^}o?aMyDhq37C_X=+f=dtpQLUMr8Jf8Uwj zi-R|-paCvXD=PlrOF(#{r~&IkQF?UCy!4Wv#sV-WV*Y z#4}f3Sdp2yPUq;oSFH|1W}-LSM8CE9EN4quhQ4b>yZNHTo7>GW&~vT4C4SGCIf@@K zf{ttDNAx?+9*}Tt4TyXP=dUbThJZEhdzOx#Yvl!$J!eUzS)tnPI%_Q{&li zkH`+EpZl{ritlkRvz{}mw6C9vf4e{rkpqVE=bKPLCZ~ zo7Xtl{_n6rkBzI%?)|TjW&gMT>n;))x9#TYu|c)+O?+XSW&gLGug6B!O7DMY|F_EvHzRT(qnyV<>&pi{oibk9y`2NdiDn>u>TLKZRvn(2&4p3L9a|8-~Tu|Bo(6@F#^*Qqsu@H_T@wAOyp-`M}|G(DDIn*)-u|D9v>SjXDw zRoVY`t-blbXa8F>^jNRjxncRt3GV`|C0^Ula+Z`eYhTKG_CKq&x5O7x(*8GU?ML)G z&Ki(NtqsVp?f-DCy@0>5|Mgn#I%_;aQ}tNaT6w5_`yU2+-e2%;i}nAuZK3{ObZfM= zd$&8y`O!Jo>1N++ueHaZA@?sg0=W^$jX-V$awCu%f!qk>Mj$r=xe@qJ9DyV35ZNGB z6Ib>S)@Tm_3#9cpaA1a%ie18%m8>nUELc{uw76hpIg&ey3zwfzv9h#>6(Yrd`ss?Y zm8A;`swzs#E5!$fiNL@4r=KV+Dk?6osw_Ajh2Z^*6?jKQAIn6Lf9`Mk@KRUd1@LYh zTXhsJ6aI`f1@n=TQd(Icl6KJZLX_#RDnROERq^uTQjubbwbm;$M=GjtuYP{2xY}Y7 z_Mi4&U#+kJ+a8n_XYyKu9%W8RWdS;>EGWjh?CkE<{gfSFDy}qNqydcicdq1RwiYZa zTZFRx*%w9?_=0lm0)S&Hmn|qLEyMRI#E&+=uxP2c#w?K$aM*w68sg))2!6zh;;NMu z_!?pev%YH?cFYl7FA(Vf6aG70X9h*|mNn#=9|?PsWL@Fd^2#+uW#xzFCNdh$Ghj+qO-jKRfx(dG0;#^k|#t zSswsQAx6{VV(58m`C8KmWvkY5()(dTj1*`}m9Of3rl7P5U>lc90qFdlddV_CL#H z0siK<_|5k!RGF>+1N+~|gunkg*Z8;X|46YOoA7T9lyYDEPwfBjUv`yj`(H2DV?}?- zcliVRKeSknP5Fy2n6dx0P{=Vw{GZgxNx$3(Mj$r= zxe>^XKyC#7lSe?r|8w^LfATM!`);`r$c;d51ac#g8-d&iXi0QlbVhVybY%4K zXs>9;Xp3n5sOi?Y-?^W<@3=3!d)+I9_Hq| zZQZ7B)D1g7IbS&+Id3}qoX4H}oLimioXec^oYS0yv&JcNik)Mf$<8QekkiNM?6h(k zI+jE2AMDTV_pm3yGxo#wJ@!ufTKh8lJo_{|VXv{v>|*;^d$K*s9%T2iJKL@7hPGu> z>j&#|>pkmL>ly1|>u&27>niJF>nv-NwZU3xEwu`*8P)`AxHZ7)VYRmov7(mF|HZ%H z@9|goUhH^qJHMV^#?R%a@{{;VzJwpgr|{AIaNeD_<_$PAtIcoCPt5)1KJzhix4FZ- z%Dl+jVy4Y?X1Q5x&Ne5RBg_G2ceAb8#B@xJ{lGqB@35EHlk9$WE4!9m%Fbq+*m_pU z7PC2QGCPVL&U&);><|`Zy74dL3*$ZG6=SdQpmDo#y>Xdwu5qeylCjcQViXutjWNbx zqmR+aXkpYdB9WgWUqs%CybyUba#!Sr$hOE?k#uBDWLcyja!h1oq<^Gqq-CUTL=XQE zJ`jF0{A_qn__pvh;fum&gyZ3r;l<%&!xO_p!-s}Dh7SolVWNMdf2hBTT^;Vzcj(*o zzv-vy>-81-0)2))P9LoI(%b2cbsnk-eH;2Dv_G^j^hoH=(Dk89Lt8_s(CX0A&~c&3 zp%J0OLR~^FLk&VaM6~aNjBV0TR}$ z&*J?htXq%850fxjm&N-@=tfz*uY``v;)hCTJ1pKuLT;P!-hdi4v~UfN_Yzn$$W!D^ zBi<9lqy{(QJ;d3jQDelrXO2aH@-xRGHDfznk9YFOI=X?!J9?yop3LJNJW@&5@pyZ`rIN?nNm6|}t>E#tUdu8;+IZwRx{SwL zdt@p-j>q#nGMP^0@m3z0Kqs?!OAt!K!=|x#3lENG@#Y>J!{W_6IGV)|@!%*PZwe^B z-)K64$D4R$1Rc%ejXg55`W_x{%G&W9=VEa%$Z4Xb z*l#(F#}-IZQzcr8yq2^e^F0zHX&x(7q-Fz;&GSC9fy7v>00i$zYMmK7PCzZZP8^;q z53ftH*c=JV*0I>J9^ArWvnAY8#$vN1ECHM;;ldIYo8iG?7Mm_%@j@1xCSf6tPW9kC z7MmjByh0W`M#8x`I@yDBSnOyC=geiXNfOS)(TNgHo5^AmBpg4C#l}lG8gQJ1qX5TB zcog6m3C97BmT=fnEH+BQF*tgZgrmo>*hmRSjb^bC5{|?P!zCOxlEsEeI08q9dPhgF z*bonnGh;^zsD%en93CtW4;jT`gCrb0gvE}KaL`~DJ6ys&gIG+egYW=AsSd&eA7wGA z4#EQlvY1o{;YSCsm{bShet=RPg!}emF{uv1efqMPR0rYSeOOGW1N`8WfZcntSVkGpaDI0d6WRbryXCW3Mjg;_`)({I^Z|~xZ^vRn9RPNIgT;h00PNJ6 z#e^yV?9hqDgdzZJ*MY@^8UTC)9}`MI9Bt2HLInV}#!;aF0P|Y2m=OPft@2o`jf5>* zu~=&f>$YUEJPDx-V?ylXTqnw6p3sK^jCn%evRTX%`rKkMPw1Q6hzX%@(y(d7gv_UA zxW_J`#fXH*&S653!J0>zAq{XpGOh{;;ffJb! z&p?L`WI{Fr4Z zYbInc(7P>}5WwQRR!m4=pk^(Z5WPT6n=v7GftoaBLg)fDYQluX1!~xc6-v~gArrC| z$1tgrf>y6vpJn7M)$!~~DXU`|CxxtzS)7!yI-X%EVs*KDNr@YKq*hP-J_Tkr`qs|OiEL2OCWWWgb03qkQ|r-_Nztk0_h3?TYIs7V;MDNcNV%zX+|Q)g z)H-xzQfg{l_A@CowN71_l$jdDoD`Yb{tiq^Os!2mlLAv~-G)hdskP2yQe0|~a8g=o z5O7jhYOPu_DJwNRaZ*%jEm|@uDYd2tm=u&+i{?zqNv(McCdH(Nr%*~s4NqZfIerHk zF)1Up#?6@&ky_(MOiDqkb3%XuBGtUIw&jXVlGryf#Li3}}^y()r?? zmaPmo1DdonoD68(#IQ4o&gOS8nBwWU%U{1yIy!K{&qfp3jTID?@0V@ zcTOk#ZMk(V{x&;v4E{DfJ&M1LPhEn)4L1$K-})Ox4|S7s@wZNVpcXxzMBTT=HUOPq z`)kqH-M8JZqjyKIi=H3d80{Nv7i|zNj!uK!KPfuWe%yY`ncyUyN8I}E4H-{BhiJ2C zooLwo(f!hW-+k46+Pxf}fIHmt+*R&oH|8#N=fMXs);-ca)a~rHbUt#K^M><_^R@Gw zbGLH=_8Pp|*$&IU$SHM(Ifpqjoo-HRr;$VK1CC{XhdP8Wz}~;w-e_;NPp~hxSJ;c} zx#l;p#P?Zw)^h6#dkX3l4u<95$!=lSvkmKK>nrO+>vim1_^@@Cb(58_&bLmtR$KF} z@v!-aTK%l9Rzv=7+lIL-nQlIr@F!Md6nt(MXHPZ0w9!!|pQ|n#aQaA8sCI zb~Rg>^-Y8Q#J*(jv6o@}?_>#fJiCfr$j)F(Spl2EM#1v$!P>IM%rd?)UN=56_86DI z>c7rN8>L2X?L+M~?P+7H_Mmo~vDTPx9Adb}bVD=7!UxbH@_pn$WPjxO$Rn`*Z;V_X zIX7}jWPPL}vM@3+GAzM{f^n-weGKx+-*jXk_T8(C49BL(hfw zgia0h4>b>wkgI*IU9DZ9ZPwOn<=RyElOn<(f=;cVm9)KL!nURD6r(V1XZROgQCf$ZKI1e>na^POQp0Dkd}+jIoI03Ls-KWk zo9c=Qno|@J{ki#Rv>x@`KM{36t-A22+@P=QG&C9^WUs=bhmofg9yN?S zsqj(5$P)^W8%7>ic-T?oF@?trBabRPdJK6);ZdW>!wQcaMjldl*hsQR;Ss~gg9;BD zK_2k&IC8%lbQsX&KGiT}6uDR7!9&Pyg$E5L_b9w)5V>370fWe03J-jg+^O(@f#eQ_ zA00q$SGeB*a+|_^`;l7}?$ej-Qn+^?vQyz+y~z%Rd-fu?DBQg#xmn@-?&KzgyXBJ` z6>i^++~DJO8Tk!uug*MVHE@Eh&ORSLIjPp(wBbvv?M;k?%5 z3WZzck-sb4vK6^p;kqr!WeP{@l5Gk*QF5umwnHva*s{sR3UiBGq_D}!g&c2;@P3%& z0s)~f!ZqZ4PjLu4oBYjt-7C<6)vkHr}?;)Y*u*1R&uJs$Xc2Y}L9%!FO9ORjhYlG*s#%j1sgOhRIq-7c?#C8U!Y*L?r{pb(YXpb?i>Ye=U4?T zd$yT*+F_^95(xjN*Ua>eg=%Ja$3ivJy^p|FpQhlr-BT4DGj584V|O2;;KV7D6&y46 zXaz@)nWW&TJ0~i5=cow^jv75)!QG?ADL8!MSOrH8AEV&#k)stHF?^JQ!$%yY;E)j` z6+B|^2nG8+HC(~I{e~&{RNtWr_CI2Xf_;V@so!5%&PE12KoFa`JJ_fs&xdtU`R?mtw)4jua_*kylj1v_==rC^6nJr&&Fp@)KP z^1Cb8x=p?h^SUXR*Sf2Mt@66~uw`cjTej+?V2hR=6>NH-gMuxZw^y)vi*^b&Y1&r7 zMoro%c%V^h1sgZd^I@Y_3N~uo(u1&T)%2WU(sY`(2}klh(PH=R8yW|$rrrkaK@ZnD z;E1v?I(B}G-43oSi}r^PW{YmX0cBpa-QUJ|2fU-#WshU$@CM-RJ>F$tjR}*8bnygu zCvp$D+iVcsOQPZqp&u+;d-E`yp$wB|q?$$|_uz;!N1D>r)qBEE;z%Q}*G6sH>d%08 z^LmYv>&f=ei#Xy2y@J(qBbcHi=xNgi)jvDpBfeqbO_Fgam;WyYA@?sg0=W^$jX-V$ zawCu%f!qk>Mj$r=xe>^XKyCzbBk-R)0$Be~!+L!Z{b%&c=m*i)qt8VjjqZ-_j9wGH zG#|Eh3Nlx_5u4X#Qq;g27rkFUyS(w zMq~hF;{V9@A88M?d)V#lrgqd0TR&P~TJIy*|7mNFb%%9>b-8t(wb_bUtB~nG&zfe9 zwT`q7wK`iZt@;+@zwod5NBj-`9Dju0&2Q%0`31j<{U46#KO+Ak1Hk+S@&DJ50dP?K z-z+kxnWK>5-_>kkMor4TVIQ&A*)wbpyPaLnE@S5)yZ=PCf)%q_Yyum~`m)Zf1*^-# z$nF2!c*l6rc-+`+++u7u&NogoV#w?-G3FYRjgiIxBj3m~8W=3{bL1b9_am=Fo{HQb z*%i4ua#7^WNHVe-nf>!3QzN4zM?`u?+C`c~><9^e8~!-_26Fo!3f~dFKD;e_cKGD* z3E}c^QFvN-RCqwRYq&)iS$D|o|3H6Pe_X#?zd^rL-=Zh={@w;dVM_{ z`T=?UuZQ-A?hD-#`g`b{(8kajWcAMtO$rSS^$E2PH4d5DPul0&+sNpDNV_d7`al1V z*+Q$INVeQ-?u;7IweG9>;uC*oI;@`I!H2gm6T zino((2W4Fx97X?i-29;E`#}-*gJSAr^FfjM|1zeog1G3aT|p+mc?#*TiT``Uf)smlruXC?oC}a0|No=d{vYK6WXJsf zH^uq0g8RQ8#{d10e^zk+zl`W-hw+2pepW<3D~$j9vHPsx{{KH>_g?%Is{#C^gRHcL ztaJ#ZS*XV@I}=1?m;9UY|E%zSRw_VN{68xz;1A;eL8Lz`6(B3TpB>W=;{RDe|Nk=5 zpB2de{h)t#Oh1V9XNC8FKYIWBk^ZchepVp=_e1vC;r$?bpB3r1Rdn z{~%5u#PoxZJ<{@9cuybl@|!DIr(QD!qje5Z(2X`#&~}?BXxWVwUgMStEh*X zv~)exxQXMTMvZL`HEd*gs6j*Sq52I>583sZhp6AEgQ6FHWGXVP)wnlXT)BeT&%KiYg0ruJt*tgo(z*Dfr zPN6Qqa=Xx;hP45M>|Sm1YrIMG^R6I{!x^cSNp* zH{kThNyzkH5Sb2Nz`#g1tn#lD(clUAF#JmRiSRw)8^hbMI{)F&?V)Qz7luv?Z3wLh z6@{jSMui51x`tYWq9Lk%qkW*gtUa#Xt=*tqs%_B{+DdJaHcK0)4bpmOtyKoOL9e3M zX)5#FpqJCDG?jU7(9`MVn#w#k=tg?F7JP0o-KeR|bAuMp#hS`IH;`6epsCDrgAS%6 zHI-d%(8K9qO=XuGw14&Un#w0PXg}JYs(f;T_61k@KeaFsJ| z(4OEbXWUEu@#boRb=TC=1%r0SxhkJrupU%pkQ=mX^($26j2pBw?MhY7xIsJNT$Noe zSZAs-$PJnYu5!i&YeiMgxB;9;RnE9U8`DNq<%NUQG@>eJ+(1iXsARg zlW*wT>OqE^=xeG>9)tXYd`0*BZC{Xo&^LYdIr)OV;j_>15UOPv2KkJ9O4YIqgM3Op zqp$jBeo78dwWPry2gv92b-(QZ&QwbiMBAtI6~FDhnznSG&)y;L(dT{kHhG6W=d-tP zg_pC~e)@vX_LH~h(^>3I`l8R?ByZDav)Ehor7X6e?)BNentt?gpFKzR(Z_uDEP0MT z>a%Cav-A<4?Iq99hkb_1BD7Z)dy+okvnR<@^ua9lgx1MtPmsOzp)B^4re0KoJVBnM z5BP15)C{5b`0OF_2))~9d&oodE}uO}o}l+3v!30eYLyc9Z+*tv`Jm*3um$IbeGSt0Eb?e#jeoQG9`mt0Y~Bue%lq~N=+?H5^dY*wSL>SnuYXApIt(> z(d|CFm|Q}y@YzM=3VKZzyO3VxvkS>Z^s+2=0lnI17m$nT-?P|7^y4gcA>HP)b8E`! zMLs*5oJ%kC*;(XldV$Zjk_+giS!@fv*k@bFR;pGK7-S1MlV0Mtok`B3=V!64^vNu? zg{n0N23(wL=oY{26tbC~>9bAfQ>{lZ$R=_!-RifUOg7Qeve-s?md`elQ|K94Y!f{@ zi=n8FXGR+9&!lS6fw68qRf`Uc)$6EQTwttPP1WK8W92HU78e**E2&ysU{qGogy#h@ zD#%7UM_r&|1yw5+j1?91Bz1g6IaR9~jPgpVRwEeY6?BF=r+fw7pz!i?x?bUuZS(|% z7nM+D6B`Q`(Y5M$@j|*r;i6)?TH*Q2wRS!(q$es|IG?Wc@jR`K!t;vg@jjlfwf1o# ztx|Z-wX|H}*>h-_!n0=6QiW&Eqm@3ML02d|VZZH zU37uMM;}9r6rOZ6ov-l3>9oYh6KJu*6DHCEACIRC6&^o{7W#N1UF72l^f-lwjHh!I zK70s0R^k4K(^(21)}PK)xbIXTAbeO_Qn+DbdW6Ca8q&iR zuHS$TRJdM!IzZvN^=N;Eqjl+F3cFF-PhrQUeHFGHdZ@zOrkN!Q$c#5Qm5UKf&8VS0 z)EV!#q{9_{wIdy)@Jn6j(F#A?lTKB5*LXTl;cKVUWeRVbM^`C)W;u;1VN|mQvf^}~ zl|$UAg<3|C?!nstP+ zHhUfIfc^jIK$K!b{Qn=(kD~jdFGin=?tvA5OY~}F0Gt~=Et-TSpR50utN)j)|A*op zqO3u#{$FMokYACj|Cg)(m#hDW0&KbZf0ZaVtN&+a3IWzfes~7h^$jBfkOdIQ0MsK4X8KVXHBa{KiDMvjTcoqy) zg%b$?HNyUHt4$=)@1h5yZ$)2-K8}k2J7N1@9NmigeESnjUN4*-2czrcTx5K zDfa>QHb0WT9yR|LyK~)R+)?fkZZEfk+sv(lSpJXBm(KgntIpHT9_J3{2Iq3r``?Uc z{wimwGtZgkjCGE54n?*9mQH<#A)f!W{gM5K{ha-XeYbtHz1_aRKEqDh$J{5MD5zlM1J$Efu8wDo|s3-$dkuuij1vMQ~Gh~|$+#(!^A_HSZYyqbT--{&v$ zCs66{X8w0n_20x#;AM#APs^(5Zf2orvaNYHl@C=4x}Pd7L>pQ_a5{ zs`=G3!|b2zQ?{Qy#~wni|F!I5b|z~2tzspJ=ucw9km=uvHDfLd8Q&Y97;m5^;Dg4k z#?{7!#_7oOuQC=HvyBPH5Y+GQU^GQkzb5i^qaU2*i7DJ+?( z?@HNC$FQWV@X97nXGvM&l}(z=lCs7tLw($&Eb_{B;vQs`S2iA(kY!%kgk3Bt>%6k@ z6IfCfdS$!Dv!tx_%0@rLlcLnCrZMtbO+HJ?YOieMXqJ@aUfEG2SyI+}Wy2q0Nm=lf zjXa7aWyM!E0w>6luWZP0mXtML+0Y>@DT}_cAwyYGR()kh;^;8>MUNc8lCthA8+;^7 z4v|M68qAWi@+&(6w<$}%vH?f1q^$kQ`aQ{#viK`|vL8#z>aVQt0G5>HUs>4-mX!5h zS@*syDGR`|?$5KNtN_cN@6M941T5JGi@`GN=a7`uU|F5IMpBf6 z#R8f-Mlw?l)?_t2Dayf$y&9e*Pw=Fu2rI}FmTUupl&8XZEGg^58uRA!WS)1*d-MQLiW;%v{5R=)JSob=3i2v_lP5)` zSV5koukxfQ7AweJ`Xo<^da<%)FHee+v4T7$T13@Y*@F8Og<}PIRJ4fNv4ZTOkMg7_ zA1laxbPrF83bKOSMepNDQAJjeTj*UpS>GeK(OY=3o=0w_xAA0MkL;qi@?;&4?4-MR z5(|AY9qgbxdD8XBEp!J@IvznlmM3kGAhm}lEsr3xhbOs5kl4eMrbplv=Sk)fr1kKm z;Spr@@MOdzNb2Fquq3FGxSJ<+k8CGb@MOq44cR_C32$2FMv?5plhiv60b8CVUJDX` zcw&P`koUtA>pgS5BS;70i8UTUHV{v&_6U-J zc;a}EAm4{4R!Jfp@;tH9Z|T7kRUScl4^LEj1bIL_QQ;A!0rA8N|Fq6LQ7(ya_VYxU z*Mb}%o+$MRzQS^kAOnaemPvwqivv8d)N4Wh4^J#ngo2cK1i3&wvDo{}1>_>0Smd=J zFNh}=dM(Jw;E7_7AV-KL7J%RlY56Ws6nV#yZo(7uJ%Vf#o+$JPl1+GGo=1>t!V?7^ zL8=K)9On^an(&0s26&JO5>0qQ=mSBJXTlRgBM5?Y5uOk_0i*_D5uOlQL9`&rgeQbv zkf-4n6`Da1q=WE;&<%pTN#5cKp&bO-Pu}DSp&#UF`*}iW2tkl+!V^MA2!dP_o)B6> z5Tu&$gwPX$AY+6lgr)$gLCl6Hgsu=R$TQ&yp)KTT`&nW<2u7{TOFSX8g*bjD*~$|_ zUkLIPd6FlD#t;MvCOjc@h9Jl<;R&HN1VMTUPYAss2(n9fLTC;_kX*tOLU#y)91)%n z+5@Bpu_c}m`a`rJvxFyv29c-XqC$rVf*cT@5L!f#`^kemA@qnK50LwLLTD0s+5LdOV#%n_atT1F5gj_`!g zGlC#*geQci5d`TUJkd>(8ieS0LTDS&B6cxJ2z?_@!yO2fBM8zvctUBN3sJE#p|s9L zsN|UNv<{Y&E#wJL>ky=i@PyDhq61`$@Pwy#2$DW{Lg*c|h>#&q2+bo-L#hZ*c)ACt zp=(d~5F~Z*gr|E%-Ud&2x(6*=c*4^?1X&_H;prZNBoUtQbPqv}2v2yr2T$TzJmKk{ z8iX);!qYwI8qcVwdvF@M_H++YIdGb%d&owzi6=bWLrx|edBW2@BIAQ6Jl!J_MR>x~ zJp_3oJmKjcbZ`n!c)CY~RC&VFJ?ITxd%6dwoy-zK_uy4pkBPL%gvxLw)fGf%| zy_T?I1xpB>gQKh0v4qe#fU8!sgwQvDD_60E&^CbBTOuKJ4Pa#zO9)K^SYF8zLeBu6 ziGGEa0bEhR;zGxW6IQUe&@h0@%UN9L7r^<2EH1PQV97QX7rF&-Q3;C+%>uY^5sM4G z0*I|%;zFwc78SF&&?$iPidbA|6u@osSX}56z{2?~F0_d_0hbWE1aKx!5Sj#V#&i}J zdPE$Z!Qw)T0M5CV#f1(5oIQudg$4n{Ogk?02jKKsEH1PM;I-&M=nlXcGg(|{4silH z6M6%1B2ExmLmb5kLT3PupUC1uV*pOT=21dlh;t{fxTh_S*~Q{QTj1!?$FR826@ZhD zW^th@0LM>aaiJ#wG1HCLGA)B<2RenX)Y0Q(-s;zA_=9@>}1g+c&)wkL}VbpY7= zP!<=;01$h@#DyvV#9lCQp$Gt9>cZke4FFqIJXmv3*is=DozlhUmQihLhu7(V}-a7`+)5`u(%NVfS6Lpg~$hN+m6MB zzz4*Z9C0D;0kLC8TnKwWY^4wvq8<=?Da3`K2gDW%aUtdbo8x0b$OB>{gt!p#fDIe7 zxDfDwSXZF$As=azh<>Ynm3}@f#S`^1y-=T`kJJx?mDo&ou+Q69q4z>BW@Y$aAH5`c zW;7nHf^T4EbZqp9X!mGdv|dzqzjr@Ie*aVMZuds_Quj>M|F3Y1+^OzJx1Zb5ZQ`2F z&+rAji@g5FPzB&d?EH7Ov(Z@#KfoMR02u7_aPpkGj%I&jzi+u!koB$gf%TI0C|3Jl1H1oJ>jdltaGW*C8e;Xb+F+Hx&cEXy!sdUB z-^s5uL|%%@{zvnnyf@bNHwa$>EBc;r<8X&?-|z_Rc6UHKPJdlnuAQKrs{bp@ zB1WW8n-rReg%sbT0>CTgWJQ+|EHSeDYsEe}tj|UX>>z&5}YNN3JqXD(h`ba=UG5v=F z%1}T5p=`>XJpnaPPyfMyMo>`y0iRa?jEegA`;=-mrgL9FMcw>+14>Xhf45IFmGkcj zXf;ab-yP6jP&@ywfc}i)`F95NCsfb(bC4*kJk3#gM*pcRI3n!{rdBYQ{ZXhsirxc( z<3CU1=`O4+^66N5o7*R#f1^vBe+Kk2I?wq>K>O0Cv3H68*#Y!1cThkF(TCi_13Hx6 z?+)-OHcP1Ed=k*Q*bU<8fI8UL;iG`M*a70hfY!rC63_ay8f!k>i2-d(_hVmG|E@aH z*W58a#Rd+p^G-nj^hKrh@EY&+Pp-b39_760Q|$iWIB!6F93iX74y%69c|B+yLWeo8 z1@uTd#CbKF9_hT|)9MH5kdH+4NQC`hdPn zUU9Aq=u6~f=ca(Z2zpI6eaX2tpf8XYovQ=-Xw5+9%7ETgGsU?qpm&nHoNWQU1N4e) zdZ%+~KyN2^IF|?XHqeW+>Fv%X0lk&n=3EreUF25h!hr51yPUrVbO-49*>tCKK|pUI zJDk4-^k#C4b6!AiA~!qd2J|v=nR8Yat(oVX9nkYo4FAk5TC>Cv+2|BIt{65+#+}oH z#so<^rv)@l63*s;qFlanYCumSG3OMdQQ{wZT63~^M2e+2ntVnk5*XGXgpfW4Se;Q^-xuIRPC=9(A?{G@rcSTouq3*em3gfSTA+^Fi|NLkw#LKejYz@AA9 zg#C=)6%q2RNkD}>^JhmE5%R22&?@9vy?_dNRyUwRp4HE$uR3)CD&$!-phBKC2&j-} zPBwkXaRVyknH^9e&v-zEJc|TW$g^-jg*;=~^iD?)sE}u&fC_nLWYgOnEucc4`4a() z2zlmDcP-RD+GW=SCqtguzhu*$c6C4@&+MNA3VCM#6i~=B`^SJnp4mTS(VBVoKLZMR zW`CPSYnGtCnfKaTYfeVMQ<1q>*#Gd!lpF0Yd@^j9{YgOXM|m{$nL!WRANu6sK`@u>vZj zOGBTENwNKOKw%=-Zz;-Y`|5XWUsQA2mbSM~30hmzwze+iL zi|y5#WwEEUranUyNNeIVgn_iiK0_QxYveNog0zM{L-0qd?=wVyw0b^6_(!YjGsJ(i zIzB@HNQ?Rm@g%K57Tck@BL3e+oA*D7|G(qDFKn^UVo)R3QU)E>VTh{Yfzki2yopp(|#Y$SoTT86D z*3s5*?CjUsYHmd>4YvQs{B`~`e}M18&VCp0)A&iek}u+O_%Vq159WvR&b%eB&zboP zR`7pp?nl)BF>|-M!`zM?{?0Iy<{GmUHU4Ir6JX;XW_B}Mn~hA1Rin<|$LtNn{vTm? zV>SPFb^-SBOR_bv^^4d{RQemn4rAR|Ypm(F7%{#x4j6A4FTm!%*Vu_1fQyZ-#zw5{ zUtugXW@WMf`eJQ=GwkR`Bi}|oio6!ti&}p>BUeVwkDL)nMb<{ju)cp*WMX7^q<=-9$lRFm)@sgLavt9(+_hN# zPwpTOvyC+4t#XcE%Qq75_^sp)o)#`!ancTQD^Cljtt2~GTDonW<;U~1aNCOG+X;?K z$E{PeoTa7X)@ePMrKRK6Y1x{krQ_Ba{2)t9$F1|=V4fC^ThYgPWV@Lbu3Ic9MM#II zh3{50JVW-ewDjIOPD`E^-dl0}3Gxg}OaHCI9iA5cTXFnh@&rpu53a*FOG^)~V=$JM z9$Y74u(b5xI^hUQOAoH2hgn*BaGj8Dq=g69q)y04XB@buQ^V5Ie`~J>l>S@0>>8Gq z{#$!d8B0t5tzEQ;rKSJYo?FDy(tm5uSk2Of=#^4?=3JJZCm|fiY3aYUubIKp(tm4D z29*9=d-BmNo$=pNd+N7EicXvESzB&^qfrMpXrt#i}) z5@PS%bTki6nnhlv^?16y zM_wSW@^m|o>?SYpbX$+yj7)=c8;@L1ZszIM9=U>C&(nDxxfp@IbSsaXk2qbrrAN*o z=ks(6k8H)x=jrAiIRhcHbTf}^BxkVnAt01mu?;-k)H@z08_jeRaa^-DV8^m_W3S;P z?2evpBtNhLZ)HOX%Qmod0|}Slt*kHM(sNn5o_BO9OV^d~+$AhsN5X}8>7o)M(3Ey1 zTu{u?j)X-ESlX6wKHh6fLIj%9TtWnz(x!w3^H7UMLTrwnmZI9ij_7G2s__M}BYHX` ztEq);(9=>_Ti6FZoe|d5Ld+>GWHrv6Ig6#Gq_$?@eubpQ(Wx_8O2}zIh=Y`n(tr>L zDJi3^$;YsikkL2_8IY0^+Cm&DB_*_l?5>oQ(AKz#EF~qhHFg|JNeOL@y^W=Wghmfz z$Fh`=&w#g$HB%Y!47=^97)wd%Yz@R2LOSD&?gM#B$YwlbsM>NAO9|ykO9|PG&ahK;DkGe!g^jFJ zQaW3WaITcj7GA5Al+IS;rYt3;vxP0JQ&Kux^&9b&kj~;Z>(Q<(CFQe)-PBW3K3mvM zJtgI{l$6fg`i`Zfbmre#EG4Bgw_KKz z(wSrGlakVz8>W#G(isM>VWcv`+2l1wO2}rBQEH?zq8YWNSV~G}w%`&oB_y*3ySG^l zPYKbCOgq}L`U9R4!dZ~!v?Wgo@hnI)+MK6^fCj1lfHvbPA)-Y~Q8 zgwPh`VwPjCP(9;#?4z>^vm| zx*#u#&j^tYQj0MoTZ7j{ zh<8CAM>a}I2zZd1f#h+X5+Yu->_G-gN(gyD9;+F|Q$oxO@(6j1r-Yyv^bUVH{EQrxplHcCp0dvTqIcuI{>i;OC8a$hch^keDJkt4xr^M*Q&QS90_OxzNomh8|Gbl@gtQm;2~Px1HTB4? zDIxqNIh&`1_!s1C=%|!X0D^3(spKi40t69h9Vwv%1UUm~9Vwv( zKx!(<89XHvfoM4$86zp73M4s=r-U*PLL~?iBd79| zPzr*aOkzAG)Pf*sB#ESiVh|)n(mW+pgCI$g;whmV1cCFGCxvU%<|1-MoVaHt@xZ& z6KpL$C)ETiD`!ceCh)lxWh^PR1Y5P1C8d^NNGVAQErAn|8IepaP9QLvA#@Xz7T!~4Vc!y0}jyfA!= z#>1DR_I)zEGCU(Z+PU4i8g&Fuan?D@P(^SeRvq+24Z(U&$o>`;1YfcrweLXvzzghC zvCqJAy8yck47dB*9Z@^bvVO7tfgJ{3v7WN-$6AA{t&6NP;d@wZEyMl-Q?1d~Agng% zU>#z)@IHLc5Agl`dF(E57r&8Tj`aqopmtydU&v?kiF_FEhw6bXc|9J%ii0oAcTqp^ z3G-g~9Oolg;nVx8 zhq?m;dQbIHZvTMpsvhqi7SP+O$HH;sfA-euac=K`?yR2R_6+C+^jx=lK+mTaxcLFS zq`JcG7SN07C2rS%UPLc;yJXXgPs$L7`0lHqM}D zyUhc7Dm@kMH^24N>NRfDfNnyoA5XC9iFCc|2M`23(e-_{HceLl&Gmx-HcikZw)60N zo<^rTemKRZQ|V0S^PqJqo#q?}=oC8D@#7~p9ZRP;?+2}8=vc>(CfRf}9pm`%BAbq) zqa8m$WYeSQDCe!&$?D4mu;CU(@g0=>h$Ue(g>R=x6kEId@n8@w|Zt$rJa(+u;sANFs;%~_8I0?nuzgYii7Wm>64c98Rkz~ z!e^L3X>p%n2BoQLMVTp-c9P$QIh3}+XPBR9sVo-P*82<-IW3*VVw$QuhG~_i>X%_& zrL7f_6o`8Gt8hgRkjH;FHIFT!yHOeg~u?7(o|6~%%U_^oD9<_O%)8oJW5kV z#W0c5RN*lgsM>R0DJV>(G*z?=vnEZIj>2S0Q$@%yh0;`^GAwn|RN*nKb<elAO%)r%`ZY}z7{dZKO%)i!vNcT=6~kgR zO%)QuYBfz262lrbO%)HrA~j7F55p=oO%)HrGBvG_`W@SM)Osu2uD#Yv;Wq8Go(eZ? zqp7lDSgWRWSI6ts*YXvvTTknzaGknZW}kDs4XlnP_c=FVRcW2o8P9juRJk*(%hOcZ zH1^P7ZHPK&*LY2pcf)!`O_i9#I!A4~I_Iicnkq4eS-Pgm++lXFsWN()|7)t$9t;aj zmF#0%OEp#E54MoDM)jFqtBC}a%&QD5N;^Tl{lY5LPW2h=S51|W%-GhNs(6;M^)*%T zEaOzrcKV-#D??M|NHe|}O_e{*_;9qRe9^8oM7CFDKpP;`SrO3sXsrrpJp?VwvuS;2 zML_G~EXD3!4_v1ypTqlL~_S+x2x=ZApKslMC!GN3aLPWGOT z>G)?N{@+hK>EQVPuIRO>{dYEM_^*#vMi)otMvsY(LY9B8XoqMs*!yAkNB7G=+52w_ zri+KeSZfa>B^xhozC^nHs>rSjeY-?IR(x!&M4;yrgY39^{9sWIX zT;8ysu^&Vw|Eui_k;DkZWMf5f`my4l)p zU5IRew6)GEM>YT1SP3w~8eny|+M)u01DpH@{uzIVzr>&9_w!q^BH&VfHmU%uM^?aM zJ{MU5qxccL7w^EE@jA%(|Iz#sb^Tw(u77*XJIou*%gyu5&1MXi`BHNpcK#b@4mJB> z@4q~=A#C#+@^-VZa1zsE;G(GPBl)#9)L@X0%NK%#u$th0iBE%Mm@v8E`VP}K8(B`c{cKJ zfU}u5up|R5-x_n|#SO2^TnE<0fpAaDW3MXtSxCgms&&`60UDR zghl@kd+!}4Rh9MsRyb#$eNH918wxr%T{InNa*zy45Cu$#fQpK7Km-&-LC`Tx<*1H2 zjyZ>MbPQv_G&<&padbvU9kYlzj5E~lyK2=saPRLv_kHetpXc8B;|GjAW2p? z(9%|tjBXofCCTRY+Wqi($>a8V`#L~!xPig%b&~vTU;r;g?lxV3Uo&~z!0+*@4KAWl6-4mW}QxwYYkiuB+nXn z;)Oa%jx`WQZ<73KApGGZxz)f`t963BYTya0bb_2};K~zpf_!S=ij_K1ajEgTD|CWQ z>beV;>jazBb;knPq^?_btWK~=U3b*wI#DsHy>-agCfK8{gH4-YkGk&iWjetgbsZXO zB-o>_JM220V2`>E8QTPV)OE<%CfK8{L&i429(5fuwh8vA>uS4nf<5ZG>kih5mHgKC z1+qt7x8Qc2V2`?P{{=e19(5gZvkCU7>yVpGut!~Y`@TBC9(CQk2Xulx>bkk}bb>wV zx*52DJ?gr>=IR7{)OB-L#qpq7VQzw@4n|xq~ zP8`aJ4jYL>7|}~3aWLcfX*#ilaoBjBSj;$ft4=Ip#B#vIL5!ov=)^+CQKNO@Kt?PF zOe|o;a=^p^jKfFh#QuyZ0ZHt~hz1&oeHpue`!Ei9Lnr1lqL)Tu9%FZpPRy-d+pQCO zGrocEVlT#yFLYuKBkDd9dop4TU}82S^eZuoabUYn%w%jEs1q|7p2)2Gr{WX)x|;$X=o%!rnT_UV>G)u3uCq zScU8Q_R|Sg;X3sCO0Wvoq2Wh@Rk-epTAg4Ou8RO!h3illl3*3Cg90X4h3f(#onRHN z!;p|*6~774`Mjbm7jch_AP11pYi&duzIJ z6SDT!bm9Wm-kR=ConY;)8H}T>y)|#)t+V#lbPUxA*4~=8al^9q*0kd!ThsEbjHGRI)an|0N+I~9D+FSEgA021ytqHd4IBRbWZtgg1Zw+puIBRbW#`HLAZ_T$s z9cS&Wv4O0;HQLs3*4`Qn?{U&z+gpPw$%^#aHK>wg1Ax`Q`!3}hV^p}F`DIf)=;98V+#YuxXz|W8hb8v$b zCmrVCdM8dwOuucH*SR^m11sa~~%~rrpf{5XPu6$S+C=)(~-m1=s4?iWZ~gD&N>~LyHLkjrz3mf{$rhv%-GY3 zlTOo1Ut2y&$A=^EdLxr(IPqcC<5zjE*71s1dn02e>o}`*WCU&oR_n-cAggs`7?9OE zG8D*a9q9$KT1R?-tk#ikAggtx3&?66=>)P`M+O5~ts@;kR_jPxhmMn4<82PY(TZGq zBW;6pyoHa(aFq2r(iYQk*6T<+j0uBX6Ab zI?~vzj-Kqj()$1aH#zMj#Afdb@T6 zf0nG)VfqVYwGKOwCRXck=zAS!wGLxgjk8*ZF$IXTT8F=Pu>YUeV;COuTJtw%&OFOp zZ>}&8GY>Fln-eihY&WB(W9%@#HaZzCT^{u+4^)dKfKZi!q07a@Tzf@>m2qf@}V$n?mVNLQo@9)c16G5mS>@2DDh zGQ1^xTlgB(4P>!~V10N+_%P(PW+QJr4B66X*a__jeI5D$s|cP8JrcSrbYtjJtOh(6 z4#EkcBe9NPF7o)JL!EFCB5)9X2!0lPC-^dQ`S)Wb!PUVFkAZ;7oPm2@rm%H$;QXR6DJuT2~U`4 zd?-AAn(?X26O0dp$G%{^Cp>1X@viXbF~;A8M~yb#5gs|-c;Dqw#@oWfMjCIryw&)t z@Q^o*H-vkJ7_SR=_ZY7UcMUWC;&Qj~s&L0U##Z4$9mY$-u|dX*!tF8Rg(~A=$^Wx( zw7>tK!u=Zk^4;Ni$uHjvo`?MMec>_5FW(~`gZ%P4$8(ThzAro}`M;Le42Jyj-QiKl zFW(oQh5Ygz;bF-CS#={6RIPkk*|OF4)s+46&Ea{9mHvT9a>oD#T#$V!{!foxw9m1_` z#_cYbjk|@LT8-O;2W&TfFWi5CajS5&zwtZazER_5;o82&O~N%z#w{+_8aE1uYmDoK z@r_<5jBoT>VJB!@Bdi_cYGF$oS5?2!ewJ}%^&9P1Hm;B>w$~Up2*2BE{GrNlEse{B zalc$D>;#P8x?DCc7q%SZH^Qc6TwMKM`ekGniGw)gjcUI&JlY~yg@8T%SXyFAl4OnCYX<51yg(~U!f z@%V7C@Yt=!VqrWoEE2{e!$HD$WLPLXa+GnP@Q9Jd0^#8!j01#+jW?FKJlxn{7>^J8 z2;=c#zAzpi<_Y8RVPBWKjk&@dUl@A{%*%uRHqHuzpbup43#Us zs5Qn4V+h+@7(>{8!VkL|pDPLc(3$MvZW1w-I)Eu+bzuxZ4Q2{FWh4 z3UE&iM~)9_HqYq z(7^tI*_g?Xz)k=S=nP;uKRI7G?>lce&pCf`?m>0_HRukIN5kt4=ni<4v)GxB`uyFT zUT2`w7fan}M}YV2*X?KRN9?=N{qG9<0y~BI{5tgiJIr2S&#|Z2qwG$*$*!@L{#k#C z9)NG^7ci&4SKq3y))(olK1-jXSL$V`(x0oRV|Rca-HL92wzb3h#`@U$o3#~{`dh5q zF|WVGDp==Q8_^Z;XmkhI$C_b{N6mh_)!z!DKfw3uQ}s7g0X&MC{f+9k=nrs~I$8Zf z9isL@1;7~9iB%9Gu>xXma|*fw#Ng@J#=ng1 z#(T!A#*@bV#;xcGaJ~^YHX0{jkAMZnY-57aYqX*pfC*pVGymJzAK)=`BfQCfDOPKo z?O*R-4p(4b>(Gs`;6o9qZw+oOGyzDHr|tDN zASaYw&KR!MX?C?6LSZXSuD0!M2wCZhsckou?KFAXK5xDE#~CLZ9a38lO}r`!Hz|Ah74{xa}@>TaU;M|ua}%O+1!<& zJjY3s(amQba?)gV)6bmlJ=IB**-gQz-qW2l+1(UurZZ%C^O?;~nk;V$HqjY2z3FE* zIcYY%>#+qV&amlS@7+iNo8ENUX-=9=ZwfX#X*Rv|v(wpaCy&hpN1(qwznyPkm#?P)T;DVRxT z$ol3pGo3V<-xN$ikN7m%-<5z~nho%J?^OC3Ho)n!sZN>=@cJE(dZ##PHo)t>EflZ; zj$p@oPMQtydT$%ngr+M7IR2j&v_wy{1745K>2Zb~a5~fKq}c(l-|-%uVF$b(%O>%i zvIAa^Ws?ZV0mmD~N=XD{fm84jR!XMH1gGFtIzu)%pLx|ulMznAD|Cjea6a>jlO{8q zf|uzG+u`(TFFR?n!|BXcI>UxIo!RQ7$q>hx@^CssmN;GZ5SA9E$rPvHK?=wgr{Er} zT1t@>0Vhr7I0aki4B6v+W{Z<1gPel<=?q!qeCB>9O(r=7_t6=$$@$EE zPMVBz1m!t&hOBZra}xz*mQ!#UdT^)7E~nrMvWpk^!k-2>9oa1k6)?Ns)`rrw8?nE z@j7iVt^oS0_-mc^F=Cy6+RKOnlC-Ca7wXg*jHo3^oz8d!aB~%}(5X#~=q;T(jqy<6 z#wuQ;QyUnU0#B_TU8+;3FkW-0POWELc$ZF{T*aGpY8~T}g*vsidUT0Som9oeI&~uB zVqCC>aS?EJ^@2q@wTkg(96fQ7vpiFe(pS-;&Gz>uDLqJ<3#;T zAdM6F-e%6wsmeg%t)KINPSHq#qo^fG(Le!2ElG-o2_Uv;NzotyoH0wMXovu!mLx?3 z1Rb5JQ#3pPr$3=nG&lgKPS+_K8h{gUEe#C7$y0TTh6Nz{jihK$pbIAI6b%W$Cno3= z4G6$-KpGB!qsHkJ4F-fGb&7@pAQJW|8VG>HaFm7tVDB)UqCo&Sq*teC2mqp%Bt-)N z5Va&Jl7ApF`Y95BAliwfNcw?jCz2xJ2cj%4MY0bZ)ZwJ6qVIVLBAKd+zUQU#a3@t2 zeMr<_oK#ix5xk*OB>MRAhCw<-k`F}nNQwj>h+KP$Rx+5 zS*J+Sz25%y-{=$xI*{HK$vH5JcST|jM9E5uq#THnl@tj%5Iu8JB;!C7?WIV>f%FDR z!h!S#Nx*?&yg`z0AWBwJB;G(&iKIxnfv6Hmk#GZ1C6Xf92BJzNMWSs3trRJ?*Y1!1 zmgE}sj+@JNsv_3jsOQYH?G$OX*B3p*bEZy_RO6D<&(J9nYP$M#og$eAp1oP8NTh)q zfh5tubsKex1R8isMyE)gf$LAvDH3P8V7*R}Gy_oymm*;Xt^$%Q15YgJ6p1nrm2fGN zWFRWxQY6T9?P{GOIR;|MbBe?mxEx4Q3_Ko^MM6wRkJl-ZVIZkjMTEUkRKlfLgQH82 z)hX8C=uube6l-u4m2fH6;3z8LQmnyI^e#!U21n7mB*hvWMemXnYj6~ma4FVcn72B` z8XP?k$Qm3y_!gaF4UVFBNs2W%irytD*5D|59i>=;a zIEu}vlB~f|6woDEgJF>CBx`UKwRB0=;OOKhb&@qWI%%>_vIa*dPSQ!%;3)Jj$r>C* zzojH=aP-OXI>{Ow9SLL&j*fjnCs~7|sK!gO21ilRlw=K#qM|9u8XSc{CRu}{FO1Yl z*5D}iuu8H9M^S>8WDSl&L6fY(QB>n4S%agf#!IpWN1>oe*5D`Msbw2HwryVvi3%y zvPst7XrDTrWbKVYu9B?1QOH%2wKqyN#;m>3KoI$V&-eaqp2)XYGapBl_-gYg%oOLE z)6CIkr`d@9aemZ@e`dU6ylgyyx#I7QtBniMJ?;!+t#O=jh_SCR13Lu_!ECV(z2koV zZ)E>7;a|c(_yv~M?uRv^#&1BE^t<0Jvs><7B~Qo!9=VaXb(gK4)zZG8v6*miC%(#a_+{u zfyCKKoXz8Td84g46AjcFFyB z+s$wa{Q5ink^YN*2CD^rudl>ffpg&zEZ2vif8aDd61xTT(-u|-w3W3D?${p9+U-%}<0wVe{jvo7fOCKdQQk z4Q2B~x#Ep(^XKYvwTgRSzVAkG49s`k=tl3&=6kzDH<^F0ri#dpFhAINYR4k;A8rIc z!F=0|;1rl|x)Iy~^9?tGBVfMfMsNYlSKJ6Tfcdf;!D2A~vP-nte0`T_llkf{(P`$^ zU80TVOFN?-kDAZ85ljQ~h3YQ_W0di;@c)!-ko5X62K{H0Y`EVT%pCJMH-ZCXzPL-Y z)_i`K=p^%5H-e#KKIKL*am**&2+ofwD>N%^kNM;-=VqFZyAg~W^HDc~;bZ=JmuRZ_ zr=1b*v&Y;B{*C#d8^NlQ&t0O| z%)57qUN!IBC3?mD!!FUw=54z~Tg_W{M&;q=UTy>@$dnbf6)VWR**ypQ$Gp*vVELHW zyAf<2^IA88wPRlGMzA)`zwHt|WWKda^q_guF3|(#4ZB2J%}1; zk<5$TbLf6$%KGJsLu5*7zhV)YlG?9WNv5RsDLUIMRh1CVkeL_WaGF`^CXF5lzy63T%*}HB(rZjf$=4rxgel3v`a_qRlESY)^cquA`}Io}nv#02 zU%b#fQC?@!LUWDq!X>7p^XnHbHcxVoFEUrVe3vQd|N4bDo06ccpSRdNUapyYs3}QI zcs=F{_xN4rO5r&Vn8yn5ImbLkc=n#=(ZaK4n@0)HoNNBVlG=yUV@f(7 zR*xy^e0V*kr10VJn3BSW#bX{KuQOqWDG6ElJ*K4d;qsW0&WFuoN;)4tk16SV7(M1f zd7WV+O-bj&>M1&qofO>PLaQ^}34B>wLO-a?m#4#mR4;ROjR6T4QQ&RHqaZE|u!^knG z$m@h7=H%*AS$)`?RDCL|FPoC6hh=6;@d9i)bD8|)U31Ol!Z$B8B~f30&7tNga{P)T z%uT|m&^kl-*DK7kgfBSWJXaWN?xa;q#T7QC{Y%AUHl?LZ#Q`_36iFek-u#_JlwvUN zb0g$3%tzb^`6ct6>gOF$HYZd+FHi~)U{sk>fB>h;l%fMzRp!|0`-E3zN&y1QDpLv& zU{RSPQykT%$V@z(@n|v!|O5!%khny%{JjvGNx1`z^XEv z<@madW~Qnf|KU`b zwQ~H}hJW z9rFKQ{{P6(V(k34M`$<9;s;_cfI!HDy)iyT4gV|H4dB7x9q0paX|NQGS7-8v1osQh z!j1sLgM-kQFN~c4e++EHT>f=*=6fV?H+BWMB5*+<6*wcXF0dT4`32S9e4W@CpeCT4 zpPetA4=|s9!Fdcj1K#Rf?Of<&oHL!1o#ogaU_WOjvj06!Gr9oyF`xg${;U0h{U`e_ z`v&_G^yWJgng8QZ$v@woZjZJH+x68-{txsUn9V<^Z`apoSf5z=zXtmO9HjTsll5@j zj`_T{enu7lyVwih3GDy(JL?MTJnL8125Ti|^aoh8km(;{wV*qnfjt2KQ}X<$pyU5h z{>A?J$n@`q`u~Ccs6POA;5*-^zIS}D_?|+4#yhYA;8I`77l%J^qVHJTO7c(V*x+q~ z+m>U4w+Rajb8PT7AxV*AgSTnWV4Y)ww+YJwb8PT7!A#4s!P|rdHaRwUo3P3?#|Cc` zR=MWb;BA7LmScmr31(W34c;a!ZOyU4+k|B|IW~BkV8`Xy;B7*lAjbx8Q_TRKWrMc~ zhJTg~-X=I`SvGi^uv{+725(a!sIzSFHeoSMmJQw}82(u{c$=`IFUtmR6Bh1d$>6oU zO_r6d_`7yf+0K%^>qDR8i_1=y{9T`~(OdLh>}1K|rQj@Y(aDm>OF`cq-#S@xc_}#B zdzOjm@a$K zJJQLL7ffeHVDI)Uxxo|+-SLW(B|n%hd&N7{$&w>XXL_-BdzL(53c9_$PL^C@3c9@A zPL_OO3Ocd%dX}7F3I=;Soh*676!fRxHMzqS(4Oj9@`owthi%leMS%$vLI~TVgp`@{TFM4p>f>++zx`Ig*nl{}{oJ zZ@nQWOAa!fvAu(xEP2Qj1ikf6mRw{Cu+f#1B_EkDbDS(W$q07*;I*ABdC3T{n>F@X znNqz>mGS>fuAbT9RZezNHTaijhm)OH4X|CMlbuivu&bYw9bXNwr=OGE%?*BXvg4}3 zcOC>|B`61+>=+5qraL>j8vID_U{p2u!Skb&9mzr2#@_bX5!Ex_Q!u<5P}A@1ujwBrhEy*Ldl2+g&wNe4knU=*y^MROs~UXn+3sXJtHBqZ&z;xn>i>CLB&h9se0yBbQ;e#R)d$yqn&I+HK6L1Y`vU8^-6X?HK1+lv;C_9ZCjs> zR)goUZGE;M2j$V8=bdcd>Y3-zgFIVT4W96P;$&;9!87P5o~@|{PkWwmvVE$-li0&P zO9KhrV$>TvOG62Q@&xn-&(dH*XRg3r>{%L4D7YLYCRrL#D7X}RJY;D|q2LnKn`CKF zsr*cNxs#<~g@UWfCpcLeSSYxhUX6wp3NE8pqrrs&+U7k=!wUt!rI)4wh638zJxfCj z1+=q!mIfILE~Y;W8fGZC2&;WFG|(U@FZW#JWN4_NGw0#+WN5IV;6kh&&d_i}!3Ee6 zCqn}c1=RIBLqiV#%=u1+1|0{-8N!%E?s6AKJ3r$yCQ5+S%RCRK_1~qvy;s?M!9( z0iS)2lc|nAl@>4=8hyOJ#xv0ZCPRY{_zch4IzwZR*W0-H44t8o2YAjVJ453RuGoa# zz%w-L;4nJFWN6RI@AgbioliLqiGh z%ENVr1`^=GKpIAXdmXGZG>8DF?4>g_ga9W@(HR;*fG9l4(C`7oMmHH6Jb=iyW@zXD z4j-d4G;k0O(-|5zfFrl+3=JAU^ghYZkO3SvLT6?&BHNmw;ew71*BKftfZaGsLj_@% z&d@*sMB+6=!vqkC*9;93z^)#hp&HQBmj|)&Cq}V#1mwOh65nB?8wky07T2D3=IW9^m)qA zKmf$EVupqRV11*`&>#TB17e1T03ZrUGBf}H(dQ{c@()DAlMIPJ5alfyl73)Kt(76+ zw}DoMRNreimUV_CA7!xh_>d&{z`Ymg%pk_4xPZhSM-N`AGbHsu6e(m#=z)ut=nTm` zaKT)iA(01SE0qjMJP=!{WJutFC_KoJyaUm$Cqv>6#8xU9l6K(yeRPI|9XM~k&XBAF z=kBdDB4KgI#KvWxK zNVI{wjn^5HY~Z-vbcO^QI2L!h!8itg3MAGzItoWgs)4Zl4KyJH)#@NU~7v`lNY-2-E> zgJ3g!0u$>5wxM6(tIku-1J3Qvwa&$Oh+vmssk6T`%bDN|#TtQrj*T4zzp_6-ufXT9 zM&K^{M*C8`XrGJy16QI`-~xM=Jsy1mo9&40#omD*=-2gA*g5cb`f^>=XX}&EA8-lQ z1x$fUFi_WN)B3^s#Ci*z0UyGOfUB(wuv_2;>v(IawXd~1dIEM}Er6|lR@>A&>LsiM zxJzA+Zh&dE8T$k-Qw!8AwHx+BXjCEe0sP8*A3Fp-Vcu)rY+maBhyOLK?Z3}|i~lmz z@Sla1{l{Wvzqfxf`t`N>YyF1rd*8>X;C~i#`rCY0A>;omtm$9jJJh$2Z@O<(by5%O zsOof9=3`2d-IWQMk`z;A5~d`@RGEb-NrYFXVM-F(m3f$ww0LDArX;;xnJ%dp-2V^r zG9@YV%EU}b>bo*CQ<6ZZsIQU)dSz~=BrRTp{(Q1(Y z$m)$o52ndV5)O?X%#)R5z#Bc7C@aZ;Q}mOP40xjlb6_Po??w+M!AdgHjoA9?2PGNl zMr?QWlafp}Mc*sQhg0;OlDsw|_{x%1Z}ebZtRyi)(YH!+*Nq-bhLvQN8$Fl}E6H6q zdN2c4lDls7U<#}x&)n$2R9Q(5nxbt=^4g6aOo^4`pc_4y1}n)yH+nD+R#(cOwg(eo zB{}Fu+9gX#p1F~>$x@P$Zp4C*HVItt2Dei0!dnQj&vi z#1^10D#%zu^aNPyXJdAUlu=P;X9@m-?Jm88HblUwyG_Z()o>fBwTOO7K)6SpCwCFy~d9-3XIjwaJYz>s6<@ z5vI!O^j)I!)kZhM#8;i_Mwt1kQ``u%U3Ib>r92t6){QX5RVTR-row97E>S|ASWSA- z%vP;(gEP-mtKI9+>QtRj&4Pr`Rg&h$ zY*!sym1<0Em885eaaD)Q@zv)kNt|MCt0c{hxvM%WGiC@fDRj<5G zU0*dsxVBD7;ulj^C5hk08YPL}#X{;bGjQfu4@{*_wqMlik91~-DCreyn* zibJS!?m1YIN{W3N$pBSSYSc*Ps*-Z4#xg8jB}G?_W%$5KinSWaJXTUTRx!Ypl%`eu zboH40e`M(^DcP&!DAZrwb4YBcciafs5cQrLA%&toa3kbh)W>dwWR3c)`U`7VT2_*8 zhhv~5-ww+_waYb&mZ*Wk3+5`xx5G71t#W+-1C-?6;TtH)y~8+AO>)gX`>ICa`THoz ztHV1`l81+Rpd=3uUqf}s^?T1#l9Pv(pd=3u13^jd9S(w$oIET9C3$#w2ntzSn!@^E zA}Gnp!$nY%ONWi1BtH*-LiLmDr%qRrtA~@I0&;xfBqjNIcnM1K^Dq;X;C;CqR zuk-)2{~z@KUx?kKreQ0$ZtPfAhYJ3GqkrE=p}&S+!VUoUhi(g98~RNsj~f0Bn8_dY zf6D&{QOEy7@bloi*pKg-;KSGl;HKc^sN_!uPe&KOUj&y155Sy$a&Tm2CxBY?@%vZc ztH6hWw*oI>R)1gM_kn8y7o(d0Z0yZ&FFpEA{+=fXDd`yYs!{yn>ni zgU%h!4bG*g=%;ReYp_4xq1Xvvjxz-{{hdyeQ-dA)ezw2F?EXzu0X$~ki>m&s?ThRz z_UJnW^ZRA?LD&gkx;@tJv0LrFwvAo-zR@4+zv->|3B3i?0N3eDbOF`<8}({^G-mqy z=oxyv9*T;9{yM0=*7w%u=_eU)wQK;8ex=HJY% z@Bp@$x0~0QmzV|fTyrDVcpPmmG50ZNnB(E}w441+SZ~JnSmp6|bOd{_uMy86%7iqrr$62KoYQ^S|eR-T#dLLI3UO7kHsRg_Q&+`j7T6 z^3U~8M`wT@f2+SA>IJ<2+dcr7!(B+?rt|0m8`2$pzSIRaq&vRsqYG?EcOd&-U_-hC z>sJbFNOxelU4aeh4lK7Tup!-nUYP|pq&t+>1vaEROr;BKNOu^fF0dip;Ww-T8Pc}5 z!*3NTeze_DwhClN+ulK-iW_YYD(eE9(Xqat>jIn6vCsSJ0-MpXx^23^W^`;@on0U^ z+UJdV&M!L!a-)5|7;P%%6v&UJ08g=YfgEYNDt@+8AWND~oa>1@1u~^6pdC^RWJ^;( zJERtdR|DE1wLsQ1ouM643uI1HKuz`wy$HO%7#SRg9Y}dV`p!+ z3*=AZ;?17(b)n)=dt;|=)&;hxV;d8?z!r53{q_rNQOB?vy1*873~QtdY*EMhf1(R) zQOEjyrVDIQ$3E+)3v5xxj>C_!MIAfgVqIX1I(GaCy1*8746C6FY*ELs?zzAgb?g_* zb%8DF7&bsHutgoiuBQdIsAES0*`kgeeVHz>MIFOls0FsDV^~C3V2e6-*^#=y7Ikdt z)w;kIb?mUEy1*873@Zl<5&E5YV^}#@V2e7o6hFomb!G~SV2e5if4;yLbqxM|fi3D7{P_Y~)G;g!EU-l#n*(HvIyUbfU0{nk z1}DD27Ih2@0}E_X$FMLk&lYv;o;fKL5(JX_Q; zIPrP5sACUL*Lk+6W8;8qQO71ft@CVA$0knJdA6uyu;24+QO97v=h>o;!G6!PMID3v zo@a|X_VhTNXNx*E`Z=9n&A-0qM(aFV)UnaHfGz4+500+n3x>a@^K4Pah7Q;H6?_z) zdww}17DnfP!HDcq{y4_hdUXC+#&+N_jDz3S`J)+8Xp}#S5gDWWk=1KsI=_q&&Byac zFn-cs=MQIW`B3K%V{B^C`K63-;`4_xe%PS%hcGta6%MXqqs}j3jDD)~iy1$S>ii-` zGyu*YRJ{QI+d@Y8^Z5fAu}nF?fDwtN`~i&E_&vWrBTAL>`!PZ+^ZPQQR4Km?qZ739 z^MSTE=2-c8bkL5Kb$)K;bo*G~-c_8Y^Q^}0)5hvNt8x1jAggivWFV_?JMuDlR^#@u zlXRZdxP2myvKqHf0J0jlPX)3Xw@;a>^Q^}0lc(rBt8x2yTtI5<^|sSpR*_?GJKbfZ z$2dxNSw)b&?YrSxR^;}vxZzll+sERDV?}PqLeo4ea{JJcI!}s>SLnu_LxK$Kd0pp8 zj_D}w5fWqI>)kp}S`2Ilk`eZ7F!0kdB45ZtI6c{)bpRJ<5Kzufq z-}Y8q!1CLUYPCGeZ#$~h@)h~@wxe1t&+6Mg79XC~w;iiM^Q^w@Nay5PecO?Y%(MEo zW37Fj)wdm)ojj{=JKZ^~zU_49u==*sox|$ejud5{)wg{te!r}~?R{!>p4GP%iB;(%Uu=zkQb8HY|nBv-GxMDQupl zw+$KDJWFre{F8N_rMC_1`SUEjZCKBrXX$ODzZsU^Hu|_My>0YyS$f+-VLMOK>+`mG z&cS;p?X|saAuC^zUK_hQ_ZAAlv94l|@0K6D0Z)-iSW#w(98bwy#){h7194l{YKOie_E4C%bvGTU!Q|4HC zTk$D#ti0HvTjyALTk$D#th}vwqRO%IwxT(Bj+M6+^^7@I-d5YOa-_Vrx7D_C72&n9 zwNj3Cw`B;Bb+={EmpaF~+cFUKo2sG{2p0%TWaveS$A7%@p)NyTX4JPSa)0Ste9inZK2Q0x{I~II>)-(LZ6Ivw}n0# z>uw7wHFK=HEojY)-(j9he%b+?%+bXj+sF>vNscblPvIo91~G{(=d?lw2$8)n^Y zejl&Py4y^}x~#j+^f$q}+uZP>&av(`V+_r)?lw2z$5?lpsaThFx0(KESa+Li@U^h+ zHb=MX9P4f~NZUQvg$TXnyPcGx=oX&=p3o8*V{A-7gS`| z+cXTf3+XP74#VA05ngW--6O2LO?3BGl-JwTGnD%O_wjA_V4t{)(J5{-=7!761?DVs zH?te7|3gOE_{w-6v%)8gd(rv-QX_AiiIxAy7>kU(FdrO>b^kSnKk{|tACcE0Pe$&G z+!DDgQiz-tSr<7L9sl=+YcMP_FcOUf!sYNc;g7;^g^BYXyqv6}TaILU37dVQ??(88|Z7ft3RPo$UX0&L!v( zc(!wjbG&o7vjBSqPI87jF{i&1u*>#0_DA+x@CP2X|7hQ0Ux^NZNqe(>qJ50L1nUHL zhdaQl{!RUp-lpHxuc89rfvPu?4zyJ6Ct1*L5`_J&N^&jUy z1ik%cpyGcBX7F`>%lETyyYGG9>)6NdA=LX{@4E!u{m%7m@SWgW=3D673w{1pi)j_B zsk63L4W?M18tWzDa3AYM;b7Q$K^PCH&sP^5;R*G*>Vl(K*?Ly4*jA^-S*N+TIN~(H z6gxBaY1Dc{uK%RJ^>^V98?3(yzuj)VEBsoI^^x#%qpj`2Pmi;{5`K8P^^@>Db1bpz zXuc*EAk7rThNKy$SeP_P6)Th`&0^DH;;p-@uZgE@>nY)>W34|4Pnl{x;__JQN#V&; ztcP75Yds-6X{`0Q@Wjd1gD#J?9uuB0(R#q;vDTx)<0n{dc>tB(tG|oYOy&0fQ{I7{ z+5ZE}JKS$?to4xaZeuMe?Pwo6&3amnkDY4$&gCiAEiO;Cu6KEo^=FqST9>*!!MfPx z@z#AV?`GZWa@l%Dc;tBN9^s)Qtv?F)yl&kk+&$E~+vOhXPT`KXt=olT9oB8a?cLTN zT#i}47jA90ZWeB7wQdq_Zn16@Zfdq}5N>F)t`n|ru&xyzP;Xr$+<$;|wQ#h*b(L_x zsCA`q-+tB=!gYPE%Y|#}tjmP^)LOq4j`XoE5k^z9-v~nkFA@#}tqX-QmHKs64>6az zpsI(MOr0-RK*w$se!bheqk1)FS?5)+29_w3hBo(=8O&MLv>MH{tU~o?F;TLT!sF&! zMd6-tR$jQH$I1y0?69)J^#iSpaJ1e^3-^s$DdF0_RzesBv2kIH3cnJ@Z|7WL{C3U} zMoZYUtDg(=t~0BjtF3JPvU&w(Ue+1Htx66jS!;#y2~QHnCp=LYpKy&ZKH+L%e8N@2fsl2AFuwnl z!kB>_U;Qm$3bvy9TWBp?%jJrX2UsUp851(=7s7+Sw2l)VILJEIaW;kJR+(ZWq_ z)=|RvOF2?_K!deRxORYbm~hPy>j;89Fh1=PVSL)f!uYg{ zgfT5UsQSq9zI{?ipq6>GBY3 zcj3YBTT_Gw4Yno;#|Bvwh1+A+1mU)JYrJr4k2THZ7HhIFG;yrU&DK=m<~D0Lms_pj zF1J`?gd3WzkuHB|Nj(B4W7Y^cUjLDGfN*rXHB=Z{*((gK93l)g=n<}|w}!b~Yjq2U zYphOT{Cx}-4u-4_VLUGm64s6t6UKC`y?V!BJ~ps=$26C%Ho0PZjnyUmQN6W57|)2K zgx_zr#tGvYak}vHqpUeq#>~!Y6`nrHY8IY0$!ZjyI?1XRo-)ZAAUtVztHtH%R+G!q ztOl2-TK!#~VntmpTQ$O?CRzQ2hmErOx;)g9XHZP}tXetVGt}xM+}&eEggd*fu<+nc zDc!hLE~SvcHB?GO%z)xU)ULG>?Tyy2gPZAbl6Slj9+VN0tYt9MV6rGBX1Jxyix zeRUkAg}UlH_u&{Tb=9|Sgr&Ob8#lu8T=kV3VSTRp(v7e{S8aDAtk6|oxDl4tB zHM;6^H^RDH^{E?SVXpebjj$M3edI=1jjKL%Bdowx|8OHL!By|O5tiMmcOBnX-df*i zPh@Fi|H!P!gvikU4`%DHBeVYyR`y?y&i?;bwfg;ofj~L%P2eMB^@4yu7&Nnbnhf4h)oKKv;Vm<#O$mU<;T;L>-%U|Uza}LBjeIly# z+nhS&@qe;E$G&`9?LXW1ppXA$sL?+MIs6s&QhPsp26pA^Mi2jp?a^Q9_w>u?-+vc! z_ZMPEz72W>`tr>~h5iW4&73Z3$kF%ly{EibRewa}50Qr=-}v_R zE%%*`we)}V-Guq|df%auzeJvmeCQkPKOu6he*!x6&5oRi8F{VmN52`lD3Xqx?wf9Y zjVk|F%s-oV`(E}v1{dcdGg-~>FE;0y)6Fqvm)UIAn#%a6@dY~mzlQw&gT^0>>x|$0 z->v5Gf9QYH|GfWC{=3oj|1y8c|11ASFN$!m4gh}2^Z-!rSgEu?FFS|kOd*Q8a@Lp^9 zrJe5~>&b>++$Aiph1(?Wu)G#$pN}l}Sa{3s3#>jV{Fxi9Ssnh=4OXuSf8qwKR);@! zgY#B}KXQY`H-S%7z3*|MN8nirQbu@Hnd2H)wXw~wl*3rdU~tabGHM$5w&m4nCw7ZriXLsLhnPRoNERfNccSV!m^Ee~;26Cw{{ zR1_i)VpJ6(4`NgnA`f3w7b3$v3Pbd?>M+pZ*Hf#*Ku1~2;~5p7$Ri(Bs>nk@2TUq? zyzF?ZL(7A2$K#Xqk=0j3HA$rr66R&VN$!1#Cisc)vpd7Qrx1Q-XNVnr!%y!Fvz}b| zshwd4_wMOn^3D+NIpqFw0v^2Q(8Qf* z@t#8yb_vTHLVL)cRbbt!&@wkzyDoHu8=SN@bhsO=SshyH2CGjB9kw%A6FSrlR;>;l zvNJd@bfgWwh(_?BtTw8Y*6Fd;$Jt74 zogRaiA=isa@box0h@Y)TyVs%APLFaU)Y|EhZiHexJ;IGpZKsF35z6iKFgHTIogPa6 z0iJ#;1$Q#(rHZ>Da)OHMWX?(TcQON}@;aH_QhlAwZm9xKX24WoC$n2Bo0EAX)y>JA zlM3f#4oQ`BGXJE~IhjvV^_|RDsg6$OrBpH}b4#k3lldf7)ye#m3g~2RNwsn^|D>`w znKx45oXjn$v`*%nR3s;JNUD;PIV6?I$sCgEg>o{7q)IuNLsH$H%uA_kPUe$T zAt&=jDv^_UBh|>sypam(WS&U{b25iSZJZ@@MHI(bGFL=(oF#Kbl+sx;w?wI&CG$p9 z#aTa8AAV34XMHb>9LsmY$g6xSyxVx|8{u)&tsh<9&H7q+^f>Dam!Gq~6o%XMx$w~8 z)@Qg$KWFeISgv{Xc}^FugC_-eY~}a?Fwk)L6@h);n^% zsl|F*7<2f)2{*J`Z@JuPy(!$k!Ft{0PjtI5-1XO7{?z)5Fx;b8UH(iD6o!-Zip!tt ziNbK;Uv_z$7FX;)Fbj!^ldjB~szMiBbzXQobb_Z+>sE!oaD%n$!rR>7q_yGC-C+Lw zF!dqlpSfdx_#HR6bAI@3H<-00{I{LK{o%jv3}%Jja)bM4h2Nz5|B=2`|8@QUtjNU3 z@W`M@eFPg-hkp!j3%?hB9gnS#gzrW!|BCPh;S~D!uMZy|J|g_zssI14`u`p3*8dL< zHU|3yP4x2L9{5M#FM;Rb6x@SV{Z|Dp3}galqMQHmfg=J7;T22^jKRA87Ptl4`8WFc ze+0kaCFgNxi}MHPdgr%J5gq-h{{I-~U}s-+0G!|qLw0|F6GBh_AK)9jYrkecV?T`5 z{WsZ{+vg*{zX|#MW9>uW9n7*P+QaQZ_5eF%`}7a`b7c5mLxz6~I{ROxFMxw^nqGz7 z{|-cce+zO=f5Ai`;S9zzsZVNKJ}gY zSiPm5SC6PWvC{t%l|yEKojOh(tmdQEf3zB`>e1ofH2-OCH~)dm{`2Oe<~`{0f0cQm znL$T~_2%*B5#~bd{5P%Q1T?`3(8j-wuZ@q4zZx$Yk7GxN+l*`B1>}u$j19&r<0xaX zG2hq&dph(Q1C9Ph$ng7r@_+7s7ySdC@jr}R18(wP?myq3^l!qx0Z03n`1kSeUaj`8 zMXkU;ecOER`Cj!sjaq>_eK-0p^PPvi15U#&hJQ|p3gCRcUhKv8wo{@KI0|0IHbW&Y zg6mzdQkS?0u6LgWy2M3rz5C49B`$*NMdOkZ7s2(;!BH-P>&4zEB`$*Now=t|q9QnY z<$FCZ>k^m2_3k#)DNz|59ly(SuY*mzIk?4hmo9NBTyN)YPKiq4=%Q;qx9AcV!}Sj8 z)Fmp0^Ll$@gX|Jj!}+|u9(3<3Q9T@8{u|G=y2KT6z0FO!#1(P9%`si#in!jUW~W3I zadb5`g(^`^9D?$3o{M#ftKxd=@uOT7*NgrgC9aC=#g0lPu8QkLZBvP>;(9TcE^$>{ zFN%UoTou=gQn(UV#r2}}uf$bxz2S&cqN+Ih&(HE)Y?Y`k&i3|(tx}~d&hAClkgDQr zFEU9bDvI;kDDo>+isHOI6M{4>qaaCLoYPm{W71slEpu|;iJxCsus4C9( z_E=V_QWR(RlPmDit9$1Zi%bny8A?RiL2te`$Tk!tKz!rzS1SGit9$1Zi%bnx>2%L;;Ojr z2)WY8IRxk%_ncj#N;#jm%kwzi z4VB8#RgcrVp<+1-oW1E@+^ zf--a{#6fva&wWlQSUqzu1%YaCZTS$aKa&8PE0%0GxX~$TH@LwmS=Hbs&$UiTRf8KT zFss206d2XudJ6p2;9Af1PRYkX`4Bqet)3}iKgE)#8eD`k#WURCLZ^6oHMkJH>WZ7I zK>=Ge7B@+NB~HcDs=@hqwIZv07h2jOV3ns^=RBuKDo?++f~VvZN$2U;yM`_!rKdA# zx{S1*f^+cA6-n(Y0lv8+={*JKdeTmj6rV3U*D12*ca=9`5?5r+?^=7CE|TVZy$z7KS?Rl`%+p0y z`YtRaD6-Ob4IitEtn^*IZ|Nc{eOK=*y2wi3^-8ZUveI{T4A(_cdi;Mde~YB^z`?kn zBJ?;qSQkm@aTH!sgAd^qu3M(M4AJ&T->)k(Is^HP}T`dat+h z8T?8sQt#~?2CQg3aH}qo)Z>B?!*r3AzH`_JU1X*2M8muyD}5(wu#2qpooJ0$WTo%y zen%Hs={q~Rb&-|6lfDU7`cC?tv(k4C#E-GkcedjwD}5(ggB4lnJF$&ek(Iu)|5v)m zO5fSHzb>-UclO1vl$E}de$lM-oe}&PD}5*S11qx9chYZ{lpdcABS^6#_1;d56-Cl| z9K~2sB&7#ptSFMs12I+EMi;Dq?HNxp$}H;{M(N8l()H*g@nY!Yr@H-3*K z+jJD)C5bj+moAcI13Pg62{s+=L`yHmE*vGXrVDT_Ni}dVj*?K*wfGApnFbERQ4(oj z3`i197Yx!x5@^DhE|NS0TRU}u#2JWs`T|Ka5LNaC5@sMu>kA~yz`i(2qD+Xzt0c+5 z1|SJC5Nj?9B*%pPb%Defh?PYJl42m9ISVAjK>RruNQQy5{FOu)h;sJ=NigC2xrcy1?=~*be9d%kN<1Pzxl#=s2vcLPdS;!FVYaUrKNlD&p(yKw*4=wYLL> z@dei24iv^0j;s7%9jJycu=aMKM!vw>ODVMiYj4M79A)jL0I6z{*RhwgM|JWzGt$yp(h+ zu<}w4t-#7l>9+zaZwIn(1y3aq@yxl#YWpZynljDCjg|F{1u{|EjzcCP6*9>DDIYU9^N(%59I zHjXqF8go$fKg<}2O!IHdtocjy3_KQV$>(8yIm+xX2bck42UZOH!+70z+HSPN=z8}p z_PTphKc^qocOa*JF?u(ip-2BG5JvHop+X}xc~W<6zXv3_q|g>~== z?02`yT4o(+?P*Q4hFWb_9oE4Aq&`RQyRF#q?jCisx=a<-IavR`0=4e@sTpb{y4Sp?ZZ zRsrc|a~n~|!F{Q{+7)*}QNdlu9UXTOSKM{fany0fWyZC>_he*5wa(o8-RJr0_j|s3 zFZ{#%W@JTGM@F1CBjUt4*H|g*57s7Yg*6*q|3j_5R)y8xV(|I@0x|wyG9NSVgvWo* zJkLB8UjFmVDdtGCpW&~A;|01flC{nIA(4_Akt~Qz~%szd&+ViO}d-vgLH>kp_>&?{`RE61NA6Gj_ z6=ro0uC|~m%&KaxHkvBT%4)85sz9o^+MKE|%RlBj^e^aE&Tm!3>eE-W)1-GF(N>eX z-lB~rJ@$!~npF1?Ei|dB7BNjKt3|6KsS>Ux~ylb*fAzclI5Q~a|?_K9ybK~sIB37YC_P0&=|6-kx&hbCyMuQWkZsfF)~Fq%p& zd{>0gRBGY7B8;X|3*QxCG}YgA1!$@tG}%4GQ65PZp%J^qwW=gEY=pQ*lcB@JR!xQs z6<2G5M$|nqghtdoF@#3cJu!qv)SWMczN7nF2o0i#Xb26WhiC{5qWfs5vP$Uj8bX8U z#~4C`=#dpdgXobJLW5kde<&K{I!(|Zx>JPEAbRwN(0BBZ38F!it3yQ)4WhgoDuQSb z<=s#bM1#y!RR_@^Cuo8OS*ZycWQ8VZkmZ`7L6&HO20306G{|C2&>%~Tq)IH(1P!A6 zD=LC$5aq{F5k!M1KaPqZ8btYVR0Po=%5|e6hz41xYeR!*!xcn>tWqTw8YH3#8bn)K z3k?#|C1{YKCTI|~Axbm4yL|IVQ@T49#_a75g)ynysc|>^c7-v4+o3RKaJOpQ+P+0$ zyuEE#7&Et<6~=^ao5n5dn-s?6?FNN0d%Ip?Oy90k81uJl6~+Ya8ig@~+o~|8a91k~ zFQ}`0pQS3xzS8%C!Yu9zbq8jTH!6%7>TL?+Rd|=em=WKjFs9ndNue?le-|m7q^eX# zYU9oXO=@b9rdgHrs&U3?(zBN{R+D#oI!7xKMs4aDR)$fVdWMx@)TW+cWf-+t`ZK7_ z)C9E!6bZ%O70Q94GE^HEO4lkwHMK(NT4e}!Q|Ve|2zFEHT4m_no`#g=S>~l1(w12O0cl!*D%k9kyV{U)C!kFCu zPGL;xPt~|!H)#B!y;fn&>Q7P_)A~&sx3@PcjEVgQg)y^VuQ1jLPSLony-wrL?UNP8 zl>S78F{fXnFede@6-LghRle1zwCxjotATm_N_7WT4jL84n#E}fW5p(^Fcy7seiy5V zj2MBG?f&~`WW;W#pC-dbIEQF5beMCnCPRigeKqN~2PeQ)1qb(Y#%R*_U__BqC4Kuk zqcrK$7y0E>N$)<+2uwxw_iFJ{ zq|yAld4Awe#`Z`o^lk9DfNjVGQM1#?403r=Wcig?f91R->l%~!vl3w8y2r^ zs9V0OVbRKlx_D#Xx(%z>u4r7lcyUABNsHI6u3fXKap~$+b&Y*%7cDyc(0&UJ=~LIR zcA-}`7av`}cx|2k-$Wqonf6@FkjnvZf8#b+4xMm5yqY8gRk5Ya|cPU6R_Dt!iAnWbJ%v|AxAT#`$YkE^ch3ujn>ExOrsh zzhZ8&iMmb+ZZ1tU=ZL}w=ca-Q;mY!+m+0w>EG?uzPLED+Llb>fBh9Oo^zKi!r#zlW^i-A zl5`&Z1&i0N_3TdTmYu2n>w$SU8bJf?h5@p7nCHa~LIcnD~Ub3vAaqT9w zPRkj=&3#K*xBAg7N@fr|Q(U)riG?@42@)KTzKBzU&^YNQ?1d- z!Oc~rsa3tYIwc$Vey>>GfFZMH{#x(FRZI_Vt}o3GmVJ(DM?>(^Sy@?cGb_tVZ)N>= zJG*3~MB5O&IifitC3}dv;${sJE?I-fF}PsevXzS#(;HW)WJXoi+{_{cOI&rc_OOl# zZthi*PVK4K>a{eqO}sMqVx^e>Hv6Tt|KB0J#)w@T8|=R6u8(|ytn~5lpWF`7J8|^y zE1b{ojmY|q;z0f|c9~nTU2F!T++P=7V^1pO+?JO~a;ztGzYi+LEp7(F4ZM$M3EwX{TxS^3$tiudV*f zrnpK~>e)xL_tusrdzJrhosUfoZmuoKE3y97=o-|8KT{O14JlTPXQovu(JNE#UiKK&zv_-XbX3N~ZdT ze_vcLxz_!6tWr1MZT&A3qGp?wrEVmxssHV}iKVj_a!fa~7YE?{ztPj!X2iC|rnx`j z9RH~3r^wBBi1UWC*(t}&|0L0cKgyT!D7&5=ZGUF}!R`@xA<__W5o>;W=o@5|iwB<# zZVa}wc3M--uiz(M6L>1HG!Q|hrTsUvXVBPucq!ID)W|CC|Ih7#x`&sr?%x`oIo0@r zp(Xr+-x%fvRjB{S64tM~dvQq3uEci?F5!3l+W0T5K=ntLuzLL`G#jxCyZ@JcNpW-g zxN>~Sz*77rdaQ_JpyK}j!UIuxDNPW*gPcd^s;zEX$DerszNC~o)@)eKXkV&! z*RA94{~y;5HJ8?=sYXGY%x{a@OVTCv@BZ!n|J*kCijqkDufIa?{~y~LU(m0lJyXp_ z7q>#y6G~e3f7<__*%Dt#U(Kaa){Nk+ck2EW?>pT<#peMx@sulE0EGSpkj zzTDi(+NirE>ZVPJzyCiyf|^S*N_5R~|Npo!DlW-G)D<^xkWi@_M2^AY{{OK-)LfDQ zC~I!Ej(V(B+Nzti#}pG)_bNrFZnpnFB~YqLoBdLn|3{*0jo1ybDLDVW#qAw^26X?w z{VlM+1@^bV{ubEZ0{dHFe+%qyf&DG8zXkTU!2TBa|J(x8>oJWh$rS%LE^rHbqvDb- zUjGLdxM_9xlF}^K{K^GxaUAuQWQEh;lhB)gzjA?FP>ZiA$@a=`zozH{H@ODiQIf5# zq6^&gUZ}HVo#xl`|3KVaVIW4{r?IzVdt$p|4@ z{^Gpjyo}U34Gm=TDN5O>O*1DvR1ihqjF#ruey_l$U0 z+$nBGrof~)N1P@a#8NR^Oh)v)0iutn76*w|BEo;-f9D_bef&j4(7TWC;9GfypU*e* z^?U`Ni=2T+@&UY#cjIk2W52M!vya&u?0NPWV(8t%wz3R6pKWIA*$Os~O=U;1!HA;Q zgLP)DSj7GbxdT78_t`HZd*FTcZT59`-oD5_3mF7g*$WZvaGX8N?q}E7U6Dml*hb{r z$Y;m_uon>z{}{PFazo_u$iR@7B1c4eM=By6kOv@$sE1#MKMcPfelGkl zvI*XZOaK>$&kU~*uL#cxPYRC;AC8QI<>3PmC(jK1GxT}r{m`qSXF?B$?hM@=x-yhR zPQcR;sc}hYW@sF80`?76h7Jrl!2+7EP0csC_pVp;E}*nJVhaDspNSBB^0EVN}dWl!c!EcmP(#6p64kF zR7>P(;~Ac!P_=a1lXM#et0nSC;7OjMaJ599pb`pLD@&f>DGFIj6xNo=jmCDKq`xwm0S?G zoF^&3Es-sO3wV-3+|q4ZjHh{$g4_~0&q(nkg}Ehi4#JKnDbTIF?Hrz@P`4n3jm8$9 zq+quoXpnn&lEU2*Ss&QKk`(YZVAiiUw%AE8mXauCkLd z=577bC3aHAyscl6vy(FBZ9Q^6C1uRpdZc_x%9ywHOP1M58S}P2hmXmaxAn;Pl$0@V z>*wLBjCor>Z?2t`F>mV^U2P|2%-i|}i|nL~d0Rh!ft{2wZ|mpIx05pFZT;2wn2dQ_ zKX)EaQp{WW^JWEbT}HjFpD~vwDe5ghQ%TCG|xb+D0l$0@W>np45q>O=Ej|k{V83VVzyuwZ%i8=y-dSsWEQxl9T{&B(9PU(AANVWH}(i$0A9RfB@@VU`Y~yfLV8%ahaVY`yU9@Z9L6RlJy5% zx6w|L?FU@D&Q6l$2VA|@PLkaRT(;UylGO)Xw2URm=A*WG#tu74CLfpQV{DMY2b_li zLFOLts(E&jj6L9NKr;1!$IZ5rWat5B9cL%W%mYrJWhcqV10FryPLhcS9CNgtBm)mP zbc~%O^G8FgHJ7#|~(4hU6EN`qbp zvpva9l0nDy!-w#sH0O23-NyZPlFT`-A96TLdd56ZXY4>9vvAoo>tKEF=1FPG>x|pT z;7VJLyNug-Qrhym!Xz|Rf^0deLZ*TQS#rQ$`&h!WnQTC09Y~PD21I{Okhum#irEAiYe2`f6P~FK)S>4lq@}LoB9b6W z%>s2ilJE>Qt1H+EX{X~GF0d2QPRAQ?Rodxz;|4n+?R31M(N0J^9lxN#PDnc)Ujn$$ z``Y-NC3ZsE>G+(jc0$_e_`ErGLfYy0oOyOa+UfXQT$Oe@K4-3-kajx06<4L5j?V;? zb~-+Fr=1`>9SFpcydoj3bbJz?7FlUr-7(2dkc|c$H`7i?3mqSit7M_+>UcXr_8D;J zI6EP&bG$zwS!Z0W{oGEFZ3gUFYbT^-j`zgVB+HDeJ$l*+X_w<&a8=snc=s-LLfYkc zm+p2#+U0ncayudIa=aUEkajs<4k+z%940y;?Q*;;u1dQc@6ydqNV^>G($!9oUB(x; z>2D{@gq=U4pDJAoUT? z90%gmN5~%IDk5$sq&1Esvrs}><9Nij6Ve*T!;wgWtT77!COl)z;srY)ZE@`sKxvC> zr`=*Fq%E#JW}2Olwzw9#5);xE*WNP4PDoo^I~Gvd;##B}Nl05KO>eQcnekjA*S_v>~-8spm9-gZJ7 zK5oshP;wi~*Z zw8gbuaD%kPwbWgtEv|hZok-f^+D?DA6Vevf{<))_khZwCBW{qkxVB>_J0Wdx?E$z! z+TvQoRZK`*T#Fn}32BRKkqRXtZEu# z_a!3ZzsMg!R=;Z?f0x=nMsAFJjd}Om_V4YD_6qxX`ytHB2ibMDi9CEQBOgU}M_!5C z9mz$`jjTkrf!UGq$lrIheO&P7P;2Yqpa?DwejB_zcyaK|;K#^l@D%37qk|oShXs3B zKUwcsUsx|7g8o+PEX;`4S;vQh!Fz*^=K1Egp*f*@%+cm+=I_k4=3?_H^Ipt{4>Nm& zZVEkVcEH>`A37y;Ug(6-1kA>}h3ZWa-eG=hHks4HQ<2f&jBrDE5oY!M!dLMv{8YY{ zACJt0#&Pa*r)9_M&X zjb{q|oW(_YVPT!Ks7NVc^ui*gl+X(_Ev!RI==qw`l-il6X`!F-v@=%+-+#_{fpspj zLVs4-jP+&RHACVt)WcIZW2X}@QoN2iwVL7`>^7%HQ@o$u?er?rZN?6#XOV66 zXvfuI^p1myl~C{Kq^Z|CIubX~>FI1LQhGWYHT9m(22H)Ev%W~_>8vYKdOD3oN>8VuNa=B`)zo`fCu(YJ zMf(vv-aL>_I5Wk~s< zzQD#>%T>4WW4(v$v^Fs9V#2ZD5aV}mjQuHM8T9KkBiC2phJxRP$r07Xvucp+K#LJpe zPZE2I6g^42RHW!h;>99GPZBR^N?k`huPKdl@m!Ij>xgHI6ysbxQ=}N@;^`vAI2V7? zl%BhIN>fq+@uV8sp(#7WZcQdn5l?8cW3qT$e~MHA9Ak$l9Bawy zHEzM`6n0{)R$<|=8il!Fy%c7g^;9^*SP#D)kao1%Z-*WQR;BLvdyHuT40`?MLiL=2 zUJp2@Na-nRTNCt#+*QR=8gf?_DUGqqiOOaBK|3i^dk3U-oVi2JLrwui;AUWAW}t2rYB#dG;FkS4|>D(Tvcj$&rZK_Sl+YKZw;0= z0?UdwlervFr1b2LC{lWMhZiY5yZ+7TPH|X~(zEMVr1b0#DN=fNeT$Ty-NDW2cA?+S zEQ6k%e*dx{t=9r~9g9(6iI8W0pbBPQQ*>qs1nl$eC`$dXp$KNYbJUjkQk>c6$cbn6l{Ouyev*T|SDV`mFvq^z69SHj|zm z*V<;%v*TLZ%qctg^XhS#lc(_KG}$qkYc)0L+40@_PornYwN9J#?6`g*FzMOxeZ`wG z?zomolg1r?M!%gQ4Kpj(4B3`hnPy10%(`gyobeLt2LJy+dvll(`v&{}Z^d4WJs!I! zwmo)LEE(GpI|aM{i()fkV`D>N2gj;oonkFwLHA$S`+v{fi#&M`AWz;^?xmRXugAXs zOn0n1#68%pb~|9!|4Z~MQ@L*D7pwb4bwm!H;b7JV?UW&lVCY}i z&3`YnH}q8KfzWNCYeU)4`N+E17+M-SE;Jsi2Yo`_L+wI#@Q2{%!S{lDu|{!UaC`6y ztXZ59Yz!VBoDn<!@AzeTNheqSnI9j)*S5QkFxq> z<)@p~&f?}T=0C6=@uvB_`KWmp)-J9vFE!6LPcm1UbIgh8F3tXl4AxEY)>0kv%vffS z&ph*#!Ro0jBeBk40o7wH#7{BBQ{!BQ&zRUq>f%V06p!+rO#3|3P;+@~+gU^&&pz5B2X)>A!P*PCUq zpz7g0bu5DwRS#EIvkaD0J=|j-%V15_!__@l28*g5uEIT7RrPRX70X~*)x-NLSqAH> z9xnfwWw5a7VazNtSXuRO*KRC>rBx4?b!8c>t$O(5GM2&Os)tAHW*MxmdU)6fmcjC> zhv7Pr!TPF)k-sB@1y*WN`Sz2+8mnX^pqb8!kzi)9(kjbHO*2?*m5e4E8LYQTM&g;l zlB>r&&B$QYRhE&GX0Y%o87XH5Yp;@duS8gWRb|^n1}m^0^MojaMOaxzs+qw$tYoCH z87#&6Wf0g5R%0b2LCs)6Rx;Al4Ax{l=4o99%d)Zza>X)OnZ>_qy!o&UmS#OXc?!#5 zZPvrk&I}f3<>yK7GFYFLpQo8c21~S(kyK}}N-G&Dbp{Kyl95nnuvY6a*~771E6cnd zjul&3=Jjwa+TveT@bITt2CKFne(G?R!Md%7>-VrUR&G7q_6wFi&BuM}SsF{X-t{lq zvNYCiJ>2pumd4_(hwJ;W^hrMcswGQr^6?&gcB7Aby~ffTeEeE3mR|4U@^@HzosZus zXX!={Q^G2iZtyWiVtTEQF%r`!`WPcIy~f8FiRslo#z;)B@^A%GuCeq9KE_B)uk4iSVNK7yA zF-Br~zK<~y)AM|ck(i$AV~oV~93NvOrf2&YBQbrPk1-O{vwVz^n4an5AwyaESP#do z!N4Df*8F+ICUKCXJ7r6>9r zmM1;I#})W{$NPBEe3l;PEmtFS$c$zC)~l(!+m_m1ePA=a1NlE-NO!P=ghOz>T2Wbm*+#>5i z%0if|2k8o7vL2)=gvol4rVu9UL5f0{tOw}{VX_{iCWOg)kd_c8>p@CFn5+lsh<86m zlT?uki>!xKkqV2fhg6XYi>wE!2;EQCgEWLNSr1YW!el*2KM0fcAoU)rgx57qOI7jkvjBAxlZs!2JtYN~%WOgat@R)rgz004b>&aT69GB~>GC z!UCkEYQ#-gfRt2?xCsl8lBy9mVF6N7HR9&T-7F1^aD5a@N!5s(!*N}z zM%)}WoTa2{#65RWsTy$;)-@$nBW}XFrle}bO<32IRE@X^>zb0P5jSB)Q&KhJCah>m zsz%&|6-`Oih$F8sOG(v;o3NrOsTy(f{R)vH)xaq5X1O9oy5TXhJSkERkCEj`%~6aj zPm0vTFWV_nq#qt5%abAnA?8`06lsW8MwTZ50e4@}x*nWEm_^iZsPzWO-7gDjp-tlOkOq=2@N;DT`M|mM1kH4DVN% z;j)y}mTF9JSxRb4wF%3UB5mf(K#EKiE`#bac7Qlu~*Bg>N_jqw;+o)oDJ zG0*a(NN2n>A$Yxi5q>}XO88IV2N7-Xx^OOhL3nd`U3gh|HoV?P!t1>@+%?=5Cl69#JM?34 zow?lnJ~+#qjm-6X%!$GA=I6mP%;CX{f=k^~-4or#?y>GTcbMDH?d5iHTf33yk2sm| zVe~b`Ja{O&GkQa`39%2)#JPkO(Yetn(b3T(qIJ>oX#1$Zsf4ecPn>P%}dND%zMmjm_eTv`#APS z?Ah2uIA3vH?6g=mb{3vFX7+6H}quamQW(JDRfw9a%g0zSMWvlCi{ea;{?RF@RfL5 z>=93j`^64{Ih8n1{0`?KmWWwmf*3CPiC&_MXpPelKk~ov5BY2S8U7I8$#39I{33oP z-@sSAGc}_M3dKg@sFLb;q%(igo)yPP2zjSdz$Qx3#93#{%6*Pc1;lf z>$Q8IA#=_D>{mecnt!B8MJ4}GllLq52l{(@z9!z*1aFaVX>#QKVxK1WA1U6nKQTCd zThESaPrt^<8F{t{H_`44Sb;6`7tU&oGmIQNmWp7B$m52oT&-}qVJfRBd~TT9pM=v5 zQ#n>^rmSQtCu`04DNN;Ltrqj zDV3o1rtTBB1`ZZ8G#M~ZOxI-h05MIIjpvA|nw+yy9HU9E*TfV}Uh5?$Yf|;Tn4}5v zwW~DMajT+Is5I4aYtek6(p1N-`CElbQysS!ED|bBHA)r;m8Lpw;ax)IXN_B1=L?mV zHg3(jUZ}LRaqIe7VzmAR)3=FHnrxdcR9f1&HQ^31LYLfuA+5<#yM;;$8@I+BB~*^u zxHW8y7@}`O9$1y5mS$Um_93Zxz3G;`rJ(2?vqjUQqvkvY5znMzm=r$e?? z-7|0yyGr5SuQQdh93F=3a&^77hN%?Q@G)d6MKwGNSx((kJ&>t%)o?RpDqS_43o#j> zv5&^;)}5sk?o!Q?8h2v}g**M3U7~QuF6<(WJ290)8s3EL0(HIp0qlH*v3+}4%Ec$uB3*`3C{Y_(?FjXT*Y&8{=Hvz40VjqBJ_tXF>xSC;=D zn|1&9N-uj;Gjg*JU~rNQd9L|EO>Vl1{jBeUZ#?@a6+x5e90c#`kLWndt#Y<#vT8Nn zT6|N=R_7emM`~KOX0!b#SJR>;J5J$P3pPt(H^yfAeWb=^GyFbMQ()6oYZsP48yEWb z^eV7v{yl(G{aSDmg11)q=;^$b!efr+EfpU5Fn1LmK9WZj9yXji3J)E_TWCCl#}pni zj0=s2a;7lOK138g{W%w&1A1>Jf6LxaQ|`aWrb@$XL}UxS<7D1xCeVt;T}EN3krAd!k$yOybJr2 z!kB{iDJdy$>pkzVv{?=u7u2jJ|ZA!f;@^S7E5bJqm{->~6om#l!3_ zzrV!`>`ryZ=WW;{3hx}p_A0z%5_?_Yt#jDh3SZE`{_JBMcwl!ZJna^?QyD9)j0sB< zywI7NtjP&7k>2Ye7RQ8|k{@|YCu5lN+GqjgLC&UH_M*kK4Jo+B| z1fGgM5WOvWEzSp=4^M%{=+fwM(TUL!(Zk>?P!??ywVj`kjqpR~73WEu64>Tkj?9Fc zod!e%nC=|u9O2Z$cetfvVeb2hcmuHj9>mP|YMc}Jz1Sd@Vct7V3>NjGT(rfk_j~?V z{x*LBCk1x$>u~=69DXuC0jK{b@!|XsUWN4!!G30cWAC$<*%Rz;b`xu27qH*46WKzX z`X9{>XT4bGW?SI<;rhAemoud0 zE$cKxlohVlfMh73t92Pdl<=9FQDBl8njta?SL+ut9?4W)hJYme7|kdQ$z;tChlEek z4ADpU1kETQ$Oz345rhxd3~@mCP|Yah#~{rR^Meo63;{lPf5p7uNY`sd(T%PuGK4p} zQZovcbggEHmc*~oj6xUXG^21u8O?)<#V<-~Mlp*n)ePIT{1VM5 zX3*K1Q81rVHKPDNYVA)(@KI}jGK7y>`;%dNHt5e$K%NH8C?d~V%_wqDV>5ORKhYoc z6nclB;E!&^-r*}1UUed0q40_vU#jr(6?}=p%a-%w6<)fEFV}bpU#9SqWqh&5OZh^D z7hTQgDZF42pQ|t?mva=Jw}h+R7KG>FYPSX9ceq-Xqo6yp)cpvwbF9LMr!!4qgw2_v zFrwp3RJghqAFps#H6N!i_R_{GjIFez6)vyfM=9K`oFA!h*KT}_!ew3gXob6!@lgsR zG|otc5dmkI!XnCtD2%W--Z3cH<9d#5XR^!;QIz-*>W-`D^5YcFE#ZsQL-hhJtlAilJg#um<+TbsQC_1kPPg|`mAZeG~Ei9*y1w@Bb^JmqyQyo)le$Xn)h9M@0uk`$Q|F9ind3biQ*wL#)3& z&Ti*E=T>K{lXlK^PIJ~e$0O3;cxRY%sM8Y%fm=CY@q_qMe1JHAPl@})cKH8aEY1*( zhygHDj2FYi!J<+eAUNXteaYX0@BiccE`B4=BevgZd<|cK$bO^vVZ4Ty@iyFs=l@si zBlbFbmOadNu^ZWC>|*%-Z)7VG*Y6lMh7ExCe|L5ObC_X&V}A<&e~RjNkG{IMDSQVLJkF|%`2iw(l2iuAK0{{OH5YKOSy;Y zb`cvV@IMc~1JD0Q!@I)QhqK{x;rG8PJTE*sJR*E3y#70cv89f@il2tw3_TnABYgh1 zB7WZ%#0yvxS`?ZQ8VjHQgG1G!PN5d?2>6%w`hOsJTX1VI89W=g7?%f+3yu#C3HAwg z54J=6z8|d5t@jYW?o)6JD{Gx^oo+Q+OReLqiPi{2@T)<@hc=dN{)7yUADOS4 z&zcXLyUZKS%MiowEOWiN)SPLKH3yl!MNWDXMMMDm5+WzPiNNdt6FKQkLd>$!ZU7SIq5%C&X1kJ za?*bY*N?KUB_l0Zad( zaz63_mXrQN!1uG9^dBnc_m5;b=|5D?9{`sAL*;zn<18orhsycm16fY`50&%YFR`5T zA1dcB^=3KgKUB`E-efuHKUB`&tYSIoKUB`Ue86(jf2f>)&_(3Pf5>c?uwmboGWtWifu8@5xH(YD{L~huv}NL0N#@Zk6^hn z9}gPLa$VH*AuQL~$Bz$Uxr2P%XAjGD@-ck1avgoV2kqFw$M9{-9q42Dw&f1+@s7zX z*WSlt@NC-oc;pzCYwKfpHRjr=>!Vn%wU0;OdMh6f$7fsmc*t;;YvJRe_{}kOeJIYF z0%LUWU)~RN6}hO-0-b*OOynF@_F15l$O%YT`hyiZiybgEzEf{Ni#zsOlW^LztyrYa+EzFa^t@2MM#(NoX@)A2ZFZ4j^mMWd6{Dw**Q)T39nVqZ3$k#GE2MlLfA~?&Eo$X_s zWyv1rW1MBl&hjzNvSera7-w0s$NCs&S+X;H44=*HbRXj^OLm%%;lG)k>SLT`$sXfl zc=BdRd+0a7lQ&DsLl|dSvZOnNah4@ZsssPc2z)=Yq&bA)`KKNWH0uz$Y|2$ou^j z5qK+SrQTFT;1imadQ%aBPiR)^O+^G-l3A%Y6%qJ^W~JU#MBo#em3mVVflp{w>PIsW%l7c&ulo-c&?z@+?bw15aEpq>adu;&{v(yQDcD^Tw`J z9o*@SU8y=?-q@9@1LlogsXEnRId-M$pv)V)Qgy(*u`5*v%p1E>b-Xf+U8y>#g^sUh zrRt!}8_5;wmwBUAst%RWXqBo1=8aaVI$+*tCDoz7NRC$09q;e+MypgFieJOBol%03 z8>Z1Jm4~9&h%70O_j&S+&XV?cpC`}gEUAyjywOVf<1ufvk^*_m8?B^4#N=ot74piw z(Mmexm3gC8Dp6IKMyph!sxXaKsYF#_8m&@^s=_o{r4m(zX|zfustVI+B_+bh3e#vM z9U@Gll~jl@jaJei!Zcb*fe6!RCH)~xqgAR;RhUMrRG+Fajn=StPi2@!YskkmT1k5_ zTEjG2NqGp4CrY-#z@SN+R*hHoarR3A&gO( zA*CUVQJEo~A&gO(A(bJF@6C|L5XSdrNMQ)WIUqy&LKvRi8B!PTI_f8FA&gO+A!Q+q zQJf)N@$Sc&QBoDc7{wXV6v7zA8B!F&7{wXV6T%q98B!C%7{wXV62ch88B!9$7{wXV z5yBX`8L1)_VT|02RFR4>&YEPTiqwV%Kgu%4d-s!%Cd(}L@${Qnh7<&+ui&A{GNd1b zC+uPwQV+rtC$J1@hj)D<%aC#qj_+j|(hb6Uah7+Ek9XmIsT#E*obJj<)u;`mRF)mBm`e!tVDt z{r@i30G`0k{xfjaK|C~-w5Y0rv?GS+BmlONqi+f z60eJA5dmE|cq6NV6Njf|^}t+-twzjNQz5Gj^7FcK|1qc8&MKdmq# z>MO_j>S)YXj`P)sfv%k8s}Tf!x9?zHE$k=!UCru(tz6%$KRM7=uKU&dDs1KUU%j`+ z-lx6^VeLOq__m4mC#rK4PQkPLyJl<9iT|nDEDQzZy;NNojUgM=Wg{{0TWQwc*v;B& z))$f04npU^Ka-Y75xz{3KK0niRHVKS+?6%igISIuyoL@9d+QbD)L2JM;(%+g-}PGWTe$nM;&;qitMR()Vav2$WwqSe<_xE9Igp|wXF$$bxTd~t6fd-s~t`7t7DqrS4T9#uV$Ly zR|`$>t6M9A&`RPAP5RV}(=@5?BTg=oJ>qmtZ~}CrCgtyl^_sN(Li|pXmS2feH2JEf zI7yQ)+KN*(d8b^g(*);9H)yh_UTo6jsl&x)MG&xnKc>l7E%~EhMNY{196m{HTiew(nP;jn~=gP)AjKkNqI+ z|J!D_;{N}Ai2ip)?BduNvBuc(i2Zj|Yyk59mBrd%?*D_g|4)(s?sK;z@?X;3;(PT^ zL%jd~ZjIX+KK*9&8|?nS9(@`f{kKN1!ruQ`(e?1>pZRYy|F@jyore+k?^^8oZ*fj? zR$|yp#Ge1bP9<{t@&6F>PtEz~Ag|vj(O<+x8M68Xb;Q5_yzAeScj7KL*f;D`_7;1Q zJ&tUC+Y#?C$+oal*c!Ho&0u5M5ajZ!W}R3I7DUXy&+Yf@z4lY~1IQR~t)0f6|4H^r zdk$j$4a1IqC1M$HMEv_Q@?K<5)?GCAca$FE}|k0?`_(k(rEvB9EAuF*xDUmgvB$u&BZofJ4pG|4qOk8Ki7a*Ym*b~u~l z8XZ{cKpW8{*XTUfS~STuI@C$KpG|U&4rQ$ZD3fb+U~PW+Ni@kdI$$v_|K(3l1ZR zCR(NQ7!L^1MC)`OBQJ?2xl)JE+4K!^r4Ed|B%0((9T<5@G|81Z{I&3s5KXij@NHq0T?9ax>qN$H!9~BM~P4((?2N{1B zO}$mw>&Ba+sZOz1jn_m|T(LI`wW6t3u{Y?>8pU2W-VjZ_6no8hT{QJn>=olx(bS{J zP-nGbdyQ8_Ql2RH@3IEc6#m6{@Vi@uX<#uFCE)?h{SritRLZiKcFf-EG_> znz|}>Utx-9DpL$E|DuWZiM*$Bk8!VP>a5sZSbJ`wy&|vdE~-WD7UB8bY1}26I;yf= zSnF=;pcw6+H62)F_$>!0c01O(o7yXOn{m5nYNr^E6pN;|J}XSYT6a?$Rd#h@o@i>V z8P=tnTB$qd5o@W+GKJ-$sfA`nMl{9Loy(03Z*qwR@Hd=whG>ea8_zV(5>1X`XBcOS zCZX7719x)8PB%95CI%KjM?S+ioi*7+P~)a8tSRE-Et^ME*3*e*RYEX(x%we*Tv3q`f5X zPHqa8KN*aE<*jmEDT4m{GuYWMSh{re*V!6 zv-|=u_-qOIhvnz{82(}Tc|L}JSYGZrl?(WX<>&a<;UAWlyH4c-{$Y8!>r^ffK`l?J z=GBSoq-liVAC@OYBMkqrJn0!>_=n|5&AjW!vOH-SVfcsTNy!MqKP*o==HZ>!u5(d2sBn(pjystsRpf`iOuR+3~cY(aGK>|scQRZt9-T8*d`x+z+dKbuh8U*8F(7Qn1 z7a?N!2494PLGJ>2UxbABE|B*{NEq}kkS7s>RTc$$7s!(e5i{srAWu5vG1BckDUruW z{qv+n9wYV7lN$M-yI15%k32?i4S7-|kI}n8o;1m0r2ctdm4rd+pYv5oz?Wv1$oZ-y zJYSleuS&#F=Btu0=v^R3s^ry4?*ci}C6Cd&K#r7&nD;J_BW?1^=v^R3>LeM|Kj-U{ zD9j_~>yt3(T_ES{6BynFa=t!^0^S8!j`RtdD4^UqQYXSt?i^_oVJLKtl!-7DI!C%h z7z&*uRU!<9&iR@o0?|NJACapz|w8&>|L~fp9G#km09#NS$Q^}Dc zd3VxmBuAR$F`B95NR>QBvymL>lE+$M_LC!J@>mS>j~r=}e0BOlh1G` zDU`FOb zBZ#rTD|~!7AHE=bI_BH)a3I_QISxNW?7SyJCn1vliqKu5bFeNk;Wu#xzC@gX*PUmv zHnGdO(YegI*g4DDh}DUCSfU%_3~+io-LXF5IEMH}e2T1uFN(*-Jvftal}L&$;uNt) zED|%sSTO{r5~@Wf(E=+J|KgwHT*6-d6n}u<#;@gBoJ=^KH}J)L20sc1VB$EL(257y zcUYr%6Xz2C$Zlg>u|jb+P9-d7$FcEPpXkH7>uLFZu)hWNx4{2SE#N0`i8{uc#y(eN zG9y~3bX6uZ(0wR`*x zw^+N!HY09+)p*6dw@C4h1fMQl`ik*}d$*=|FM0*1zI5q}g?jf+O$+tLi|#IeK|AV9 zo#(2!JyECK*X|vfWZw+^OykP3FvXZ`EY>4EGjIX3TN7Ycg}TyG@g0 zXSz3QGUHhHCQYW#aBtLP<}CLHO{PtEuh(SiH1|49j+yFStI3pO+-o!$KgHdu$)qXn z)tXG1>RzSE3jxyBBNnZn=ArChb0RFD#OF z?gg5(`Pw~Slh$qA^ECOaoqMh(9lmzY(d57mt_n~Wb=tObRe-vv)BZsBY<*j+uif8k z(zd;OmL@G*xo2w9x~;1=grZK1uU)n25_MX(cGVtG)QPol)lLscOIPh1MIE>=s?D9K z^R?@$BkeJGo?mC}_4oW@#?8hy>*Z$bI_p`@u!V2!(F|Mo)=QcpJfZbsGq%lop&7f` zQn{RB*tEaVdcIlN4c2o-R#yMgUVx+7GG`q;S#JXQI%=xYR@Hi-PLJY~XtOs?`dB%m-JyZk> z6kBhcXWi}ZOU2f#x9(DS?ix#l5{pfpYwc3kkDhAnRCx5!)*T8DA8p;P@POgg4uuaM zVBMl{?=owR6ZmswXR+^x*IOyRC&R$k$)ov@H7>U<*SMS2q;Xd(r*P*oE35E9ovn<*9S^e73U}yer4(-8 z!AdIJw!M{5n76eqRoLd%B??Du>tf%3AQrJM^8E*51?xi95esY45!F7Z+oE7y;D6SQ zerKJhu!vgcD$HA0=W8sia}ks~Sxe@DZ|GV6R^?P;4cibZDMx!BWyj-wU#EPi# zg7LD|=>McBvg*BHsW=8v;|b$wOC55LVyWQ?OU28GVpHHAOGVv?VzPgyrJ`pLyTek^ zGommccUa1NMKLkI&Z4bu8hz1|jEr@r{~MzlPO>&De8L9nRD~CwV4bY+poP|18b4}n zRJiLP>m-FcceOSt9PMm1D9oeQi3;OMtyUOMYL&v_h;@R(p|G`5;b6#Gp)hjNFZWwJ zYFW$t){YjerRt7H2U#0@>})JpOB7yxlXbkpOIBNp6<)Z+TBPvoh1NoaXUw)#3_RJ;f0u*0nh>iVIFS>qKxWSBKs<3p`+ znEwZ&=F!IgEN5Kk$Ius{cSA3Q9t+(Wx)HhS&kvo3)94FA$Am^9`g~8EMR!9+@E^gC zg0E8kIo6g%?C~wX7x0u{}wZOn$tfLz5`hRvf=Z>r-e^MuDt2kKOY$G9WD>I3$xhQ z&Eo4{gZzcRkFAd_fd|8pvHr1M$XysUH(MRdMx1Yd4g20R%?HgT)+N>z)}hu})&^@f zvfxd$hFej~z>fIE=HJaL&7-X<>l5of>vii-);2^n{HOa@#6Nh&{S#ifZ+EYAbM6K1 zW_O*t45uF^yQAGB+&Z`1ZSRU`A^LUn6P$l|A^KSKZmfM=8BIk05Is4%I=V1AJ$iI> zaI|l7#!7?9zvX{HbiB7w^Jb^9asB{EdCcUSrR&huBV>f@oqFu`}5Qwt~&YIf&8h2v*0+ z|Cgs&{%pYZcAZ`2dSg3-2D{30#&!meaFt7p?L0on-KalBEAco_E{n(XO~5R|%#pN)dMT9-}P;jYqT@)Y+3 zO?FInR~A2oRe5)XE*m<;U9QQP$K7R`j2h`K)nw!-cS(_qagWzz#7K9sCc__h7iltN zsJpO8hPw;&Z=e-uq*$SM=N2g%2mw0OUrlTD?rdLlY)Hx7rV0mS(+abx zLjKsst;U0<3i(6q7Bi(kcZ+eWnbhn$lqEFVYFuYts#(g|YN~KRwvjTD=Eb@!X{5}H zG`pnGU@C>Sv8!~6sX|%Vw5w#QfLJzmm6E0kY-Q8hzPU@+ve$UqyrUU=*}Pq|myNxq zdS|wc7mb(A9lGqfLVr`eGuy_WjOWZ-blFqJljaRYRv2jBtl5*sQ>F^bWE)Q!yUiPQ z*>05G)Qml8{y*)VYj9Q7701us`zAT(u5-fE00F`)JmhTz6qJwv2_Ybl@P0paI)H$n z2o~j`Q?+({AR>DAW@^VeQ|l;Gky=s5!PHXQB1X_c5h)@?Ku`qQ+V-Yv?S0k>AMD3| zi(mZzvi7<6a_&w3cklf_>-;>SS{%tuntbnF?|}SeQhV2{mE5w)_uj_$f##L;4FYMS zd@dRImiM;gzE8flr+$*$k<>7mEKNeV1tE1p7(kHRm(2HY3qtZ#lXi=uPW-up5w%gr z1j@^zZV8l)h>9AZJnE3Zu#wTg1}KaAB~Usn8k9guUDP*${v}bL1nT-ny%VS{jRqv} zN=?**KgxFNF1ns;=}E8P-DBfF2P31~uMaZ*aKV4CU$uq80RE9i7 z_R}LP@cu4b8BrVZ6xmOgm*ILaSIS0&JYV+HW#!nDmMgy(-G^elc7r9Tw8t zY5#JiOX{$bEkpkjTx@2j>mTxBfS;}{4S4~olspN={$zN6;NNi#yO4o=FQF^mck;Ja z(Pdei&}Hw6R&#j!oz={)u=!A>1gGzfK4@3gzyEKK72t-&{qXNS=y@{SK+o z{MD&%KUKxLPhT~Pb?-i^l69{VRh{VGs)BWoqiQ(o?mbjF>u%ju8SAdy)G*e?z0`c=nm=z)@@IzL97dl)PO{{RoAn|X;%sBf>!D} z)-4MZH?j(ww^aT3=S`cdzO0)xRef0JH&MM=NBOE3>o8J1SwpC8yV-P0fpcB|ubel9%E)8$>Tr zDLEGom?-QePjJmUESjrs2_5tfE55h^ytllAsyM0b^xjfk5_-wosX8b0Jg)q7O2~N6 ztBwgh;~CWKfKdx2YDam#k1N6TMV5XMNi=)s*$Ex2Yzq7vHM#Sua|wBGwBR zsgU)8g$h{DU!c;g=gn6E>$&rk&w9>WmB)Ja9F@y@)@+r-dgd&ZVm)J~Qmm)XP?Ghu zB}%ZKJY9LLCry_1tZOF8EbECi@;|I6OqBm-J${1x7wd83<#(*djFbOlT|GwrBhl6J zN}_kmzq1}vEx%>mcZj^mx@TYcHS4ZDC zZO+KktXs8_U$8D{C;yh{R`L|<76tNi*3DYTzp`%9Ode+)HIaO&3WSk77XR)-C_jxS z)qAzGV5(iR5R9_HmJf_r}OLOUsyxF{Bxo&%Ok7;Y(V_} z9kss-ad4XiDaI3ZzZ$*hh6coGO>|uY;+!Pvbv0(Ak_N;{Pt>2kAYh0n>X|^zr0CiN zs%oRI2~<`@#R*hYMO_-860gF?`9I(_XG#CR|CRr5-Xw)x{ZHo~$^RqnKkdx_8K(b# zgwwYr`LpxK<(K7`4q)fMUC|5J&Hu;IdhGi*KdQk!_CZk(?D`jm zS?uJ0B0Pva|6UJY#IHP$;x+%P!o_$oOI0`oulO&*YfmKZt)If{{ojW-;bq)e--1{B zuYn(6x4*Ga3jLuo;2$U4RX>wHn%}%-gE~z8@5WAl&jvpU?!(OgLhSQb8Qd83#KeC-cKN&LpTrEpKL3sH?eSNFiT^@B z-}mw^<(?Z{DuF7xK2png84JW@4AW;kaheGp{JGd0sB|`8%I`BKPCm_jBLI zRf`ueh45(Z=G-;8OLFI8ufNLNA(%(#jO!NZoGfM%PGPsd{W*IvkMLs7j+`xc@4tI; zmg5S`)ST*^(wvfssl}Ty%FKOb; zW{7!76K6I<%u5=|Y+}+ew0TKGsZB?imo${yB(ix)L&;5Bqr9Y{?4~8kOBzaVTB5w9 zq5P&L%1atba9X0gq@fI_CCW=0N^x4EyrijQqP(Q3V4}RF8OcO>Ni!lQo0l}hE#Ve0 z?nHwrPpG~dm@+2ZhXyl@=?G@+OleHC3k_x{*KiXW%*{-=2My*XCftGsGlYq&f^Uy2Ki9xo0}Z8t)Ei zI>mY->W(}H$9ITz&A7l6#d^ZTz_e#w6PR|f9zP*4ZDT!dd|(P=jdz(eZDKtZf9qPu zdNiJ&RJvSB(zLHLR-x(=yh0S4z_&)|I0ILupO=x9_bC3}rP*7k?HQN@|kE zJ5d_SX_Ee|OJFFaNxI-nU?`($bwOY#p-H+WzK@j8B#k%FGL+6FeFn!+Hj{KAj-g~G z={ALdp4%t5)-HMJhnP^-l)R)z#e}kUxl8+R5rKBOOFzVfvUa&kKg5KxrraeRfeB^p zQkQ;+cl*}PM^IM<+WCkdVnSIvAMry>C~M~dSo%6!0GeFI(gpD~Xz~?Hm&GN}OecEK{`k^=QKzkiG!e>(;$67HRm+;xQ{?Pr$IU( zJ_PNY25GgqozvjRO7SLW=QK!T#V*j!X>jE3 zY&B?d8hVei)nYej@*4Y&y_OvVn%u_L#)#KIli%3dtJzyXljGRhE#g(s$kNkdJ81GCOWVZL zpvi?SJw?w>K4fXDcnUN*k)l^f1-Pq3m(%#KWM; zqipS7aUW=MshI8sO+IDmfh?X0aw<#riw8iHS6SLD?gve7WoeVx44VAP(nhffG&z=~ z4Pql`@+?dDi4CC1wJfa{_PSE6DmR52`d~4EdU+W#V?oIA_D(j-_H5WXRcUZHZV48S*wu z>*$(aayO#vc7fLpI)B6C=4bJ`Lg#Ntiv?bR===?7t-z~;oxdS15{n^2{$?L@p;!bN zayUy1#6rlB$61;$7C?qv&OYCB^q9`)@KI)G_dtex&W@ZeW^>WYZWr&dX^}wpCR9~v>M;{3^|{r zYJpcFlJ{8}Evg|y?q{h=jE0Q!KYT5C#RFvKb8VEUf{gP&tW}Cpka7Npv<^Q@8Rvg^ z=J$w7$Rz%UkAw4nFKDv$d-DIu)UQ!Ao`J?Q(0B$K&p_iDXgmXrXQ1&6G@gORGthVj z8qYxE8E8BM|A#XW7+R~KJxT0$V`!yl60pD+5ET6QuE;PlnbeNI$v!zfXEo*#H0l diff --git a/bench/fixtures/query_server_schema_rows.json b/bench/fixtures/query_server_schema_rows.json index 92ca47c..a7e97b1 100644 --- a/bench/fixtures/query_server_schema_rows.json +++ b/bench/fixtures/query_server_schema_rows.json @@ -518,5 +518,31 @@ "typename": "timestamptz", "elemTyptype": null, "elemTypname": null + }, + { + "schema": "extra", + "table": "gadgets", + "column": "tag", + "dataType": "character", + "length": 3, + "precision": null, + "scale": null, + "typtype": "b", + "typename": "bpchar", + "elemTyptype": null, + "elemTypname": null + }, + { + "schema": "extra", + "table": "gadgets", + "column": "pad", + "dataType": "character", + "length": null, + "precision": null, + "scale": null, + "typtype": "b", + "typename": "bpchar", + "elemTyptype": null, + "elemTypname": null } ] diff --git a/bench/fixtures/schema.json b/bench/fixtures/schema.json index c03b98e..679022f 100644 --- a/bench/fixtures/schema.json +++ b/bench/fixtures/schema.json @@ -269,6 +269,16 @@ "type": "number", "optional": true, "customType": null + }, + "tag": { + "type": "string", + "optional": true, + "customType": null + }, + "pad": { + "type": "string", + "optional": true, + "customType": null } }, "primaryKey": [ diff --git a/harness/query.ts b/harness/query.ts index a3dc6c0..0bd0180 100644 --- a/harness/query.ts +++ b/harness/query.ts @@ -16,6 +16,16 @@ type Opts = {flip?: boolean; scalar?: boolean} | null; export const zql = createBuilder(schema) as Q; +/** + * A builder of its own for one program. Upstream interns queries per schema object, keyed so that some + * inputs JavaScript's types rule out (a `flip` of `'yes'`, an `orderBy` direction other than `'asc'`) + * share a key with others, and hand back whichever was built first. A fresh schema object per program + * keeps one program's result from depending on the programs before it. + */ +function fresh(): Q { + return createBuilder({...schema}) as Q; +} + function sub(program: Sub): ((q: Q) => Q) | undefined { return program ? q => apply(q, program.ops) : undefined; } @@ -89,7 +99,7 @@ export function apply(q: Q, ops: unknown[][]): Q { } type Program = {table: string; ops: unknown[][]}; -const run = ({args}: {args: Program}) => apply(zql[args.table], args.ops); +const run = ({args}: {args: Program}) => apply(fresh()[args.table], args.ops); const program = { '~standard': { @@ -211,7 +221,7 @@ export async function runQuery({url, program}: {url: string; program: Program}) databases.set(url, db); } try { - return {rows: (await db.run(apply(zql[program.table], program.ops))) ?? null}; + return {rows: (await db.run(apply(fresh()[program.table], program.ops))) ?? null}; } catch (e) { return {error: e instanceof Error ? e.message : String(e)}; } diff --git a/harness/schema.ts b/harness/schema.ts index d9ba8b9..c9d024e 100644 --- a/harness/schema.ts +++ b/harness/schema.ts @@ -60,6 +60,8 @@ const gadget = table('gadget') mood: enumeration<'happy' | 'sad'>().optional(), moods: json().optional(), seen: number().optional(), + tag: string().optional(), + pad: string().optional(), }) .primaryKey('id'); diff --git a/src/zero_server/protocol/ast.py b/src/zero_server/protocol/ast.py index 49c9197..8e88b66 100644 --- a/src/zero_server/protocol/ast.py +++ b/src/zero_server/protocol/ast.py @@ -1,7 +1,7 @@ """The ZQL AST: what a query endpoint returns for each query zero-cache asks it to transform. Ported from rocicorp/mono `packages/zero-protocol/src/ast.ts`, `data.ts` and `primary-key.ts` at -zero/v1.10.0 (905d08516ba0caaf314cd28e93a5a47e8acdd482). Apache-2.0, Copyright Rocicorp. +zero/v1.11.3 (e91e55f05b7a2c631f77238e1dd6fadf1029a538). Apache-2.0, Copyright Rocicorp. The AST carries server names: `map_ast` maps a client-named AST through the schema's name mapper before it goes on the wire, as `handleQueryRequest` does upstream. @@ -17,7 +17,7 @@ from zero_server.protocol.wire import dump_json, json_model, parse_wire from zero_server.schema import NameMapper as SchemaNameMapper from zero_server.shared.asserts import AssertionFailedError -from zero_server.shared.js import js_keys, js_sort, js_string +from zero_server.shared.js import js_keys, js_sort from zero_server.shared.json import JSONValue if TYPE_CHECKING: @@ -68,8 +68,9 @@ def _names(names: Sequence[Any]) -> list[Any]: # The nodes are plain slotted classes: the builder makes dozens per query, and a Pydantic model costs # several times as much to build. They are not frozen, which would cost as much again, but no code -# changes a node once built: every transformation builds new ones. `to_json` writes upstream's keys in -# upstream's order and leaves out a field that is `None`. `AST.from_json` parses through the wire +# changes a node once built: every transformation builds new ones. `to_json` writes the keys in the order +# upstream's normalized AST has them, which every AST the builder makes is, and leaves out a field that is +# `None`. `AST.from_json` parses through the wire # schema in `ast_wire`, which accepts exactly what upstream's `astSchema` accepts. @@ -118,10 +119,10 @@ class SimpleCondition: op: SimpleOperator def to_json(self) -> dict[str, Any]: - return {"type": "simple", "left": self.left.to_json(), "right": self.right.to_json(), "op": _json(self.op)} + return {"type": "simple", "op": _json(self.op), "left": self.left.to_json(), "right": self.right.to_json()} def negated(self, op: str) -> SimpleCondition: - return _NegatedSimpleCondition(op=op, left=self.left, right=self.right) # ty: ignore[invalid-argument-type] + return SimpleCondition(op=op, left=self.left, right=self.right) # ty: ignore[invalid-argument-type] def _with_left(self, left: ValuePosition) -> SimpleCondition: """This condition with `left` in place of its own, of the same class.""" @@ -132,15 +133,6 @@ def _with_left(self, left: ValuePosition) -> SimpleCondition: return copy -@json_model -@dataclass(slots=True, kw_only=True) -class _NegatedSimpleCondition(SimpleCondition): - """Upstream's `not` writes the negated condition with `op` second.""" - - def to_json(self) -> dict[str, Any]: - return {"type": "simple", "op": _json(self.op), "left": self.left.to_json(), "right": self.right.to_json()} - - @json_model @dataclass(slots=True, kw_only=True) class Conjunction: @@ -171,14 +163,7 @@ class CorrelatedSubqueryCondition: scalar: bool | None = None def to_json(self) -> dict[str, Any]: - # The builder writes a condition's subquery `system` first, where `related` on the AST has it last. - related = self.related - r: dict[str, Any] = {} if related.system is None else {"system": _json(related.system)} - r["correlation"] = related.correlation.to_json() - if related.hidden is not None: - r["hidden"] = related.hidden if type(related.hidden) in _SCALARS else dump_json(related.hidden) - r["subquery"] = related.subquery.to_json() - out = {"type": "correlatedSubquery", "related": r, "op": _json(self.op)} + out = {"type": "correlatedSubquery", "related": self.related.to_json(), "op": _json(self.op)} if self.flip is not None: out["flip"] = self.flip if type(self.flip) in _SCALARS else dump_json(self.flip) if self.scalar is not None: @@ -301,15 +286,12 @@ def column_name(self, table: str, column: str, /) -> str: ... @dataclass(frozen=True, slots=True) class _Transform: - """How `_transform_ast` rewrites an AST. `None` leaves that part as it is.""" + """How `_transform_ast` renames the tables and columns of an AST.""" - table_name: Callable[[str], str] | None + table_name: Callable[[str], str] column_name: Callable[[str, str], str] - related: Callable[[list[CorrelatedSubquery]], Sequence[CorrelatedSubquery]] | None = None - where: Callable[[Condition], Condition | None] | None = None - conditions: Callable[[list[Condition]], Sequence[Condition]] | None = None # Mapped correlations by parent table, child table and both keys: the same few on every query. - correlations: dict[tuple[Any, ...], Correlation] | None = None + correlations: dict[tuple[Any, ...], Correlation] _MAX_CORRELATIONS = 4096 @@ -326,22 +308,20 @@ def _correlation(transform: _Transform, parent: str, related: CorrelatedSubquery correlation = related.correlation child = related.subquery.table cache = transform.correlations - key: tuple[Any, ...] | None = None - if cache is not None: - key = (parent, child, correlation.parent_field, correlation.child_field) - try: - mapped = cache.get(key) - except TypeError: # python-only: the correlation cache, handed a key the builder got as a list - key = None - else: - if mapped is not None: - return mapped + key: tuple[Any, ...] | None = (parent, child, correlation.parent_field, correlation.child_field) + try: + mapped = cache.get(key) + except TypeError: # python-only: the correlation cache, handed a key the builder got as a list + key = None + else: + if mapped is not None: + return mapped column_name = transform.column_name mapped = Correlation( parent_field=_key(column_name, parent, correlation.parent_field), child_field=_key(column_name, child, correlation.child_field), ) - if cache is not None and key is not None: + if key is not None: if len(cache) >= _MAX_CORRELATIONS: # python-only: the correlation cache, full cache.clear() cache[key] = mapped @@ -360,16 +340,13 @@ def _transform_related(transform: _Transform, parent: str, related: CorrelatedSu def _transform_ast(ast: AST, transform: _Transform) -> AST: # Mapped in upstream's order, so a batch with two unknown names fails on the same one. table = ast.table + mapped_table = transform.table_name(table) where = ast.where - if where is not None and transform.where is not None: - where = transform.where(where) - mapped_table = table if transform.table_name is None else transform.table_name(table) if where is not None: where = _transform_where(where, table, transform) related = ast.related if related is not None: - mapped = [_transform_related(transform, table, r) for r in related] - related = tuple(mapped if transform.related is None else transform.related(mapped)) + related = tuple([_transform_related(transform, table, r) for r in related]) start = ast.start order_by = ast.order_by column_name = transform.column_name @@ -409,10 +386,7 @@ def _transform_where(where: Condition, table: str, transform: _Transform) -> Con flip=where.flip, scalar=where.scalar, ) - conditions = [_transform_where(c, table, transform) for c in where.conditions] - if transform.conditions is not None: - return type(where)(conditions=tuple(transform.conditions(conditions))) - return type(where)(conditions=tuple(conditions)) + return type(where)(conditions=tuple([_transform_where(c, table, transform) for c in where.conditions])) def map_ast(ast: AST, mapper: NameMapper) -> AST: @@ -426,22 +400,92 @@ def map_condition(condition: Condition, table: str, mapper: NameMapper) -> Condi def _mapping(mapper: NameMapper) -> _Transform: # A schema's mapper keeps what it mapped; another `NameMapper` may not always map a name the same way. - correlations = mapper.correlations if isinstance(mapper, SchemaNameMapper) else None + correlations = mapper.correlations if isinstance(mapper, SchemaNameMapper) else {} return _Transform(mapper.table_name, mapper.column_name, correlations=correlations) -_NORMALIZE = _Transform( - table_name=None, - column_name=lambda _t, c: c, - related=lambda r: js_sort(r, _cmp_related), - where=lambda w: _flattened(w), - conditions=lambda c: js_sort(c, _cmp_condition), -) +def normalize_ast(ast: AST) -> AST: + """The canonical form zero-cache hashes: conditions flattened and sorted, subqueries sorted. + + Two ASTs that describe the same query normalize to the same JSON. Every AST the builder makes is + already normalized. + """ + out = ast._copy() + where = ast.where + if where is not None: + out.where = normalize_condition(_normalize_subqueries(where)) + related = ast.related + if related is not None: + out.related = _sorted_related( + [ + CorrelatedSubquery( + correlation=r.correlation, hidden=r.hidden, subquery=normalize_ast(r.subquery), system=r.system + ) + for r in related + ] + ) + return out -def normalize_ast(ast: AST) -> AST: - """The canonical form zero-cache hashes: flattened conditions, sorted conditions and subqueries.""" - return _transform_ast(ast, _NORMALIZE) +def _normalize_subqueries(condition: Condition) -> Condition: + """`condition` with the ASTs of its correlated subqueries normalized. `normalize_condition` does the rest.""" + if isinstance(condition, SimpleCondition): + return condition + if isinstance(condition, CorrelatedSubqueryCondition): + related = condition.related + return CorrelatedSubqueryCondition( + related=CorrelatedSubquery( + correlation=related.correlation, + hidden=related.hidden, + subquery=normalize_ast(related.subquery), + system=related.system, + ), + op=condition.op, + flip=condition.flip, + scalar=condition.scalar, + ) + return type(condition)(conditions=tuple([_normalize_subqueries(c) for c in condition.conditions])) + + +def normalize_condition(condition: Condition) -> Condition: + """`condition` flattened and sorted, its correlated subqueries' ASTs assumed normalized already. + + Nested conjunctions of the same type are flattened into their parent and a conjunction of one + condition becomes that condition. An empty one stays: an empty `and` is always true and an empty + `or` always false. + """ + if isinstance(condition, SimpleCondition | CorrelatedSubqueryCondition): + return condition + kind = type(condition) + conditions: list[Condition] = [] + for c in condition.conditions: + n = normalize_condition(c) + if type(n) is kind: + conditions.extend(n.conditions) # ty: ignore[unresolved-attribute] + else: + conditions.append(n) + if len(conditions) == 1: + return conditions[0] + return kind(conditions=tuple(js_sort(conditions, _cmp_condition))) + + +def insert_related( + related: tuple[CorrelatedSubquery, ...] | None, entry: CorrelatedSubquery +) -> tuple[CorrelatedSubquery, ...]: + """`related` with `entry` added where it keeps the entries sorted, after any it compares equal to.""" + if related is None: + return (entry,) + i = len(related) + while i > 0 and _cmp_related(related[i - 1], entry) > 0: + i -= 1 + return (*related[:i], entry, *related[i:]) + + +def _sorted_related(related: list[CorrelatedSubquery]) -> tuple[CorrelatedSubquery, ...]: + for i in range(1, len(related)): + if _cmp_related(related[i - 1], related[i]) > 0: + return tuple(js_sort(related, _cmp_related)) + return tuple(related) def _utf8(value: str) -> bytes: @@ -463,11 +507,45 @@ def _cmp_value_position( return _cmp_utf8(a.type, b.type) match a, b: case LiteralReference(), LiteralReference(): - return _cmp_utf8(js_string(a.value), js_string(b.value)) + return _cmp_literal(a.value, b.value) case ColumnReference(), ColumnReference(): return _cmp_utf8(a.name, b.name) case _: - raise ValueError("Static parameters should be resolved before normalization") + assert isinstance(a, Parameter) + assert isinstance(b, Parameter) + return _cmp_utf8(a.anchor, b.anchor) or _cmp_literal(a.field, b.field) + + +def _literal_type(value: object) -> int: + """Where a literal's type sorts: null, boolean, number, string, array. Any other object sorts as null.""" + if isinstance(value, list | tuple): + return 4 + if isinstance(value, bool): + return 1 + if isinstance(value, int | float): + return 2 + if isinstance(value, str): + return 3 + return 0 + + +def _cmp_literal(a: Any, b: Any) -> int: + by_type = _literal_type(a) - _literal_type(b) + if by_type: + return by_type + match _literal_type(a): + case 4: + for left, right in zip(a, b, strict=False): + by_element = _cmp_literal(left, right) + if by_element: + return by_element + return len(a) - len(b) + case 3: + return _cmp_utf8(a, b) + case 1 | 2: + return _cmp(a, b) + case _: + return 0 def _cmp_related(a: CorrelatedSubquery, b: CorrelatedSubquery) -> int: @@ -512,22 +590,3 @@ def _cmp_condition(a: Condition, b: Condition) -> int: if by_condition: return by_condition return len(a.conditions) - len(b.conditions) - - -def _flattened(condition: Condition) -> Condition | None: - if isinstance(condition, SimpleCondition | CorrelatedSubqueryCondition): - return condition - flat: list[Condition | None] = [] - for c in condition.conditions: - if isinstance(c, Conjunction | Disjunction) and c.type == condition.type: - flat.extend(_flattened(inner) for inner in c.conditions) - else: - flat.append(_flattened(c)) - conditions = [c for c in flat if c is not None] - match len(conditions): - case 0: - return None - case 1: - return conditions[0] - case _: - return type(condition)(conditions=tuple(conditions)) diff --git a/src/zero_server/query/builder.py b/src/zero_server/query/builder.py index 9e8feac..579e8ec 100644 --- a/src/zero_server/query/builder.py +++ b/src/zero_server/query/builder.py @@ -1,9 +1,13 @@ """The ZQL query builder: a `Query` is an immutable AST under construction, in client names. -Ported from rocicorp/mono `packages/zql/src/query/query-impl.ts` and `create-builder.ts` at zero/v1.10.0 -(905d08516ba0caaf314cd28e93a5a47e8acdd482). Apache-2.0, Copyright Rocicorp. +Ported from rocicorp/mono `packages/zql/src/query/query-impl.ts` and `create-builder.ts` at zero/v1.11.3 +(e91e55f05b7a2c631f77238e1dd6fadf1029a538). Apache-2.0, Copyright Rocicorp. The errors match upstream's message for message, because a query endpoint reports them to the client. + +A query's AST is always normalized, as upstream's is: each step keeps it so, sorting a new `related` +into place and normalizing the conditions of a `where`. Upstream also interns queries, so that +rebuilding the same chain hands back the same objects; that only saves work, and is not ported. """ import math @@ -26,6 +30,8 @@ Direction, SimpleCondition, System, + insert_related, + normalize_condition, ) from zero_server.query.expression import MISSING, ExpressionBuilder, _position, and_, cmp, simplify_condition from zero_server.schema import Connection, Schema @@ -128,8 +134,7 @@ def _with[Q: Query[Any, Any]](self: Q, field: str, value: Any, format: Format | def _with_relationship[Q: Query[Any, Any]](self: Q, name: str, sub: AnyQuery, entry: CorrelatedSubquery) -> Q: format = Format(self._format.singular, {**self._format.relationships, name: sub._format}) - related = self._ast.related - return self._with("related", (*related, entry) if related else (entry,), format) + return self._with("related", insert_related(self._ast.related, entry), format) def one(self) -> "OneQuery[R, C]": """At most one row: `tx.run` returns it, or `None`.""" @@ -155,7 +160,7 @@ def where[Q: Query[Any, Any]]( cond = field_or_factory(self.expression_builder()) if self._ast.where is not None: cond = and_(self._ast.where, cond) - return self._with("where", simplify_condition(cond)) + return self._with("where", normalize_condition(simplify_condition(cond))) def where_exists[Q: Query[Any, Any]]( self: Q, @@ -301,6 +306,7 @@ def _exists( subquery=AST( table=first.dest_schema, alias=f"{SUBQ_PREFIX}{relationship}", + # A single condition, which needs no normalizing. where=CorrelatedSubqueryCondition( related=CorrelatedSubquery( system=self._system, diff --git a/src/zero_server/schema.py b/src/zero_server/schema.py index 5912b40..9ade04c 100644 --- a/src/zero_server/schema.py +++ b/src/zero_server/schema.py @@ -93,7 +93,7 @@ def __init__(self, tables: dict[str, DestNames]) -> None: def _table(self, src: str) -> DestNames: table = self._tables.get(src) if table is None: - raise Exception(f'unknown table "{src}" ') + raise Exception(f'unknown table "{src}"') return table def table_name(self, src: str) -> str: @@ -113,7 +113,7 @@ def _column_name(self, table: str, src: str) -> str: src = src if isinstance(src, str) else js_string(src) dest = self._table(table).columns.get(src) if not dest: - raise Exception(f'unknown column "{src}" of "{table}" table ') + raise Exception(f'unknown column "{src}" of "{table}" table') return dest diff --git a/src/zero_server/server/pg_types.py b/src/zero_server/server/pg_types.py index 14dee7d..3abbde4 100644 --- a/src/zero_server/server/pg_types.py +++ b/src/zero_server/server/pg_types.py @@ -1,7 +1,7 @@ """Postgres column types as Zero sees them. -Ported from rocicorp/mono `packages/zero-cache/src/types/pg-data-type.ts` at zero/v1.10.0 -(905d08516ba0caaf314cd28e93a5a47e8acdd482). Apache-2.0, Copyright Rocicorp. +Ported from rocicorp/mono `packages/zero-cache/src/types/pg-data-type.ts` at zero/v1.11.3 +(e91e55f05b7a2c631f77238e1dd6fadf1029a538). Apache-2.0, Copyright Rocicorp. """ import functools @@ -47,6 +47,11 @@ def is_native_string(pg_type: str) -> bool: return _lookup(pg_type) in NATIVE_STRING +def is_bpchar(pg_type: str) -> bool: + """`character(n)`, also known as `bpchar`.""" + return _lookup(pg_type) in ("bpchar", "character") + + def is_text_represented(pg_type: str) -> bool: return _lookup(pg_type) in TEXT_REPRESENTED diff --git a/src/zero_server/server/server_schema.py b/src/zero_server/server/server_schema.py index a61c90c..b20d89f 100644 --- a/src/zero_server/server/server_schema.py +++ b/src/zero_server/server/server_schema.py @@ -110,6 +110,9 @@ async def get_server_schema(tx: DBTransaction[object], schema: Schema) -> Server type_ = row["typename"] if type_ in ("bpchar", "character", "character varying", "varchar") and row["length"]: type_ = f"{type_}({row['length']})" + elif type_ == "character": + # Without a length `character` means `character(1)`, so an unbounded `bpchar` is `bpchar`. + type_ = "bpchar" if type_ in ("numeric", "decimal") and row["precision"] is not None and row["scale"] is not None: type_ = f"{type_}({row['precision']}, {row['scale']})" table[row["column"]] = ServerColumnSchema(type=type_, is_enum=is_enum, is_array=is_array) diff --git a/src/zero_server/z2s/compiler.py b/src/zero_server/z2s/compiler.py index 98e4737..84a5061 100644 --- a/src/zero_server/z2s/compiler.py +++ b/src/zero_server/z2s/compiler.py @@ -1,7 +1,7 @@ """A ZQL AST compiled to one Postgres statement that returns the whole result as JSON. Ported from rocicorp/mono `packages/z2s/src/compiler.ts` and `packages/zql/src/query/complete-ordering.ts` -at zero/v1.10.0 (905d08516ba0caaf314cd28e93a5a47e8acdd482). Apache-2.0, Copyright Rocicorp. +at zero/v1.11.3 (e91e55f05b7a2c631f77238e1dd6fadf1029a538). Apache-2.0, Copyright Rocicorp. Relationships nest as `json_agg` subqueries, so the statement returns one row whose `zql_result` column holds the rows the client-side query would have produced, in client names. @@ -422,7 +422,15 @@ def _simple(self, condition: SimpleCondition, table: _Table) -> str | None: rhs = self._value(right, table, left, plural=False) distinct = "IS NOT DISTINCT FROM" if condition.op == "IS" else "IS DISTINCT FROM" return f"{lhs} {distinct} {rhs}" - case "=" | "!=" | "<" | ">" | "<=" | ">=" | "LIKE" | "NOT LIKE" | "ILIKE" | "NOT ILIKE": + case "LIKE" | "NOT LIKE" | "ILIKE" | "NOT ILIKE": + # The pattern is bound as a string, not as the column's type: as a `char(n)` column's + # `bpchar` it would lose its trailing spaces. Any other value matches as its string. + check(isinstance(right, LiteralReference), _STATIC) + lhs = self._value(left, table, right, plural=False) + pattern = right.value # ty: ignore[unresolved-attribute] + rhs = self.params.null() if pattern is None else self.params.literal(js_string(pattern)) + return f"{lhs} {condition.op} {rhs}" + case "=" | "!=" | "<" | ">" | "<=" | ">=": lhs = self._value(left, table, right, plural=False) rhs = self._value(right, table, left, plural=False) return f"{lhs} {condition.op} {rhs}" @@ -468,7 +476,7 @@ def _literal( # a non-null scalar here. An empty list takes its element type from it. return self.params.plural_literal(literal_type(value[0] if value else other.value), value) if not isinstance(value, str | int | float): - raise Exception(f"Literal of unexpected type. {js_string(value)} of type object") + raise Exception("Literal of unexpected type: object") return self.params.literal(value) diff --git a/src/zero_server/z2s/sql.py b/src/zero_server/z2s/sql.py index f33dc43..f28d84e 100644 --- a/src/zero_server/z2s/sql.py +++ b/src/zero_server/z2s/sql.py @@ -1,7 +1,7 @@ """Identifiers and bound values for the SQL that z2s and `tx.mutate` write. Ported from rocicorp/mono `packages/z2s/src/sql.ts` (`formatPgInternalConvert` and its argument -conversions) at zero/v1.10.0 (905d08516ba0caaf314cd28e93a5a47e8acdd482). Apache-2.0, Copyright Rocicorp. +conversions) at zero/v1.11.3 (e91e55f05b7a2c631f77238e1dd6fadf1029a538). Apache-2.0, Copyright Rocicorp. Every value is bound as text and cast in SQL, so the driver never coerces it: a number is its JSON text, a timestamp epoch milliseconds, a JSON value its `JSON.stringify` output. @@ -16,7 +16,7 @@ from collections.abc import Sequence from typing import Any, Literal -from zero_server.server.pg_types import is_native_string, is_number, is_string, is_text_represented +from zero_server.server.pg_types import is_bpchar, is_native_string, is_number, is_string, is_text_represented from zero_server.server.server_schema import ServerColumnSchema from zero_server.shared.js import js_string, js_stringify @@ -95,8 +95,11 @@ def _cast(placeholder: str, column: ServerColumnSchema, comparison: bool) -> str if column.is_enum: return f'{placeholder}::text::"{column.type}"' if is_native_string(column.type): - # Compared as plain text, so a value longer than a varchar column still compares. - return f"{placeholder}::text" if comparison else f"{placeholder}::text::{column.type}" + if not comparison: + return f"{placeholder}::text::{column.type}" + # Compared as an unbounded type, so a value longer than the column still compares. A `char(n)` + # column's is `bpchar`: compared with `text`, Postgres casts the column and can't use its indexes. + return f"{placeholder}::text::bpchar" if is_bpchar(column.type) else f"{placeholder}::text" if is_text_represented(column.type): return f"{placeholder}::text::{column.type}" if is_number(column.type): @@ -217,7 +220,11 @@ def literal(self, value: str | int | float | bool) -> str: """A scalar compared with another literal, typed by its own JSON type.""" kind = literal_type(value) index = self._bind(value, kind, value if isinstance(value, str) else js_stringify(value)) - return f"${index}::text::{_LITERAL_PG_TYPE[kind]}" + return f"${index}::text" if kind == "string" else f"${index}::text::{_LITERAL_PG_TYPE[kind]}" + + def null(self) -> str: + """A literal `null`, untyped.""" + return f"${self._bind(None, 'null', None)}" def plural_literal(self, kind: LiteralType, values: Sequence[Any]) -> str: if kind not in _LITERAL_PG_TYPE: diff --git a/tests/compat_scope.py b/tests/compat_scope.py index 168e042..f57e4f0 100644 --- a/tests/compat_scope.py +++ b/tests/compat_scope.py @@ -31,6 +31,9 @@ _VALITA_UNION = "unreachable: a union shape the ported schemas do not build (optional or unknown options, a " _VALITA_UNION += "literal sharing a type with another option, or an ambiguous discriminator)" +_ZERO_CACHE_PARSE = "unused: zero-cache's parsing of client messages from newer clients; no handler parses with it" +_AST_DEPTH = "unused: the depth bound zero-cache puts on ASTs from clients; the server parses none" + NOT_PORTED: dict[str, dict[str, str]] = { "shared/src/asserts.ts": { "7-56": "unused: assertString through assertNotNull; only `assert` and `unreachable` run in ported code", @@ -42,17 +45,17 @@ "69-79": "JavaScript only: details read off a thrown value that is not an Error", }, "shared/src/json-schema.ts": { - "13-13": "unreachable: `skipAssertJSONValue` is a build flag, false in the published package", - "28-39": "unused: jsonObjectSchema, which no ported schema uses", + "17-17": "unreachable: `skipAssertJSONValue` is a build flag, false in the published package", + "32-43": "unused: jsonObjectSchema, which no ported schema uses", }, "shared/src/json.ts": { - "deepEqual": "unused", + "deepEqual": "client only: only the builder's query interning compares with it", "assertJSONValue": "unused", "assertJSONObject": "unused", "assertObjectIsJSONObject": "unused", "assertJSONArray": "unused", "isJSONObject": "unused: only jsonObjectSchema calls it", - "230-230": "unreachable: an array element that is not JSON, which no decoded body or Python value has", + "237-237": "unreachable: an array element that is not JSON, which no decoded body or Python value has", }, "shared/src/object-traversal.ts": { "iterateLeaves": "unused: only iterateMutators, itself unused, calls it", @@ -62,33 +65,38 @@ "81-84": "unreachable: a missing value always has a path, the property it is missing at", "112-113": "unreachable: an invalid union below the top level; every nested union of the ported " "schemas is told apart by type or by a literal key, so only a top-level union fails as a union", - "115-123": "unreachable: a custom error, which only jsonSchema raises, for a value that is not JSON", - "152-155": "unreachable: a union value JSON.stringify cannot write, which no decoded body is", + "115-116": "unreachable: a custom error, which only jsonSchema raises, for a value that is not JSON", + "145-148": "unreachable: a union value JSON.stringify cannot write, which no decoded body is", "is": "unused", "assert": "unused", - "testOptional": "unused", "instanceOfAbstractType": "unused", "deepPartial": "unused", + "toTerminals": _ZERO_CACHE_PARSE, + "assertUnrecognized": _ZERO_CACHE_PARSE, + "taggedTupleUnion": _ZERO_CACHE_PARSE, + "taggedObjectUnion": _ZERO_CACHE_PARSE, }, "z2s/src/compiler.ts": { "100-100": "unreachable: the builder gives every related subquery an alias", "167-167": "unreachable: the builder completes every ordering with the primary key", "194-194": "unreachable: called only for a query with a start bound", - "561-563": "unreachable: TypeScript's exhaustiveness check after every value position type", - "604-609": "unreachable: the other side is a null literal, whose own comparison fails first with " + "579-581": "unreachable: TypeScript's exhaustiveness check after every value position type", + "622-627": "unreachable: the other side is a null literal, whose own comparison fails first with " "'Literal of unexpected type'", - "625-626": "unreachable: TypeScript's exhaustiveness check after every value position type", + "641-642": "unreachable: TypeScript's exhaustiveness check after every value position type", }, "z2s/src/sql.ts": { - "64-64": "unreachable: a null literal fails in literalValueComparison before it becomes an argument", - "171-175": "unreachable: an argument of type null, which only a null literal would make", - "337-343": "unused: identifiers that are not strings, which no ported query builds", + "347-355": "unused: identifiers that are not strings, which no ported query builds", }, "zero-cache/src/services/mutagen/error.ts": { "10-11": "unreachable: the assertion message, built only when received >= actual, which the caller rules out", }, "zero-protocol/src/ast.ts": { - "515-515": "unreachable: after flattening, a compound condition's compound siblings share one type", + "813-813": "unreachable: after flattening, a compound condition's compound siblings share one type", + "exceedsASTDepth": _AST_DEPTH, + "astExceedsDepth": _AST_DEPTH, + "conditionExceedsDepth": _AST_DEPTH, + "274-276": _AST_DEPTH, "compareUTF8MaybeNull": "unreachable: the null arms; every op and type it compares is a string", }, "zero-protocol/src/error.ts": { @@ -142,12 +150,10 @@ "query internals, which carries no details", }, "zero-server/src/schema.ts": { - "205-205": "unreachable: called only when there is an incompatibility to report", + "209-209": "unreachable: called only when there is an incompatibility to report", }, "zero-types/src/name-mapper.ts": { - "26-26": "unused: the context arm; no caller passes a context", "tableNameIfKnown": "unused", - "45-45": "unused: the context arm; no caller passes a context", "row": "unused: only mapCRUD calls it", "columns": "unused: only mapCRUD calls it", }, @@ -172,11 +178,16 @@ "createRunnableBuilder": "deprecated: builds `tx.query`, the legacy query builder", }, "zql/src/query/query-impl.ts": { - "135-155": "client only: run, preload and materialize, which throw on a query that is not runnable; " + "newQueryImpl": "unused: a query from an AST built elsewhere; the server builds every query itself", + "150-152": "unused: reusing an interned root query, which only saves work; the harness builds each " + "program from a fresh schema so that no program's result depends on the ones before it", + "326-347": "client only: run, preload and materialize, which throw on a query that is not runnable; " "the server runs a query with `tx.run`", "hash": "client only: the client caches views by AST hash; the server sends ASTs unhashed", - "352-355": "unreachable: a relationship has one hop or two, which the schema builder enforces", - "564-567": "unreachable: a relationship has one hop or two, which the schema builder enforces", + "392-425": "client only: #canonicalize and #sameAs, which only hash calls", + "616-619": "unreachable: a relationship has one hop or two, which the schema builder enforces", + "895-898": "unreachable: a relationship has one hop or two, which the schema builder enforces", + "1090-1091": "unreachable: TypeScript's exhaustiveness check after every condition type", "throwQueryNotRunnable": "client only: called only by run, preload and materialize", }, "zql/src/query/query-registry.ts": { diff --git a/tests/unit/test_protocol.py b/tests/unit/test_protocol.py index 538cfb4..070432f 100644 --- a/tests/unit/test_protocol.py +++ b/tests/unit/test_protocol.py @@ -115,22 +115,6 @@ def column_name(self, table: str, column: str) -> str: return column.upper() -def test_normalize_refuses_unresolved_parameters() -> None: - param = {"type": "simple", "op": "=", "left": {"type": "static", "anchor": "authData", "field": "sub"}} - ast = { - "table": "t", - "where": { - "type": "and", - "conditions": [ - {**param, "right": {"type": "literal", "value": 1}}, - {**param, "right": {"type": "literal", "value": 2}}, - ], - }, - } - with pytest.raises(ValueError, match="Static parameters"): - normalize_ast(AST.from_json(ast)) - - def test_normalize_orders_and_before_or() -> None: pair = [_eq("a", 1).to_json(), _eq("b", 1).to_json()] ast = { diff --git a/tests/wire/seed.py b/tests/wire/seed.py index adff0fe..0aaf18f 100644 --- a/tests/wire/seed.py +++ b/tests/wire/seed.py @@ -26,7 +26,7 @@ "CREATE SCHEMA extra", """CREATE TABLE extra.gadgets ( id TEXT PRIMARY KEY, owner_id TEXT NOT NULL, serial UUID NOT NULL, ip INET NOT NULL, amount NUMERIC(10, 2), - code VARCHAR(4), mood mood, moods mood[], seen TIMESTAMPTZ)""", + code VARCHAR(4), mood mood, moods mood[], seen TIMESTAMPTZ, tag CHAR(3), pad BPCHAR)""", ] TEXTS = ["", "a", "b", "ab", "A", "é", "x%", "a_b", "Ab", "0", "1"] @@ -43,6 +43,9 @@ ("gadget", "ip"): ["10.0.0.1", "10.0.0.1/32", "192.168.0.0/16", "::1", "10.0.0.2"], ("gadget", "amount"): [0, 1.5, 1.25, 10, -3.333, 99999999.99], ("gadget", "code"): ["", "ab", "abcd", "abcde", "AB"], + # `char(3)` pads with spaces and ignores trailing ones when it compares; a LIKE pattern keeps them. + ("gadget", "tag"): ["ab", "abc", "ab ", "a%", "a_ ", "abcd"], + ("gadget", "pad"): ["x", "x ", "xy", "% "], ("gadget", "mood"): ["happy", "sad", "meh"], ("gadget", "moods"): [[], ["happy"], ["sad", "happy"], ["meh"]], ("gadget", "seen"): [0, 946_684_800_000, 946_684_800_000.5, 1_577_836_800_000], @@ -125,6 +128,8 @@ def maybe(value: Any) -> Any: "mood": maybe(rng.choice(["happy", "sad"])), "moods": maybe(rng.choice(COLUMN_VALUES["gadget", "moods"][:-1])), "seen": maybe(rng.choice(COLUMN_VALUES["gadget", "seen"]) / 1000), + "tag": maybe(rng.choice(COLUMN_VALUES["gadget", "tag"][:3])), + "pad": maybe(rng.choice(COLUMN_VALUES["gadget", "pad"][:3])), } for n in range(8) ], diff --git a/tests/wire/test_ast.py b/tests/wire/test_ast.py index 71f6c0b..b6cf18a 100644 --- a/tests/wire/test_ast.py +++ b/tests/wire/test_ast.py @@ -47,16 +47,8 @@ def _eq(left: dict[str, Any]) -> dict[str, Any]: } ) def test_normalizes_like_upstream(harness: Harness, value: Any) -> None: - try: - expected: Any = harness.call("normalizeAST", ast=value) - except HarnessError as e: - expected = e - if isinstance(expected, HarnessError): - assert "Static parameters should be resolved" in str(expected) - with pytest.raises(ValueError, match="Static parameters should be resolved before normalization"): - normalize_ast(AST.from_json(value)) - else: - assert as_js(normalize_ast(AST.from_json(value)).to_json()) == as_js(expected) + expected = harness.call("normalizeAST", ast=value) + assert as_js(normalize_ast(AST.from_json(value)).to_json()) == as_js(expected) def _mapper(tables: dict[str, Any]) -> NameMapper: diff --git a/tests/wire/test_query.py b/tests/wire/test_query.py index d0ce215..8dad0c7 100644 --- a/tests/wire/test_query.py +++ b/tests/wire/test_query.py @@ -157,6 +157,25 @@ def _q(table: str, *ops: list[Any]) -> dict[str, Any]: _q("user", ["related", "manager", {"ops": [["related", "reports", None]]}]), _q("issue", ["related", "label", None], ["whereExists", "label", None, None]), _q("gadget", ["where", "serial", "IN", []], ["related", "owner", None]), + # The builder keeps its AST normalized: relationships sorted by alias, conditions by their values. + _q("issue", ["related", "owner", None], ["related", "comments", None], ["related", "labels", None]), + _q("issue", ["where", "priority", ">", 2], ["where", "priority", ">", 1], ["where", "priority", ">", 1]), + _q("issue", ["where", "open", True], ["where", "open", False]), + _q( + "issue", + ["where", "priority", "IN", [1, 3]], + ["where", "priority", "IN", [1, 2]], + ["where", "priority", "IN", [1]], + ), + _q("issue", *[["where", "meta", v] for v in ([1], "x", 1, True, {"a": 1}, [True], ["a"], [1, 2])]), + _q( + "issue", + *[ + ["where", "title", {"$param": p}] + for p in (["authData", "b"], ["authData", "a"], ["authData", ["a", "b"]], ["preMutationRow", "a"]) + ], + ["where", "title", "a"], + ), # Two unknown columns: upstream maps a query's own condition before its relationships, at every level. _q("comment", ["where", "nope", None], ["related", "author", {"ops": [["where", "nope", None]]}]), _q("issue", ["related", "labels", {"ops": [["where", "nope", None]]}], ["where", "nope", None]), diff --git a/tests/wire/test_sql.py b/tests/wire/test_sql.py index 4dcc737..a1d2d0a 100644 --- a/tests/wire/test_sql.py +++ b/tests/wire/test_sql.py @@ -107,6 +107,13 @@ def _q(table: str, *ops: list[Any]) -> dict[str, Any]: _q("gadget", ["where", "ip", "10.0.0.1/32"], ["where", "amount", ">", 1.25]), _q("gadget", ["where", "code", "<", "abcde"], ["where", "mood", "IN", ["happy", "meh"]]), _q("gadget", ["where", "moods", "=", ["happy"]]), + # A `char(n)` column compares as `bpchar`, which ignores trailing spaces, but matches a LIKE pattern as + # text, which keeps them. A pattern that is not a string matches as its string. + _q("gadget", ["where", "tag", "ab "], ["where", "pad", "IN", ["x", "xy "]]), + _q("gadget", ["where", "tag", "LIKE", "ab "], ["where", "pad", "NOT ILIKE", "X%"]), + _q("gadget", ["where", "tag", "LIKE", None]), + _q("gadget", ["where", "tag", "NOT LIKE", 1]), + _q("gadget", ["where", "tag", "ILIKE", ["a", "b"]]), _q("gadget", ["where", "seen", ">=", 946_684_800_000.5], ["orderBy", "seen", "desc"]), _q("gadget", ["orderBy", "serial", "asc"], ["start", {"serial": "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"}, True]), _q("gadget", ["orderBy", "amount", "desc"], ["start", {"amount": 1.5, "id": "g3"}, False]), From 8feae46dc6eba71a80f5ac41a2c8b6e3c986d66b Mon Sep 17 00:00:00 2001 From: Jonas Daniels Date: Wed, 28 Oct 2026 13:32:16 -0700 Subject: [PATCH 3/6] perf: skip rebuilding conditions that are already normalized Co-Authored-By: Claude Opus 5.5 --- src/zero_server/protocol/ast.py | 65 +++++++++++++++++---------------- 1 file changed, 33 insertions(+), 32 deletions(-) diff --git a/src/zero_server/protocol/ast.py b/src/zero_server/protocol/ast.py index 8e88b66..53badee 100644 --- a/src/zero_server/protocol/ast.py +++ b/src/zero_server/protocol/ast.py @@ -454,18 +454,28 @@ def normalize_condition(condition: Condition) -> Condition: condition becomes that condition. An empty one stays: an empty `and` is always true and an empty `or` always false. """ - if isinstance(condition, SimpleCondition | CorrelatedSubqueryCondition): - return condition kind = type(condition) + if kind is not Conjunction and kind is not Disjunction: + return condition + original = condition.conditions # ty: ignore[unresolved-attribute] conditions: list[Condition] = [] - for c in condition.conditions: - n = normalize_condition(c) - if type(n) is kind: - conditions.extend(n.conditions) # ty: ignore[unresolved-attribute] - else: - conditions.append(n) + changed = False + for c in original: + t = type(c) + if t is Conjunction or t is Disjunction: + n = normalize_condition(c) + if type(n) is kind: + conditions.extend(n.conditions) # ty: ignore[unresolved-attribute] + changed = True + continue + changed = changed or n is not c + c = n + conditions.append(c) if len(conditions) == 1: return conditions[0] + # Already in order, which is when the sort would leave it as it is: the condition stands as built. + if not changed and all(_cmp_condition(conditions[i], conditions[i - 1]) >= 0 for i in range(1, len(conditions))): + return condition return kind(conditions=tuple(js_sort(conditions, _cmp_condition))) @@ -475,10 +485,10 @@ def insert_related( """`related` with `entry` added where it keeps the entries sorted, after any it compares equal to.""" if related is None: return (entry,) - i = len(related) + n = i = len(related) while i > 0 and _cmp_related(related[i - 1], entry) > 0: i -= 1 - return (*related[:i], entry, *related[i:]) + return (*related, entry) if i == n else (*related[:i], entry, *related[i:]) def _sorted_related(related: list[CorrelatedSubquery]) -> tuple[CorrelatedSubquery, ...]: @@ -488,32 +498,26 @@ def _sorted_related(related: list[CorrelatedSubquery]) -> tuple[CorrelatedSubque return tuple(related) -def _utf8(value: str) -> bytes: - return value.encode("utf-8", "surrogatepass") - - def _cmp(a: object, b: object) -> int: return (a > b) - (a < b) # ty: ignore[unsupported-operator] -def _cmp_utf8(a: str, b: str) -> int: - return _cmp(_utf8(a), _utf8(b)) +# Upstream's `compareUTF8` orders strings by their UTF-8 bytes, a lone surrogate by its own value. That is +# code point order, which is how Python compares strings. +_cmp_utf8 = _cmp def _cmp_value_position( a: LiteralReference | ColumnReference | Parameter, b: LiteralReference | ColumnReference | Parameter ) -> int: - if a.type != b.type: + kind = type(a) + if kind is not type(b): return _cmp_utf8(a.type, b.type) - match a, b: - case LiteralReference(), LiteralReference(): - return _cmp_literal(a.value, b.value) - case ColumnReference(), ColumnReference(): - return _cmp_utf8(a.name, b.name) - case _: - assert isinstance(a, Parameter) - assert isinstance(b, Parameter) - return _cmp_utf8(a.anchor, b.anchor) or _cmp_literal(a.field, b.field) + if kind is ColumnReference: + return _cmp_utf8(a.name, b.name) # ty: ignore[unresolved-attribute] + if kind is LiteralReference: + return _cmp_literal(a.value, b.value) # ty: ignore[unresolved-attribute] + return _cmp_utf8(a.anchor, b.anchor) or _cmp_literal(a.field, b.field) # ty: ignore[unresolved-attribute] def _literal_type(value: object) -> int: @@ -549,13 +553,10 @@ def _cmp_literal(a: Any, b: Any) -> int: def _cmp_related(a: CorrelatedSubquery, b: CorrelatedSubquery) -> int: - return _cmp_utf8(_must(a.subquery.alias), _must(b.subquery.alias)) - - -def _must(alias: str | None) -> str: - if alias is None: + left, right = a.subquery.alias, b.subquery.alias + if left is None or right is None: raise AssertionFailedError("Unexpected undefined value") - return alias + return (left > right) - (left < right) def _cmp_optional_bool(a: bool | None, b: bool | None) -> int: From 84941eca7e159ccc86aa6809a115ab6a3b9a778b Mon Sep 17 00:00:00 2001 From: Jonas Daniels Date: Wed, 28 Oct 2026 13:32:31 -0700 Subject: [PATCH 4/6] docs: pin 1.11.3 in the README and note upstream's query interning Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- docs/compat.md | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2a45dca..8879429 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A community Python server SDK for [Zero](https://zero.rocicorp.dev/). It is not It ports what `@rocicorp/zero/server` gives a TypeScript backend: the query endpoint (`ZERO_QUERY_URL`), the mutate endpoint (`ZERO_MUTATE_URL`), the ZQL query builder, `defineQueries` and `defineMutators`, and `tx.mutate` and `tx.run` on Postgres. The client and `zero-cache` stay in TypeScript, and a Python backend serves the same TypeScript client a TypeScript backend would. -Compatibility is pinned to one Zero release at a time. This version is tested against `@rocicorp/zero` **1.10.0** (see `ZERO_VERSION`). +Compatibility is pinned to one Zero release at a time. This version is tested against `@rocicorp/zero` **1.11.3** (see `ZERO_VERSION`). ## Usage diff --git a/docs/compat.md b/docs/compat.md index 85d2233..fb023c0 100644 --- a/docs/compat.md +++ b/docs/compat.md @@ -67,6 +67,11 @@ These are the cases where the proof holds Python to something narrower than "ide - **Two failures in one query batch.** Upstream runs a batch's handlers together under `Promise.all`, and so does Python. When two of them fail the whole batch, which failure is reported depends on how each runtime orders its promise callbacks, and the two runtimes can disagree. +- **Interned queries.** Upstream's builder hands back the query it built before when a step repeats one, + keyed so that some values JavaScript's types rule out share a key: a `flip` of `'yes'` with `true`, any + `orderBy` direction but `'asc'` with `'desc'`. In one process, such a value can come back as whichever + was built first. Python builds what it is given. The harness gives each program a fresh schema, which + upstream interns by, so that no comparison depends on the programs before it. - **The JavaScript object model.** Upstream's registries also resolve `~`, their marker key, and names every object inherits such as `toString`. A Python registry has neither. - **The runtime.** The oracle runs on Node 24, and JSON parse messages are V8's. Bun or Deno word them From 862b59351019e6aaef9501ded1ad1fd52232f221 Mon Sep 17 00:00:00 2001 From: Jonas Daniels Date: Wed, 28 Oct 2026 13:34:39 -0700 Subject: [PATCH 5/6] feat: port the 1.11.3 mutate side Mutator return values reach the client as result data and are written to the mutations table. tx.retry_on() re-runs a failed mutator in a fresh transaction, at most 5 runs per push, and the mutator's tx stops working once its transaction ends. Adapters take an isolation level. ZQLDatabase.run reads without a transaction when the connection has a bare query, and the server schema fetch is shared while in flight and not cached on failure. Transform requests parse in strip mode. Co-Authored-By: Claude Opus 5.5 --- ...age.MacBook-Pro-21_local.pid15427.Xwc66hOx | Bin 192512 -> 0 bytes README.md | 3 + docs/compat.md | 3 + harness/mutate.ts | 199 +++++++++++++++++- harness/ops.ts | 19 +- src/zero_server/__init__.py | 4 + src/zero_server/integrations/starlette.py | 4 +- src/zero_server/mutators.py | 13 +- src/zero_server/protocol/custom_queries.py | 10 +- src/zero_server/server/adapters/asyncpg.py | 32 +-- src/zero_server/server/adapters/psycopg.py | 44 +++- src/zero_server/server/adapters/sqlalchemy.py | 44 ++-- src/zero_server/server/pg_query_executor.py | 4 +- src/zero_server/server/process_mutations.py | 94 ++++++++- src/zero_server/server/process_queries.py | 6 +- src/zero_server/server/push_processor.py | 15 +- src/zero_server/server/server_schema.py | 4 +- src/zero_server/server/transaction.py | 75 ++++++- src/zero_server/server/transaction_options.py | 16 ++ src/zero_server/server/zql_database.py | 80 +++++-- src/zero_server/shared/valita.py | 13 +- tests/compat_scope.py | 28 +-- tests/unit/test_api.py | 11 + tests/wire/conftest.py | 87 +++++++- tests/wire/mutators.py | 103 ++++++++- tests/wire/test_mutate.py | 114 +++++++++- tests/wire/test_requests.py | 4 +- 27 files changed, 895 insertions(+), 134 deletions(-) delete mode 100644 .coverage.MacBook-Pro-21_local.pid15427.Xwc66hOx create mode 100644 src/zero_server/server/transaction_options.py diff --git a/.coverage.MacBook-Pro-21_local.pid15427.Xwc66hOx b/.coverage.MacBook-Pro-21_local.pid15427.Xwc66hOx deleted file mode 100644 index ef8900b9854c499b943ba4efdec4d280ddc78c99..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 192512 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 diff --git a/README.md b/README.md index 2a45dca..1d5fa95 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,9 @@ app = Starlette( Point `ZERO_QUERY_URL` and `ZERO_MUTATE_URL` at `/query` and `/mutate`. With FastAPI, `app.router.routes.extend(zero_routes(...))`. With another framework, call `handle_query_request` and `handle_mutate_request` yourself; `zero_server/integrations/starlette.py` is short enough to copy. The types to annotate those handlers (`QueryRequestHandler`, `TransactFn`, `MutateResponse`, `MutationResponse`, `ServerPushFailed` and the rest) import from `zero_server`, and `server_ast(query, schema)` returns the AST zero-cache receives for a query. - **Databases.** `SQLAlchemyDatabase` (SQLAlchemy's asyncio engine on asyncpg or psycopg; the `sqlalchemy` extra installs neither driver), `AsyncpgDatabase` (an asyncpg pool) and `PsycopgDatabase` (a psycopg 3 async pool), under `zero_server.server.adapters`. A push's mutations run on one connection from the pool, taken at its first transaction and returned when the push ends. So mutators in one push share session state (a `SET` without `LOCAL`, session advisory locks, temp tables), and a handler that takes a second connection from the same pool while its push holds one can deadlock once the pool has fewer connections than there are concurrent pushes. Another driver plugs into `ZQLDatabase` as a `DBConnection` whose `query` takes `$1` placeholders. +- **Mutator results.** What a mutator returns, if not `None`, reaches the client as the mutation's result data, and is also written to zero-cache's `mutations` table so it survives a lost response. Python has one `None`, so a mutator cannot return JSON `null` as data the way a TypeScript mutator can. +- **Re-running a mutator.** `tx.retry_on(predicate)` asks `predicate(error, attempt)`, sync or async, whether to run the mutator again in a fresh transaction when the run fails, including at `COMMIT`. Only `True` means yes. A mutator runs at most 5 times per push, and `tx` is unusable once its transaction has ended, inside the predicate too. +- **Isolation level.** Each adapter takes `isolation_level="read committed" | "repeatable read" | "serializable"` for the transactions mutations run in. Unset, the database's default applies. - **Validation.** `@define_query(Model)` and `@define_mutator(Model)` parse args with a Pydantic model or `TypeAdapter` first; a failure reaches the client as upstream's validation error. - **Strict building.** `create_builder(schema, strict=True)` refuses an unknown column when the query is built. Upstream only refuses it when the endpoint maps the query, failing the whole batch, so this is for your tests. diff --git a/docs/compat.md b/docs/compat.md index 85d2233..ad8ba82 100644 --- a/docs/compat.md +++ b/docs/compat.md @@ -67,6 +67,9 @@ These are the cases where the proof holds Python to something narrower than "ide - **Two failures in one query batch.** Upstream runs a batch's handlers together under `Promise.all`, and so does Python. When two of them fail the whole batch, which failure is reported depends on how each runtime orders its promise callbacks, and the two runtimes can disagree. +- **A mutator returning `null`.** Upstream records a mutator's return value as result data unless it is + `undefined`, so a TypeScript mutator can return `null` as data. Python has only `None`, which the port + takes as returning nothing, as the tests' mutators never return `null`. - **The JavaScript object model.** Upstream's registries also resolve `~`, their marker key, and names every object inherits such as `toString`. A Python registry has neither. - **The runtime.** The oracle runs on Node 24, and JSON parse messages are V8's. Bun or Deno word them diff --git a/harness/mutate.ts b/harness/mutate.ts index c27226f..9b6bff8 100644 --- a/harness/mutate.ts +++ b/harness/mutate.ts @@ -16,12 +16,16 @@ import { ApplicationError, handleMutateRequest, PushProcessor, + ZQLDatabase, type Database, + type DBConnection, + type IsolationLevel, + type RetryPredicate, type MutateRequestHandler, type TransactionProviderHooks, type TransactionProviderInput, } from '@rocicorp/zero/server'; -import {zeroPostgresJS} from '@rocicorp/zero/server/adapters/postgresjs'; +import {PostgresJSConnection, zeroPostgresJS} from '@rocicorp/zero/server/adapters/postgresjs'; import postgres from 'postgres'; import {shapes} from './errors.ts'; @@ -55,12 +59,27 @@ export const wireSchema = createSchema({tables: [item, thing, pair]}); const zql = createBuilder(wireSchema); export type Tx = { - dbTransaction: {query(sql: string, params: unknown[]): Promise}; + clientID: string; + mutationID: number; + dbTransaction: {query(sql: string, params: unknown[]): Promise; wrappedTransaction: unknown}; run(query: unknown): Promise; + retryOn(shouldRetry: RetryPredicate): void; // oxlint-disable-next-line no-explicit-any mutate: any; }; -export type Args = {id?: string; value?: string; message?: string; details?: unknown; table?: string; row?: unknown}; +export type Args = { + id?: string; + value?: string; + message?: string; + details?: unknown; + table?: string; + row?: unknown; + ret?: unknown; + predicates?: string[]; + fail?: number; + db?: boolean; + run?: boolean; +}; const PUT = `INSERT INTO item (id, value) VALUES ($1, $2) ON CONFLICT (id) DO UPDATE SET value = item.value || '+' || excluded.value`; @@ -69,7 +88,37 @@ export async function put(tx: Tx, args: Args) { await tx.dbTransaction.query(PUT, [String(args.id ?? 'x'), String(args.value ?? '')]); } -export const mutators: Record Promise> = { +// How many times item.retry has run for each mutation, by `clientID:mutationID`. Reset per test example. +const runs = new Map(); +// A transaction item.keep held on to after it ended. +let kept: Tx | undefined; + +export function resetRuns() { + runs.clear(); + kept = undefined; + return null; +} + +/** The tx.retryOn() predicates item.retry registers, by name; tests/wire/mutators.py has their twins. */ +const predicates: Record = { + yes: () => true, + no: () => false, + later: async () => { + await new Promise(resolve => setTimeout(resolve, 1)); + return true; + }, + throw: () => { + throw new Error('predicate broke'); + }, + reject: async () => { + throw new Error('predicate rejected'); + }, + truthy: () => 1 as never, + firstOnly: (_error, attempt) => attempt === 1, + matches: error => error instanceof Error && error.message.includes('failed'), +}; + +export const mutators: Record Promise> = { 'item.put': put, 'item.appError': async (tx, args) => { await put(tx, args); @@ -102,6 +151,45 @@ export const mutators: Record Promise> = { 'item.dbError': async tx => { await tx.dbTransaction.query('SELECT * FROM missing_table', []); }, + // Whatever `ret` holds is the mutation's result data; without it, there is none. + 'item.return': async (tx, args) => { + await put(tx, args); + return args.ret; + }, + // Fails its first `fail` runs, after registering `predicates`, and returns the number of the run. + 'item.retry': async (tx, args) => { + for (const name of args.predicates ?? []) { + tx.retryOn(predicates[name]); + } + await put(tx, args); + const key = `${tx.clientID}:${tx.mutationID}`; + const run = (runs.get(key) ?? 0) + 1; + runs.set(key, run); + if (run <= Number(args.fail ?? 0)) { + if (args.db) { + await tx.dbTransaction.query('SELECT * FROM missing_table', []); + } + throw new Error(`run ${run} failed`); + } + return run; + }, + 'item.keep': async (tx, args) => { + kept = tx; + await put(tx, args); + // Still open, so this is the driver's transaction. + return tx.dbTransaction.wrappedTransaction === undefined; + }, + // Uses the transaction item.keep held on to, which has ended. + 'item.useKept': async (_tx, args) => { + if (!kept) throw new Error('nothing kept'); + if (args.db) { + await kept.dbTransaction.query('SELECT 1', []); + } + if (args.run) { + await kept.run(zql.item); + } + return kept.dbTransaction.wrappedTransaction === undefined; + }, ...Object.fromEntries( Object.entries(shapes).map(([shape, make]) => [ `throw.${shape}`, @@ -124,7 +212,7 @@ export const handler: MutateRequestHandler> = async (transact, muta const result = await transact(async (tx, name, mutatorArgs) => { const mutator = mutators[name.replace(/^post\./, 'item.')]; if (!mutator) throw new Error(`unknown mutator ${name}`); - await mutator(tx, (mutatorArgs ?? {}) as Args); + return (await mutator(tx, (mutatorArgs ?? {}) as Args)) as never; }); if (mutation.name.startsWith('post.')) { throw new Error('after commit'); @@ -192,9 +280,12 @@ const registry = defineMutators({ put(tx, {...args, value: `${String(args?.value ?? '')}@${ctx.tag}`}), ), appError: defineMutator(async ({tx, args}: {tx: Tx; args: Args}) => mutators['item.appError'](tx, args ?? {})), + ret: defineMutator(async ({tx, args}: {tx: Tx; args: Args}) => mutators['item.return'](tx, args ?? {})), }, } as never); -const legacy = {legacy: {put: (tx: Tx, args: Args) => put(tx, args ?? {})}}; +const legacy = { + legacy: {put: (tx: Tx, args: Args) => put(tx, args ?? {}), ret: (tx: Tx, args: Args) => mutators['item.return'](tx, args ?? {})}, +}; export function pushProcessor({url, query, body}: {url: string; query: Record; body: unknown}) { return new PushProcessor(provider(url) as never, {tag: 'ctx'}, 'debug').process( @@ -235,3 +326,99 @@ export function mutateWithSchema({ logLevel: 'error', }).finally(() => sql.end()); } + +/** tx.retryOn() on a transaction nothing will re-run: one ZQLDatabase.transaction opens with no input. */ +export async function retryOnUnsupported({url}: {url: string}) { + const sql = postgres(url, {onnotice: () => {}}); + try { + return await zeroPostgresJS(wireSchema, sql).transaction(async tx => { + try { + (tx as unknown as Tx).retryOn(() => true); + return null; + } catch (e) { + return (e as Error).message; + } + }); + } finally { + await sql.end(); + } +} + +/** The isolation level a transaction of zeroPostgresJS runs at, given `level` or none. */ +export async function isolation({url, level}: {url: string; level: IsolationLevel | null}) { + const sql = postgres(url, {onnotice: () => {}}); + try { + return await zeroPostgresJS(wireSchema, sql, level === null ? undefined : {isolationLevel: level}).transaction( + async tx => { + const rows = (await (tx as unknown as Tx).dbTransaction.query('SHOW transaction_isolation', [])) as { + transaction_isolation: string; + }[]; + return rows[0].transaction_isolation; + }, + ); + } finally { + await sql.end(); + } +} + +/** + * `ZQLDatabase.run` over a connection that logs what it is asked to do: with `bare`, one that runs + * statements outside a transaction. With `failFirst`, the first statement fails. With `concurrent`, the + * runs start together, otherwise one after another. Returns each run's rows or error, and the log. + */ +export async function recordedRun({ + url, + bare, + failFirst, + concurrent, + runs: count, +}: { + url: string; + bare: boolean; + failFirst: boolean; + concurrent: boolean; + runs: number; +}) { + const sql = postgres(url, {onnotice: () => {}}); + const inner = new PostgresJSConnection(sql); + const log: string[] = []; + let failing = failFirst; + const record = (event: string, run: () => Promise) => { + log.push(event); + if (failing) { + failing = false; + return Promise.reject(new Error('statement failed')); + } + return run(); + }; + const connection: DBConnection = { + transaction: cb => { + log.push('transaction'); + return inner.transaction(dbTx => { + const wrapped = Object.create(dbTx); + wrapped.query = (q: string, p: unknown[]) => record('query in transaction', () => dbTx.query(q, p)); + return cb(wrapped); + }); + }, + ...(bare ? {query: (q: string, p: unknown[]) => record('query', () => inner.query(q, p))} : {}), + } as DBConnection; + const db = new ZQLDatabase(connection, wireSchema); + const one = async () => { + try { + return {rows: await db.run(zql.item.where('value', '!=', 'x').orderBy('id', 'asc') as never)}; + } catch (e) { + return {error: (e as Error).message}; + } + }; + try { + const results = []; + if (concurrent) { + results.push(...(await Promise.all(Array.from({length: count}, one)))); + } else { + for (let i = 0; i < count; i++) results.push(await one()); + } + return {results, log}; + } finally { + await sql.end(); + } +} diff --git a/harness/ops.ts b/harness/ops.ts index 3a32917..2979e2d 100644 --- a/harness/ops.ts +++ b/harness/ops.ts @@ -11,7 +11,18 @@ import { v, } from './zero.ts'; import {ApplicationError, isApplicationError} from '@rocicorp/zero/server'; -import {mutate, mutateRequest, mutateWithSchema, pushProcessor, pushProcessorRequest, wireSchema} from './mutate.ts'; +import { + isolation, + mutate, + mutateRequest, + mutateWithSchema, + pushProcessor, + pushProcessorRequest, + recordedRun, + resetRuns, + retryOnUnsupported, + wireSchema, +} from './mutate.ts'; import {query, queryRequest, rawQuery, runQuery, schemaJSON} from './query.ts'; import {httpMutate, httpQuery} from './http.ts'; import {registryLookup} from './registry.ts'; @@ -21,7 +32,7 @@ const schemas = { ast: [astSchema, undefined], pushBody: [pushBodySchema, 'passthrough'], mutateParams: [mutateParamsSchema, 'passthrough'], - transformRequest: [transformRequestMessageSchema, undefined], + transformRequest: [transformRequestMessageSchema, 'strip'], cleanupResultsArg: [cleanupResultsArgSchema, undefined], } as const; @@ -41,6 +52,10 @@ export const ops = { pushProcessor, pushProcessorRequest, mutateWithSchema, + resetRuns, + retryOnUnsupported, + isolation, + recordedRun, httpMutate, query, queryRequest, diff --git a/src/zero_server/__init__.py b/src/zero_server/__init__.py index 1d78f31..a16354a 100644 --- a/src/zero_server/__init__.py +++ b/src/zero_server/__init__.py @@ -29,6 +29,8 @@ ) from zero_server.server.process_queries import QueryRequestHandler, handle_query_request, server_ast from zero_server.server.push_processor import PushProcessor +from zero_server.server.transaction import RetryPredicate +from zero_server.server.transaction_options import IsolationLevel from zero_server.shared.json import JSONValue __version__ = "0.0.3" @@ -40,6 +42,7 @@ "CustomMutation", "ErrorReason", "ExpressionBuilder", + "IsolationLevel", "JSONValue", "MutateResponse", "MutateSuccess", @@ -54,6 +57,7 @@ "Query", "QueryParseError", "QueryRequestHandler", + "RetryPredicate", "Schema", "ServerPushFailed", "TransactFn", diff --git a/src/zero_server/integrations/starlette.py b/src/zero_server/integrations/starlette.py index db012c5..f1f0cf8 100644 --- a/src/zero_server/integrations/starlette.py +++ b/src/zero_server/integrations/starlette.py @@ -104,8 +104,8 @@ async def mutate(request: Request) -> Response: user_id, ctx = who async def handler(transact: TransactFn[Any], _mutation: CustomMutation) -> MutationResponse: - async def run(tx: Any, name: str, args: JSONValue | None) -> None: - await must_get_mutator(mutators, name).fn(tx=tx, ctx=ctx, args=args) + async def run(tx: Any, name: str, args: JSONValue | None) -> JSONValue | None: + return await must_get_mutator(mutators, name).fn(tx=tx, ctx=ctx, args=args) return await transact(run) diff --git a/src/zero_server/mutators.py b/src/zero_server/mutators.py index 306501b..d278777 100644 --- a/src/zero_server/mutators.py +++ b/src/zero_server/mutators.py @@ -1,7 +1,7 @@ """`define_mutator` and `define_mutators`: named server mutators the mutate endpoint dispatches to. -Ported from rocicorp/mono `packages/zql/src/mutate/mutator.ts` and `mutator-registry.ts` at zero/v1.10.0 -(905d08516ba0caaf314cd28e93a5a47e8acdd482). Apache-2.0, Copyright Rocicorp. +Ported from rocicorp/mono `packages/zql/src/mutate/mutator.ts` and `mutator-registry.ts` at zero/v1.11.3 +(e91e55f05b7a2c631f77238e1dd6fadf1029a538). Apache-2.0, Copyright Rocicorp. A mutator here is the server half of the TypeScript mutator of the same name: the client runs its own optimistically, and this one runs authoritatively inside the mutation's transaction. @@ -15,8 +15,9 @@ from zero_server.shared.json import JSONValue from zero_server.shared.registry import Registry, build, merge, value_at_path -type MutatorFn = Callable[..., Awaitable[None]] -"""Called as `await fn(args=..., ctx=..., tx=...)`.""" +type MutatorFn = Callable[..., Awaitable[JSONValue | None]] +"""Called as `await fn(args=..., ctx=..., tx=...)`. What it returns, unless `None`, reaches the client as +the mutation's result data.""" @dataclass(frozen=True) @@ -53,9 +54,9 @@ def __init__(self, name: str, definition: MutatorDefinition) -> None: self.mutator_name = name self._definition = definition - async def fn(self, *, args: JSONValue | None = None, ctx: Any = None, tx: Any) -> None: + async def fn(self, *, args: JSONValue | None = None, ctx: Any = None, tx: Any) -> JSONValue | None: validated = validate_input(self.mutator_name, args, self._definition.validator, "mutator") - await self._definition.fn(args=validated, ctx=ctx, tx=tx) + return await self._definition.fn(args=validated, ctx=ctx, tx=tx) def __call__(self, args: JSONValue | None = None) -> MutateRequest: return MutateRequest(mutator=self, args=args) diff --git a/src/zero_server/protocol/custom_queries.py b/src/zero_server/protocol/custom_queries.py index 2f95e70..8a081cf 100644 --- a/src/zero_server/protocol/custom_queries.py +++ b/src/zero_server/protocol/custom_queries.py @@ -1,7 +1,7 @@ """What zero-cache POSTs to a query endpoint, and what each query becomes. -Ported from rocicorp/mono `packages/zero-protocol/src/custom-queries.ts` at zero/v1.10.0 -(905d08516ba0caaf314cd28e93a5a47e8acdd482). Apache-2.0, Copyright Rocicorp. +Ported from rocicorp/mono `packages/zero-protocol/src/custom-queries.ts` at zero/v1.11.3 +(e91e55f05b7a2c631f77238e1dd6fadf1029a538). Apache-2.0, Copyright Rocicorp. """ from dataclasses import dataclass @@ -10,12 +10,12 @@ from pydantic import StrictStr from zero_server.protocol.ast import AST -from zero_server.protocol.wire import PlainJSON, RefusesNullOffWire, StrictWireModel, json_model +from zero_server.protocol.wire import PlainJSON, RefusesNullOffWire, WireModel, json_model from zero_server.shared.json import JSONValue -class TransformRequestQuery(StrictWireModel): - """Parsed in upstream's default (strict) mode, so an undeclared key fails the whole request.""" +class TransformRequestQuery(WireModel): + """Parsed in strip mode, so a newer zero-cache can add fields to the request.""" id: StrictStr name: StrictStr diff --git a/src/zero_server/server/adapters/asyncpg.py b/src/zero_server/server/adapters/asyncpg.py index e58d33c..ceecd7d 100644 --- a/src/zero_server/server/adapters/asyncpg.py +++ b/src/zero_server/server/adapters/asyncpg.py @@ -9,6 +9,7 @@ from zero_server.schema import Schema from zero_server.server.adapters._session import SessionConnection, session from zero_server.server.transaction import DBTransaction, Row +from zero_server.server.transaction_options import IsolationLevel from zero_server.server.zql_database import PostgresError, ZQLDatabase, well_formed_params @@ -31,27 +32,34 @@ async def query(self, sql: str, params: Sequence[Any] = ()) -> list[Row]: class AsyncpgConnection: - def __init__(self, pool: asyncpg.Pool) -> None: + def __init__(self, pool: asyncpg.Pool, isolation_level: IsolationLevel | None = None) -> None: self.pool = pool + # asyncpg spells the level with an underscore; `None` sends a bare `BEGIN`. + self._isolation = None if isolation_level is None else isolation_level.replace(" ", "_") + + async def query(self, sql: str, params: Sequence[Any] = ()) -> list[Row]: + async with self.pool.acquire() as connection: + return await AsyncpgTransaction(connection).query(sql, params) async def transaction[R](self, fn: Callable[[DBTransaction[asyncpg.Connection]], Awaitable[R]]) -> R: async with self.pool.acquire() as connection: - return await _transaction(connection, fn) + return await self._transaction(connection, fn) + + async def _transaction[R]( + self, connection: asyncpg.Connection, fn: Callable[[DBTransaction[asyncpg.Connection]], Awaitable[R]] + ) -> R: + async with connection.transaction(isolation=self._isolation): + return await fn(AsyncpgTransaction(connection)) def session(self) -> AbstractAsyncContextManager[SessionConnection[asyncpg.Connection, asyncpg.Connection]]: """One connection for a push's transactions, released (and so reset by the pool) once, at the end.""" - return session(self.pool.acquire, self.pool.release, _transaction) - - -async def _transaction[R]( - connection: asyncpg.Connection, fn: Callable[[DBTransaction[asyncpg.Connection]], Awaitable[R]] -) -> R: - async with connection.transaction(): - return await fn(AsyncpgTransaction(connection)) + return session(self.pool.acquire, self.pool.release, self._transaction) class AsyncpgDatabase(ZQLDatabase[asyncpg.Connection]): """A `ZQLDatabase` on an asyncpg pool (`await asyncpg.create_pool(dsn)`).""" - def __init__(self, pool: asyncpg.Pool, schema: Schema | None = None) -> None: - super().__init__(AsyncpgConnection(pool), schema) + def __init__( + self, pool: asyncpg.Pool, schema: Schema | None = None, *, isolation_level: IsolationLevel | None = None + ) -> None: + super().__init__(AsyncpgConnection(pool, isolation_level), schema) diff --git a/src/zero_server/server/adapters/psycopg.py b/src/zero_server/server/adapters/psycopg.py index 66744c9..63466e0 100644 --- a/src/zero_server/server/adapters/psycopg.py +++ b/src/zero_server/server/adapters/psycopg.py @@ -21,6 +21,7 @@ from zero_server.schema import Schema from zero_server.server.adapters._session import SessionConnection, session from zero_server.server.transaction import DBTransaction, Row +from zero_server.server.transaction_options import IsolationLevel, set_isolation_statement from zero_server.server.zql_database import PostgresError, ZQLDatabase, well_formed_params @@ -59,29 +60,58 @@ async def query(self, sql: str, params: Sequence[Any] = ()) -> list[Row]: class PsycopgConnection: - def __init__(self, pool: "AsyncConnectionPool") -> None: + def __init__(self, pool: "AsyncConnectionPool", isolation_level: IsolationLevel | None = None) -> None: self.pool = pool + self._isolation_level = isolation_level + + async def query(self, sql: str, params: Sequence[Any] = ()) -> list[Row]: + async with self.pool.connection() as connection: + # In autocommit, so psycopg sends no `BEGIN` of its own. + autocommit = connection.autocommit + await connection.set_autocommit(True) + try: + return await PsycopgTransaction(connection).query(sql, params) + finally: + await connection.set_autocommit(autocommit) async def transaction[R](self, fn: Callable[[DBTransaction[psycopg.AsyncConnection[Any]]], Awaitable[R]]) -> R: async with self.pool.connection() as connection: - return await _transaction(connection, fn) + return await self._transaction(connection, fn) + + async def _transaction[R]( + self, + connection: psycopg.AsyncConnection[Any], + fn: Callable[[DBTransaction[psycopg.AsyncConnection[Any]]], Awaitable[R]], + ) -> R: + return await _transaction(connection, fn, self._isolation_level) def session( self, ) -> AbstractAsyncContextManager[SessionConnection[psycopg.AsyncConnection[Any], psycopg.AsyncConnection[Any]]]: """One connection for a push's transactions, returned to the pool once, at the end.""" - return session(self.pool.getconn, self.pool.putconn, _transaction) + return session(self.pool.getconn, self.pool.putconn, self._transaction) async def _transaction[R]( - connection: psycopg.AsyncConnection[Any], fn: Callable[[DBTransaction[psycopg.AsyncConnection[Any]]], Awaitable[R]] + connection: psycopg.AsyncConnection[Any], + fn: Callable[[DBTransaction[psycopg.AsyncConnection[Any]]], Awaitable[R]], + isolation_level: IsolationLevel | None, ) -> R: async with connection.transaction(): - return await fn(PsycopgTransaction(connection)) + tx = PsycopgTransaction(connection) + if isolation_level is not None: + await tx.query(set_isolation_statement(isolation_level)) + return await fn(tx) class PsycopgDatabase(ZQLDatabase[psycopg.AsyncConnection[Any]]): """A `ZQLDatabase` on a psycopg pool (`AsyncConnectionPool(conninfo, open=False)`, then `await pool.open()`).""" - def __init__(self, pool: "AsyncConnectionPool", schema: Schema | None = None) -> None: - super().__init__(PsycopgConnection(pool), schema) + def __init__( + self, + pool: "AsyncConnectionPool", + schema: Schema | None = None, + *, + isolation_level: IsolationLevel | None = None, + ) -> None: + super().__init__(PsycopgConnection(pool, isolation_level), schema) diff --git a/src/zero_server/server/adapters/sqlalchemy.py b/src/zero_server/server/adapters/sqlalchemy.py index 92d3945..51ab111 100644 --- a/src/zero_server/server/adapters/sqlalchemy.py +++ b/src/zero_server/server/adapters/sqlalchemy.py @@ -14,6 +14,7 @@ from zero_server.schema import Schema from zero_server.server.adapters._session import SessionConnection, session from zero_server.server.transaction import DBTransaction, Row +from zero_server.server.transaction_options import IsolationLevel, set_isolation_statement from zero_server.server.zql_database import PostgresError, ZQLDatabase, well_formed_params @@ -75,29 +76,42 @@ async def sqlalchemy_transaction(connection: AsyncConnection) -> SQLAlchemyTrans class SQLAlchemyConnection: - def __init__(self, engine: AsyncEngine) -> None: + def __init__(self, engine: AsyncEngine, isolation_level: IsolationLevel | None = None) -> None: self.engine = engine + self._isolation_level = isolation_level + + async def query(self, sql: str, params: Sequence[Any] = ()) -> list[Row]: + async with self.engine.connect() as connection: + # Autocommit, so SQLAlchemy begins no transaction; the pool resets it when the connection returns. + connection = await connection.execution_options(isolation_level="AUTOCOMMIT") + return await (await sqlalchemy_transaction(connection)).query(sql, params) async def transaction[R](self, fn: Callable[[DBTransaction[AsyncConnection]], Awaitable[R]]) -> R: async with self.engine.begin() as connection: - return await fn(await sqlalchemy_transaction(connection)) + return await fn(await _begun(connection, self._isolation_level)) + + async def _transaction[R]( + self, connection: AsyncConnection, fn: Callable[[DBTransaction[AsyncConnection]], Awaitable[R]] + ) -> R: + async with connection.begin(): + if connection.dialect.driver == "asyncpg": + await _send_begin(await connection.get_raw_connection()) + return await fn(await _begun(connection, self._isolation_level)) def session(self) -> AbstractAsyncContextManager[SessionConnection[AsyncConnection, AsyncConnection]]: """One connection for a push's transactions, checked back into the pool once, at the end.""" - return session(self.engine.connect, _close, _transaction) + return session(self.engine.connect, _close, self._transaction) -async def _close(connection: AsyncConnection) -> None: - await connection.close() +async def _begun(connection: AsyncConnection, isolation_level: IsolationLevel | None) -> SQLAlchemyTransaction: + tx = await sqlalchemy_transaction(connection) + if isolation_level is not None: + await tx.query(set_isolation_statement(isolation_level)) + return tx -async def _transaction[R]( - connection: AsyncConnection, fn: Callable[[DBTransaction[AsyncConnection]], Awaitable[R]] -) -> R: - async with connection.begin(): - if connection.dialect.driver == "asyncpg": - await _send_begin(await connection.get_raw_connection()) - return await fn(await sqlalchemy_transaction(connection)) +async def _close(connection: AsyncConnection) -> None: + await connection.close() async def _send_begin(pooled: PoolProxiedConnection) -> None: @@ -116,5 +130,7 @@ class SQLAlchemyDatabase(ZQLDatabase[AsyncConnection]): """A `ZQLDatabase` on `engine`, whose URL names the asyncpg or the psycopg driver (`postgresql+asyncpg://` or `postgresql+psycopg://`).""" - def __init__(self, engine: AsyncEngine, schema: Schema | None = None) -> None: - super().__init__(SQLAlchemyConnection(engine), schema) + def __init__( + self, engine: AsyncEngine, schema: Schema | None = None, *, isolation_level: IsolationLevel | None = None + ) -> None: + super().__init__(SQLAlchemyConnection(engine, isolation_level), schema) diff --git a/src/zero_server/server/pg_query_executor.py b/src/zero_server/server/pg_query_executor.py index e7b9db5..5d42903 100644 --- a/src/zero_server/server/pg_query_executor.py +++ b/src/zero_server/server/pg_query_executor.py @@ -12,7 +12,7 @@ from zero_server.query.builder import Format from zero_server.schema import Schema from zero_server.server.server_schema import ServerSchema -from zero_server.server.transaction import DBTransaction, Row +from zero_server.server.transaction import Queryable, Row from zero_server.shared.js import js_keys from zero_server.z2s.compiler import ZQL_RESULT_KEY, compile_query @@ -66,7 +66,7 @@ def parse_int(text: str) -> int: async def execute_postgres_query( - tx: DBTransaction[Any], ast: AST, format: Format, schema: Schema, server_schema: ServerSchema + tx: Queryable, ast: AST, format: Format, schema: Schema, server_schema: ServerSchema ) -> Any: """The query's rows, as lists and dicts in client names. A singular query with no row returns `None`.""" text, values = compile_query(server_schema, schema, ast, format) diff --git a/src/zero_server/server/process_mutations.py b/src/zero_server/server/process_mutations.py index c683be8..f657176 100644 --- a/src/zero_server/server/process_mutations.py +++ b/src/zero_server/server/process_mutations.py @@ -1,17 +1,20 @@ """The mutate endpoint: exactly-once processing of a push from zero-cache. -Ported from rocicorp/mono `packages/zero-server/src/process-mutations.ts` at zero/v1.10.0 -(905d08516ba0caaf314cd28e93a5a47e8acdd482), with `MutationAlreadyProcessedError` from +Ported from rocicorp/mono `packages/zero-server/src/process-mutations.ts` at zero/v1.11.3 +(e91e55f05b7a2c631f77238e1dd6fadf1029a538), with `MutationAlreadyProcessedError` from `packages/zero-cache/src/services/mutagen/error.ts`. Apache-2.0, Copyright Rocicorp. Each mutation runs inside the same database transaction that bumps its client's `lastMutationID`. The bump comes first and the comparison second, so a duplicate or a gap is found after the bump and undone by rolling that transaction back. A mutation whose mutator fails is retried without the mutator: the second transaction takes the ID and records the error in the `mutations` table, which is -where the client's pending mutation learns it failed. +where the client's pending mutation learns it failed. A mutator that registered `tx.retry_on` predicates +may first be re-run, in a fresh transaction, when one of them says yes. """ +import asyncio import functools +import inspect import logging from collections.abc import Awaitable, Callable, Mapping from contextlib import AbstractAsyncContextManager, nullcontext @@ -37,6 +40,7 @@ from zero_server.protocol.push import PushBody from zero_server.protocol.schemas import mutate_params_schema, push_body_schema, refusal from zero_server.protocol.wire import parse_wire +from zero_server.server.transaction import RetryPredicate from zero_server.shared.asserts import check from zero_server.shared.error import error_name, get_error_details, get_error_message from zero_server.shared.js import js_number, js_truthy @@ -70,6 +74,9 @@ class TransactionProviderInput: client_group_id: str client_id: str mutation_id: int | float + # Receives each predicate the mutator registers with `tx.retry_on()`. A `Database` passes it on to + # the server transaction it creates. + add_retry_predicate: Callable[[RetryPredicate], None] | None = None class Database[T](Protocol): @@ -102,7 +109,8 @@ def _session[T](db_provider: Database[T]) -> AbstractAsyncContextManager[Databas return nullcontext(db_provider) -type TransactFnCallback[T] = Callable[[T, str, JSONValue | None], Awaitable[None]] +type TransactFnCallback[T] = Callable[[T, str, JSONValue | None], Awaitable[JSONValue | None]] +"""Runs the mutator. What it returns, unless `None`, is the mutation's result data.""" type TransactFn[T] = Callable[[TransactFnCallback[T]], Awaitable[MutationResponse]] type MutateRequestHandler[T] = Callable[[TransactFn[T], CustomMutation], Awaitable[MutationResponse]] """Runs once per custom mutation. @@ -314,6 +322,43 @@ async def transact(callback: TransactFnCallback[T]) -> MutationResponse: return _app_error_response(mutation_id, error) +MAX_MUTATOR_RUNS = 5 +"""The most times a mutator runs for one mutation within one push when its `tx.retry_on()` predicate keeps +answering yes: the first run and four re-runs.""" + +# Predicates still running after another answered yes. Their answers are ignored, as upstream ignores +# them, but a task needs a reference until it finishes. +_unanswered: set[asyncio.Task[bool]] = set() + + +async def _ask(should_retry: RetryPredicate, error: BaseException, attempt: int) -> bool: + try: + answer = should_retry(error, attempt) + if inspect.isawaitable(answer): + answer = await answer + return answer is True + except Exception as retry_error: + logger.warning("retryOn predicate threw; treating it as no", exc_info=retry_error) + return False + + +async def _should_retry(predicates: list[RetryPredicate], error: BaseException, attempt: int) -> bool: + """Asks all of a run's predicates at once whether to run the mutator again. The first to answer yes + decides it; it is no once every predicate has answered no. A predicate that raises answers no: + escaping here would skip recording the mutation's result, and the client would send it again.""" + if not predicates: + return False + pending = {asyncio.ensure_future(_ask(p, error, attempt)) for p in predicates} + while pending: + done, pending = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED) + if any(task.result() for task in done): + for task in pending: + _unanswered.add(task) + task.add_done_callback(_unanswered.discard) + return True + return False + + class _Transactor[T]: def __init__(self, db_provider: Database[T], push: PushBody, params: MutateParams) -> None: self._db = db_provider @@ -324,9 +369,14 @@ async def transact( self, mutation: CustomMutation, mutation_id: MutationID, callback: TransactFnCallback[T] ) -> MutationResponse: app_error: ApplicationError | None = None + # Which run of the mutator this is: 1, then 2 for the first re-run, and so on. When a run fails, + # it is also the number of the re-run being decided. + attempt = 1 while True: + # What this run registers with `tx.retry_on()`. Each run starts empty. + predicates: list[RetryPredicate] = [] try: - result = await self._transact_impl(mutation, mutation_id, callback, app_error) + result = await self._transact_impl(mutation, mutation_id, callback, app_error, predicates.append) if app_error is not None: logger.warning( "Mutation %s for client %s was retried after an error", @@ -355,9 +405,24 @@ async def transact( exc_info=error, ) raise + # What the mutator or the database raised, without the wrapper. This includes errors + # raised at COMMIT, which the mutator cannot catch. original: BaseException = error if isinstance(error, DatabaseTransactionError) and error.__cause__ is not None: original = error.__cause__ + # Re-running without setting `app_error` is the whole mechanism: `app_error` being set + # is what makes the next pass skip the mutator. Each pass opens a new transaction, and + # the failed run rolled its ID bump back. + if attempt < MAX_MUTATOR_RUNS and await _should_retry(predicates, original, attempt): + logger.warning( + "Mutation %s for client %s failed, re-running mutator (retry %s)", + mutation.id, + mutation.client_id, + attempt, + exc_info=original, + ) + attempt += 1 + continue app_error = wrap_with_application_error(original) logger.warning( "Error processing mutation %s for client %s, retrying without mutator", @@ -371,7 +436,7 @@ async def persist_pre_transaction_failure( ) -> None: """The handler raised before calling `transact`. The ID still advances and the failure is recorded.""" - await self._transact_impl(mutation, mutation_id, None, app_error) + await self._transact_impl(mutation, mutation_id, None, app_error, None) async def _transact_impl( self, @@ -379,6 +444,7 @@ async def _transact_impl( mutation_id: MutationID, callback: TransactFnCallback[T] | None, app_error: ApplicationError | None, + add_retry_predicate: Callable[[RetryPredicate], None] | None, ) -> MutationResponse: """Bump the ID, then run `callback`, or with `app_error` set, record that error in its place.""" phase: _DatabaseTransactionPhase = "open" @@ -394,10 +460,17 @@ async def run(tx: T, hooks: TransactionProviderHooks) -> MutationResponse: if app_error is None: assert callback is not None logger.debug("Executing mutator '%s' (id=%s)", mutation.name, mutation.id) - await callback(tx, mutation.name, mutation.args[0] if mutation.args else None) - else: - await hooks.write_mutation_result(_app_error_response(mutation_id, app_error)) - return MutationResponse.trusted(id=mutation_id, result=_OK) + data = await callback(tx, mutation.name, mutation.args[0] if mutation.args else None) + if data is None: + return MutationResponse.trusted(id=mutation_id, result=_OK) + # Persisted so the data still reaches the client if the push response is lost. A mutator + # that returns nothing writes no row. + success = MutationResponse.trusted(id=mutation_id, result=MutationOk.trusted(data=data)) + await hooks.write_mutation_result(success) + return success + response = _app_error_response(mutation_id, app_error) + await hooks.write_mutation_result(response) + return response try: return await self._db.transaction( @@ -407,6 +480,7 @@ async def run(tx: T, hooks: TransactionProviderHooks) -> MutationResponse: client_group_id=self._push.client_group_id, client_id=mutation.client_id, mutation_id=mutation.id, + add_retry_predicate=add_retry_predicate, ), ) except (ApplicationError, OutOfOrderMutation, MutationAlreadyProcessedError): diff --git a/src/zero_server/server/process_queries.py b/src/zero_server/server/process_queries.py index 32dd6c1..db5501a 100644 --- a/src/zero_server/server/process_queries.py +++ b/src/zero_server/server/process_queries.py @@ -1,7 +1,7 @@ """The query endpoint: turn each named query zero-cache forwards into its AST, in server names. -Ported from rocicorp/mono `packages/zero-server/src/queries/process-queries.ts` at zero/v1.10.0 -(905d08516ba0caaf314cd28e93a5a47e8acdd482). Apache-2.0, Copyright Rocicorp. +Ported from rocicorp/mono `packages/zero-server/src/queries/process-queries.ts` at zero/v1.11.3 +(e91e55f05b7a2c631f77238e1dd6fadf1029a538). Apache-2.0, Copyright Rocicorp. """ import asyncio @@ -77,7 +77,7 @@ async def handle_query_request( message = get_error_message(error) if isinstance(error, ValidationError): decoded = parse_body(body) if isinstance(body, bytes) else body - message = refusal(decoded, transform_request_message_schema, None, message) + message = refusal(decoded, transform_request_message_schema, "strip", message) return ServerTransformFailed( reason=ErrorReason.PARSE, message=f"Failed to parse query request: {message}", diff --git a/src/zero_server/server/push_processor.py b/src/zero_server/server/push_processor.py index 307698c..c1ed603 100644 --- a/src/zero_server/server/push_processor.py +++ b/src/zero_server/server/push_processor.py @@ -1,7 +1,7 @@ """`PushProcessor`: the older entry point to the mutate endpoint, dispatching to a mutator registry. -Ported from rocicorp/mono `packages/zero-server/src/push-processor.ts` at zero/v1.10.0 -(905d08516ba0caaf314cd28e93a5a47e8acdd482). Apache-2.0, Copyright Rocicorp. +Ported from rocicorp/mono `packages/zero-server/src/push-processor.ts` at zero/v1.11.3 +(e91e55f05b7a2c631f77238e1dd6fadf1029a538). Apache-2.0, Copyright Rocicorp. Unlike `handle_mutate_request`, it reports no user ID to zero-cache. """ @@ -33,18 +33,17 @@ async def process( check(body is not None, "body is required when using query params directly") async def handler(transact: TransactFn[T], _mutation: CustomMutation) -> MutationResponse: - async def dispatch(tx: T, name: str, args: JSONValue | None) -> None: - await self._dispatch(mutators, tx, name, args) + async def dispatch(tx: T, name: str, args: JSONValue | None) -> JSONValue | None: + return await self._dispatch(mutators, tx, name, args) return await transact(dispatch) return await handle_mutate(self._db, handler, query, body, None, report_user_id=False) - async def _dispatch(self, mutators: Mapping[str, Any], tx: T, key: str, args: JSONValue | None) -> None: + async def _dispatch(self, mutators: Mapping[str, Any], tx: T, key: str, args: JSONValue | None) -> JSONValue | None: mutator = value_at_path(mutators, key, SEPARATOR) if not callable(mutator) or isinstance(mutator, Mapping): # `check`, formatting only on failure raise AssertionFailedError(f"could not find mutator {key}") if isinstance(mutator, Mutator): - await mutator.fn(args=args, ctx=self._context, tx=tx) - else: - await mutator(tx, args) + return await mutator.fn(args=args, ctx=self._context, tx=tx) + return await mutator(tx, args) diff --git a/src/zero_server/server/server_schema.py b/src/zero_server/server/server_schema.py index a61c90c..c5fb138 100644 --- a/src/zero_server/server/server_schema.py +++ b/src/zero_server/server/server_schema.py @@ -8,7 +8,7 @@ from zero_server.schema import Schema from zero_server.server.pg_types import zql_value_type -from zero_server.server.transaction import DBTransaction +from zero_server.server.transaction import Queryable from zero_server.shared.asserts import check @@ -86,7 +86,7 @@ def _schema_and_table(name: str, server_name: str | None) -> tuple[str, str]: return "public", server_name if server_name is not None else name -async def get_server_schema(tx: DBTransaction[object], schema: Schema) -> ServerSchema: +async def get_server_schema(tx: Queryable, schema: Schema) -> ServerSchema: pairs = [_schema_and_table(t.name, t.server_name) for t in schema.tables.values()] if not pairs: return {} diff --git a/src/zero_server/server/transaction.py b/src/zero_server/server/transaction.py index b83156b..025de50 100644 --- a/src/zero_server/server/transaction.py +++ b/src/zero_server/server/transaction.py @@ -1,11 +1,11 @@ """The transaction a server mutator receives. -Ported from rocicorp/mono `packages/zero-server/src/custom.ts` (`TransactionImpl`) and -`packages/zql/src/mutate/custom.ts` at zero/v1.10.0 (905d08516ba0caaf314cd28e93a5a47e8acdd482). -Apache-2.0, Copyright Rocicorp. +Ported from rocicorp/mono `packages/zero-server/src/custom.ts` (`TransactionImpl`), +`packages/zero-server/src/closable-db-transaction.ts` and `packages/zql/src/mutate/custom.ts` at zero/v1.11.3 +(e91e55f05b7a2c631f77238e1dd6fadf1029a538). Apache-2.0, Copyright Rocicorp. """ -from collections.abc import Sequence +from collections.abc import Awaitable, Callable, Sequence from typing import TYPE_CHECKING, Any, Literal, Protocol, overload if TYPE_CHECKING: @@ -16,14 +16,54 @@ type Row = dict[str, Any] +type RetryPredicate = Callable[[BaseException, int], bool | Awaitable[bool]] +"""Decides whether to run a mutator again after a run fails. Gets what was raised and the number of the +re-run being decided: 1 after the first failure. Return `True` to re-run, or an awaitable of `True` to +re-run once it resolves, which is how to back off. Anything else, including raising, counts as no.""" -class DBTransaction[W](Protocol): - """One open database transaction. `query` takes SQL with `$1`-style placeholders, as upstream's does.""" + +class Queryable(Protocol): + """Runs one statement. `query` takes SQL with `$1`-style placeholders, as upstream's does.""" + + async def query(self, sql: str, params: Sequence[Any] = ()) -> list[Row]: ... + + +class DBTransaction[W](Queryable, Protocol): + """One open database transaction.""" @property def wrapped_transaction(self) -> W: ... - async def query(self, sql: str, params: Sequence[Any] = ()) -> list[Row]: ... + +def _transaction_ended() -> Exception: + return Exception( + "This transaction has ended and can no longer be used. " + "A tx.retryOn() predicate runs after its transaction is over." + ) + + +class ClosableDBTransaction[W]: + """A `DBTransaction` that stops working once closed. The mutator's `tx` is built on it, so code that + outlives the transaction cannot reach a connection that has been committed, rolled back or returned + to the pool.""" + + def __init__(self, inner: DBTransaction[W]) -> None: + self._inner = inner + self._closed = False + + def close(self) -> None: + self._closed = True + + @property + def wrapped_transaction(self) -> W: + if self._closed: + raise _transaction_ended() + return self._inner.wrapped_transaction + + async def query(self, sql: str, params: Sequence[Any] = ()) -> list[Row]: + if self._closed: + raise _transaction_ended() + return await self._inner.query(sql, params) class ServerTransaction[W]: @@ -38,12 +78,33 @@ def __init__( mutation_id: int | float, schema: "Schema | None" = None, server_schema: "ServerSchema | None" = None, + add_retry_predicate: Callable[[RetryPredicate], None] | None = None, ) -> None: self.db_transaction = db_transaction self.client_id = client_id self.mutation_id = mutation_id self._schema = schema self._server_schema = server_schema + self._add_retry_predicate = add_retry_predicate + + def retry_on(self, should_retry: RetryPredicate) -> None: + """If this run fails, ask `should_retry` whether to run the mutator again in a fresh transaction. + + It sees any failure of the run: what the mutator raises, and errors the database raises at + `COMMIT`, which the mutator cannot catch. Everything the failed run wrote is rolled back, and the + re-run reads a new snapshot. The mutator is re-run if any predicate registered in the run says + yes; they are all asked at once, and the first yes starts the re-run. A predicate covers one run, + so the re-run must register again. Within one push the mutator runs at most 5 times. The + predicate runs after the transaction is over, so it cannot use `tx`. + """ + if self._add_retry_predicate is None: + # Fail loudly: a predicate nothing will consult would silently turn every retryable failure + # into a recorded error. + raise Exception( + "tx.retryOn() is not supported on this transaction: nothing will re-run its mutator. " + "A custom Database must pass TransactionProviderInput.addRetryPredicate on to createTransaction." + ) + self._add_retry_predicate(should_retry) def _schemas(self, what: str) -> "tuple[Schema, ServerSchema]": if self._schema is None or self._server_schema is None: # python-only: a database built without one diff --git a/src/zero_server/server/transaction_options.py b/src/zero_server/server/transaction_options.py new file mode 100644 index 0000000..46a03fb --- /dev/null +++ b/src/zero_server/server/transaction_options.py @@ -0,0 +1,16 @@ +"""The isolation level a mutator transaction runs at, an option of every adapter. + +Mirrors `TransactionOptions` in rocicorp/mono's zero-server (transaction-options.ts) at zero/v1.11.3 +(e91e55f05b7a2c631f77238e1dd6fadf1029a538). Apache-2.0, Copyright Rocicorp. +""" + +from typing import Literal + +type IsolationLevel = Literal["read committed", "repeatable read", "serializable"] +"""Spelled as Postgres spells it. Left unset, a transaction opens with a bare `BEGIN` and the database +decides, which is `read committed` on a stock Postgres.""" + + +def set_isolation_statement(isolation_level: IsolationLevel) -> str: + """The statement that sets a transaction's isolation level: its first, before any query.""" + return f"SET TRANSACTION ISOLATION LEVEL {isolation_level.upper()}" diff --git a/src/zero_server/server/zql_database.py b/src/zero_server/server/zql_database.py index 6d2993a..3070487 100644 --- a/src/zero_server/server/zql_database.py +++ b/src/zero_server/server/zql_database.py @@ -1,13 +1,15 @@ """A transaction provider over any Postgres driver, with `tx.mutate` and `tx.run` for mutators. -Ported from rocicorp/mono `packages/zero-server/src/zql-database.ts` at zero/v1.10.0 -(905d08516ba0caaf314cd28e93a5a47e8acdd482). Apache-2.0, Copyright Rocicorp. +Ported from rocicorp/mono `packages/zero-server/src/zql-database.ts`, with the server schema cache of +`CRUDMutatorFactory` in `custom.ts`, at zero/v1.11.3 (e91e55f05b7a2c631f77238e1dd6fadf1029a538). +Apache-2.0, Copyright Rocicorp. A driver plugs in as a `DBConnection`: something that opens a transaction and hands back a `DBTransaction` whose `query` takes `$1`-style placeholders. `zero_server.server.adapters` has one per supported driver. """ +import asyncio from collections.abc import AsyncGenerator, Awaitable, Callable, Sequence from contextlib import AbstractAsyncContextManager, asynccontextmanager from typing import Any, Protocol, overload @@ -17,7 +19,7 @@ from zero_server.schema import Schema from zero_server.server.process_mutations import TransactionProviderHooks, TransactionProviderInput from zero_server.server.server_schema import ServerSchema, get_server_schema -from zero_server.server.transaction import DBTransaction, ServerTransaction +from zero_server.server.transaction import ClosableDBTransaction, DBTransaction, Queryable, ServerTransaction from zero_server.shared.js import has_surrogate, js_number, js_stringify, to_well_formed from zero_server.z2s.sql import ident @@ -39,6 +41,9 @@ class DBConnection[W](Protocol): A connection may also have a `session()`: an async context manager yielding a `DBConnection` whose transactions all run on one connection from the pool, in turn, as a push's mutations do. + + It may also have `query`, with `DBTransaction.query`'s signature: one statement on the connection + with no transaction of its own. `ZQLDatabase.run` then reads without `BEGIN`/`COMMIT`. """ async def transaction[R](self, fn: Callable[[DBTransaction[W]], Awaitable[R]]) -> R: ... @@ -140,6 +145,24 @@ def __init__(self, connection: DBConnection[W], schema: Schema | None = None) -> self.connection = connection self.schema = schema self._server_schema: ServerSchema | None = None + # The fetch in flight, so concurrent cold callers share one catalog query. + self._server_schema_fetch: asyncio.Future[ServerSchema] | None = None + + async def _get_or_fetch_server_schema(self, queryable: Queryable, schema: Schema) -> ServerSchema: + if self._server_schema is not None: + return self._server_schema + fetch = self._server_schema_fetch + if fetch is None: + fetch = self._server_schema_fetch = asyncio.ensure_future(get_server_schema(queryable, schema)) + fetch.add_done_callback(self._forget_failed_fetch) + # Shielded: a caller giving up must not cancel the fetch the others wait on. + self._server_schema = await asyncio.shield(fetch) + return self._server_schema + + def _forget_failed_fetch(self, fetch: "asyncio.Future[ServerSchema]") -> None: + # Don't poison the cache with a transient failure. + if fetch.cancelled() or fetch.exception() is not None: + self._server_schema_fetch = None async def transaction[R]( self, @@ -173,12 +196,25 @@ async def _transaction[R]( ) async def run(tx: DBTransaction[W]) -> R: - if self.schema is not None and self._server_schema is None: - self._server_schema = await get_server_schema(tx, self.schema) - server_tx = ServerTransaction( - tx, transaction_input.client_id, transaction_input.mutation_id, self.schema, self._server_schema - ) - return await callback(server_tx, _Hooks(tx, transaction_input)) + # What the mutator's `tx` uses. Closed once the callback settles, so a `tx` that outlives its + # transaction, such as one captured by a `tx.retry_on()` predicate, raises instead of reaching + # a connection that may already be back in the pool. + mutator_tx = ClosableDBTransaction(tx) + try: + server_schema = ( + None if self.schema is None else await self._get_or_fetch_server_schema(mutator_tx, self.schema) + ) + server_tx = ServerTransaction( + mutator_tx, + transaction_input.client_id, + transaction_input.mutation_id, + self.schema, + server_schema, + transaction_input.add_retry_predicate, + ) + return await callback(server_tx, _Hooks(tx, transaction_input)) + finally: + mutator_tx.close() return await connection.transaction(run) @@ -187,12 +223,30 @@ async def run[R](self, query: OneQuery[R, Any]) -> R | None: ... @overload async def run[R](self, query: Query[R, Any]) -> list[R]: ... async def run(self, query: Query[Any, Any]) -> Any: - """Run a ZQL query in a transaction of its own; see `ServerTransaction.run`.""" + """Run a single read query; see `ServerTransaction.run` for what it returns. + + When the connection has `query`, the SQL goes out as a bare statement with no `BEGIN`/`COMMIT` + around it, so Postgres releases its locks as soon as it finishes. Otherwise it runs in a + transaction of its own. For several reads to see one snapshot, run them with `tx.run` inside + `transaction`. + """ + connection = self.connection + if hasattr(connection, "query"): + target: Queryable = connection # ty: ignore[invalid-assignment] + return await self._run_on(target, query) + + async def run(tx: DBTransaction[W]) -> Any: + return await self._run_on(tx, query) + + return await connection.transaction(run) - async def run(tx: ServerTransaction[W], _hooks: TransactionProviderHooks) -> Any: - return await tx.run(query) + async def _run_on(self, target: Queryable, query: Query[Any, Any]) -> Any: + from zero_server.server.pg_query_executor import execute_postgres_query - return await self.transaction(run) + if self.schema is None: # python-only: a database built without one + raise RuntimeError("tx.run needs a schema: pass one to the database") + server_schema = await self._get_or_fetch_server_schema(target, self.schema) + return await execute_postgres_query(target, query.ast, query.format, self.schema, server_schema) class _Session[W]: diff --git a/src/zero_server/shared/valita.py b/src/zero_server/shared/valita.py index 0bcff54..94d2ac6 100644 --- a/src/zero_server/shared/valita.py +++ b/src/zero_server/shared/valita.py @@ -22,7 +22,7 @@ from zero_server.shared.js import js_keys, js_number, js_stringify type Key = str | int -type Mode = Literal["passthrough", "strict"] +type Mode = Literal["passthrough", "strict", "strip"] FORBID_EXTRA_KEYS = 0x1 MISSING_VALUE = 0x4 @@ -102,7 +102,8 @@ def optional(self) -> "Type": return Optional(self) def try_(self, value: object, mode: Mode | None) -> Result: - return self.func(value, 0 if mode == "passthrough" else FORBID_EXTRA_KEYS) + # Only strict mode, the default, refuses keys a schema does not declare; strip drops them. + return self.func(value, FORBID_EXTRA_KEYS if mode is None or mode == "strict" else 0) class _Primitive(Type): @@ -373,12 +374,10 @@ def message(tree: Tree, value: object, schema: Type, mode: Mode | None) -> str: # Upstream reads the length off the whole value, not the array at the path. got = len(value) if isinstance(value, list | tuple) else "undefined" return f"Expected array with length {length}{at_path}. Got array with length {got}" - case "unrecognized_keys": - if len(leaf.keys) == 1: - return f"Unexpected property {leaf.keys[0]}{at_path}" - return f"Unexpected properties {_display_list('and', leaf.keys)}{at_path}" case _: - # An `invalid_union`, which only a union at the top of the schema produces here. + # An `invalid_union`, which only a union at the top of the schema produces here. No handler + # parses strictly where an undeclared key can be the first issue, so no `unrecognized_keys`. + assert leaf.code == "invalid_union" assert isinstance(schema, UnionType) return _deepest_union_error(value, schema, mode or "strict") diff --git a/tests/compat_scope.py b/tests/compat_scope.py index 168e042..3e117a9 100644 --- a/tests/compat_scope.py +++ b/tests/compat_scope.py @@ -39,7 +39,9 @@ "shared/src/error.ts": { "10-10": "JavaScript only: a thrown string", "44-44": "JavaScript only: a thrown object that is not an Error but has a message", - "69-79": "JavaScript only: details read off a thrown value that is not an Error", + "getErrorCauses": "unused: the client logs an error's causes with it", + "readCause": "unused: only getErrorCauses calls it", + "109-119": "JavaScript only: details read off a thrown value that is not an Error", }, "shared/src/json-schema.ts": { "13-13": "unreachable: `skipAssertJSONValue` is a build flag, false in the published package", @@ -109,7 +111,7 @@ "validator": "unused", }, "zero-server/src/custom.ts": { - "79-105": "deprecated: the delegate behind `tx.query`, the legacy query builder (enableLegacyQueries)", + "85-112": "deprecated: the delegate behind `tx.query`, the legacy query builder (enableLegacyQueries)", "makeSchemaCRUD": "deprecated", "makeServerCRUDExecutor": "unused: only makeServerTransaction and makeSchemaCRUD call it; the " "adapters build their executor through CRUDMutatorFactory, which is compared", @@ -119,14 +121,14 @@ "Python's DBTransaction returns a list", }, "zero-server/src/process-mutations.ts": { - "250-250": _PARSE_DETAILS, - "276-276": _PARSE_DETAILS, - "299-299": _PARSE_DETAILS, - "458-459": "JavaScript only: a URLSearchParams instance; the port takes a mapping", - "487-487": _DEPRECATED_POSITIONAL, - "503-506": _DEPRECATED_POSITIONAL, - "572-572": "unreachable: a DatabaseTransactionError always has a cause", - "592-593": "unreachable: the callback a pre-transaction failure passes, never called because an " + "278-278": _PARSE_DETAILS, + "304-304": _PARSE_DETAILS, + "327-327": _PARSE_DETAILS, + "487-488": "JavaScript only: a URLSearchParams instance; the port takes a mapping", + "518-521": _DEPRECATED_POSITIONAL, + "537-541": _DEPRECATED_POSITIONAL, + "669-669": "unreachable: a DatabaseTransactionError always has a cause", + "713-714": "unreachable: the callback a pre-transaction failure passes, never called because an " "application error is recorded instead", "getMutation": "deprecated: use getMutator", "getObjectAtPath": "deprecated: only getMutation calls it", @@ -136,9 +138,9 @@ "handleTransformRequest": "deprecated", "161-167": _DEPRECATED_POSITIONAL, "226-226": _DEPRECATED_POSITIONAL, - "260-260": _PARSE_DETAILS, - "305-305": "deprecated: a response with no userID, which only the positional call forms produce", - "317-317": "unreachable: an internal failure is a plain Error from mapping the AST or asserting " + "261-261": _PARSE_DETAILS, + "306-306": "deprecated: a response with no userID, which only the positional call forms produce", + "318-318": "unreachable: an internal failure is a plain Error from mapping the AST or asserting " "query internals, which carries no details", }, "zero-server/src/schema.ts": { diff --git a/tests/unit/test_api.py b/tests/unit/test_api.py index 58e8d84..89e1636 100644 --- a/tests/unit/test_api.py +++ b/tests/unit/test_api.py @@ -24,6 +24,7 @@ from zero_server.server.process_queries import handle_query_request from zero_server.server.server_schema import get_server_schema from zero_server.server.transaction import ServerTransaction +from zero_server.server.zql_database import ZQLDatabase from zero_server.shared.asserts import AssertionFailedError SCHEMA = { @@ -151,6 +152,16 @@ def test_mutate_needs_a_schema() -> None: _ = ServerTransaction(db_transaction=None, client_id="c", mutation_id=1).mutate # ty: ignore[invalid-argument-type] +async def test_run_needs_a_schema() -> None: + class Bare: + async def transaction(self, fn: Any) -> Any: ... + + async def query(self, sql: str, params: Any = ()) -> Any: ... + + with pytest.raises(RuntimeError, match=r"tx\.run needs a schema"): + await ZQLDatabase(Bare()).run(create_builder(load_schema(SCHEMA)).plain) + + async def test_server_schema_of_no_tables() -> None: assert await get_server_schema(None, load_schema({"tables": {}})) == {} # ty: ignore[invalid-argument-type] diff --git a/tests/wire/conftest.py b/tests/wire/conftest.py index be3e7e4..9d0de4a 100644 --- a/tests/wire/conftest.py +++ b/tests/wire/conftest.py @@ -59,7 +59,8 @@ ADAPTERS = ["sqlalchemy", "sqlalchemy-psycopg", "asyncpg", "psycopg"] """Every Python adapter runs the same wire and SQL tiers.""" -type Databases = dict[str, Callable[[Schema | None], ZQLDatabase[Any]]] +type Databases = dict[str, Callable[..., ZQLDatabase[Any]]] +"""Per adapter, `(schema, isolation_level=None) -> ZQLDatabase`.""" async def open_adapters(url: str, engine: AsyncEngine) -> tuple[Databases, Callable[[], Any]]: @@ -78,10 +79,16 @@ async def close() -> None: await psycopg_engine.dispose() return { - "sqlalchemy": lambda schema: SQLAlchemyDatabase(engine, schema), - "sqlalchemy-psycopg": lambda schema: SQLAlchemyDatabase(psycopg_engine, schema), - "asyncpg": lambda schema: AsyncpgDatabase(pool, schema), - "psycopg": lambda schema: PsycopgDatabase(psycopg_pool, schema), + "sqlalchemy": lambda schema, isolation_level=None: SQLAlchemyDatabase( + engine, schema, isolation_level=isolation_level + ), + "sqlalchemy-psycopg": lambda schema, isolation_level=None: SQLAlchemyDatabase( + psycopg_engine, schema, isolation_level=isolation_level + ), + "asyncpg": lambda schema, isolation_level=None: AsyncpgDatabase(pool, schema, isolation_level=isolation_level), + "psycopg": lambda schema, isolation_level=None: PsycopgDatabase( + psycopg_pool, schema, isolation_level=isolation_level + ), }, close @@ -208,3 +215,73 @@ async def setup() -> tuple[AsyncEngine, AsyncEngine]: runner.run(close()) runner.run(ts_engine.dispose()) runner.run(py_engine.dispose()) + + +def with_isolation(wire_db: WireDB, adapter: str, level: Any) -> ZQLDatabase[Any]: + return wire_db.py[adapter](None, isolation_level=level) + + +class _Recorded: + """The twin of the connection `recordedRun` in `harness/mutate.ts` builds: logs what it is asked to do, + and fails the first statement when told to.""" + + def __init__(self, inner: Any, fail_first: bool) -> None: + self.inner = inner + self.log: list[str] = [] + self._failing = fail_first + + async def record(self, event: str, run: Callable[[], Any]) -> Any: + self.log.append(event) + if self._failing: + self._failing = False + raise Exception("statement failed") + return await run() + + async def transaction(self, fn: Callable[[Any], Any]) -> Any: + self.log.append("transaction") + + async def wrapped(tx: Any) -> Any: + return await fn(_RecordedTransaction(self, tx)) + + return await self.inner.transaction(wrapped) + + +class _RecordedTransaction: + def __init__(self, recorded: _Recorded, tx: Any) -> None: + self._recorded = recorded + self._tx = tx + + @property + def wrapped_transaction(self) -> Any: + return self._tx.wrapped_transaction + + async def query(self, sql: str, params: Any = ()) -> Any: + return await self._recorded.record("query in transaction", lambda: self._tx.query(sql, params)) + + +class _RecordedBare(_Recorded): + async def query(self, sql: str, params: Any = ()) -> Any: + return await self.record("query", lambda: self.inner.query(sql, params)) + + +async def recorded_run( + wire_db: WireDB, adapter: str, schema: Schema, *, bare: bool, failFirst: bool, concurrent: bool, runs: int +) -> dict[str, Any]: + from zero_server.query.builder import create_builder + + inner = wire_db.py[adapter](schema).connection + connection = _RecordedBare(inner, failFirst) if bare else _Recorded(inner, failFirst) + db = ZQLDatabase(connection, schema) + query = create_builder(schema).item.where("value", "!=", "x").order_by("id", "asc") + + async def one() -> dict[str, Any]: + try: + return {"rows": await db.run(query)} + except Exception as error: + return {"error": str(error)} + + if concurrent: + results = list(await asyncio.gather(*(one() for _ in range(runs)))) + else: + results = [await one() for _ in range(runs)] + return {"results": results, "log": connection.log} diff --git a/tests/wire/mutators.py b/tests/wire/mutators.py index 2602b3c..f51f541 100644 --- a/tests/wire/mutators.py +++ b/tests/wire/mutators.py @@ -1,5 +1,6 @@ """The Python twin of `harness/mutate.ts`: the same mutators, handler and failing provider.""" +import asyncio from collections.abc import AsyncGenerator, Awaitable, Callable from contextlib import asynccontextmanager from typing import Any @@ -18,7 +19,7 @@ TransactionProviderHooks, TransactionProviderInput, ) -from zero_server.server.transaction import ServerTransaction +from zero_server.server.transaction import RetryPredicate, ServerTransaction from zero_server.server.zql_database import ZQLDatabase type Tx = ServerTransaction[AsyncConnection] @@ -102,7 +103,79 @@ async def run(tx: Tx, args: dict[str, Any]) -> None: return run -MUTATORS: dict[str, Callable[[Tx, dict[str, Any]], Awaitable[None]]] = { +# How many times item.retry has run for each mutation, by `clientID:mutationID`. Reset per test example. +RUNS: dict[str, int] = {} +_kept: list[Tx] = [] + + +def reset_runs() -> None: + RUNS.clear() + _kept.clear() + + +async def _later(_error: BaseException, _attempt: int) -> bool: + await asyncio.sleep(0.001) + return True + + +def _throw(_error: BaseException, _attempt: int) -> bool: + raise Exception("predicate broke") + + +async def _reject(_error: BaseException, _attempt: int) -> bool: + raise Exception("predicate rejected") + + +PREDICATES: dict[str, RetryPredicate] = { # ty: ignore[invalid-assignment] - "truthy" answers 1, not a bool + "yes": lambda _error, _attempt: True, + "no": lambda _error, _attempt: False, + "later": _later, + "throw": _throw, + "reject": _reject, + "truthy": lambda _error, _attempt: 1, + "firstOnly": lambda _error, attempt: attempt == 1, + "matches": lambda error, _attempt: "failed" in str(error), +} + + +async def return_(tx: Tx, args: dict[str, Any]) -> Any: + await put(tx, args) + return args.get("ret") + + +async def retry(tx: Tx, args: dict[str, Any]) -> int: + from zero_server.shared.js import js_number + + for name in args.get("predicates") or []: + tx.retry_on(PREDICATES[name]) + await put(tx, args) + key = f"{tx.client_id}:{js_number(tx.mutation_id)}" + run = RUNS[key] = RUNS.get(key, 0) + 1 + if run <= args.get("fail", 0): + if args.get("db"): + await tx.db_transaction.query("SELECT * FROM missing_table") + raise Exception(f"run {run} failed") + return run + + +async def keep(tx: Tx, args: dict[str, Any]) -> bool: + _kept[:] = [tx] + await put(tx, args) + # Still open, so this is the driver's transaction. + return tx.db_transaction.wrapped_transaction is None + + +async def use_kept(_tx: Tx, args: dict[str, Any]) -> bool: + if not _kept: + raise Exception("nothing kept") + if args.get("db"): + await _kept[0].db_transaction.query("SELECT 1") + if args.get("run"): + await _kept[0].run(_zql.item) + return _kept[0].db_transaction.wrapped_transaction is None + + +MUTATORS: dict[str, Callable[[Tx, dict[str, Any]], Awaitable[Any]]] = { **{f"throw.{shape}": throwing(make) for shape, make in SHAPES.items()}, "item.count": count, **{f"crud.{kind}": crud(kind) for kind in ("insert", "upsert", "update", "delete")}, @@ -112,6 +185,10 @@ async def run(tx: Tx, args: dict[str, Any]) -> None: "item.typeError": type_error, "item.detailsError": details_error, "item.dbError": db_error, + "item.return": return_, + "item.retry": retry, + "item.keep": keep, + "item.useKept": use_kept, } @@ -126,11 +203,11 @@ async def handler(transact: TransactFn[Tx], mutation: CustomMutation) -> Mutatio if mutation.name == "pre.error": raise Exception(_js_string(args.get("message"), "pre")) - async def run(tx: Tx, name: str, mutator_args: Any) -> None: + async def run(tx: Tx, name: str, mutator_args: Any) -> Any: mutator = MUTATORS.get(name.replace("post.", "item.", 1) if name.startswith("post.") else name) if mutator is None: raise Exception(f"unknown mutator {name}") - await mutator(tx, _args(mutator_args)) + return await mutator(tx, _args(mutator_args)) result = await transact(run) if mutation.name.startswith("post."): @@ -177,15 +254,29 @@ async def _registry_app_error(*, tx: Tx, args: Any, ctx: Any) -> None: await app_error(tx, _args(args)) +async def _registry_ret(*, tx: Tx, args: Any, ctx: Any) -> Any: + return await return_(tx, _args(args)) + + async def _legacy_put(tx: Tx, args: Any) -> None: await put(tx, _args(args)) +async def _legacy_ret(tx: Tx, args: Any) -> Any: + return await return_(tx, _args(args)) + + REGISTRY = { **define_mutators( - {"item": {"put": define_mutator(_registry_put), "appError": define_mutator(_registry_app_error)}} + { + "item": { + "put": define_mutator(_registry_put), + "appError": define_mutator(_registry_app_error), + "ret": define_mutator(_registry_ret), + } + } ), - "legacy": {"put": _legacy_put}, + "legacy": {"put": _legacy_put, "ret": _legacy_ret}, } diff --git a/tests/wire/test_mutate.py b/tests/wire/test_mutate.py index 7481a65..32e5689 100644 --- a/tests/wire/test_mutate.py +++ b/tests/wire/test_mutate.py @@ -13,7 +13,7 @@ from tests.strategies import as_js, cleanup_args, corrupted, json_values, mutation_ids, strings from tests.wire.conftest import ADAPTERS, WireDB from tests.wire.errors import SHAPES -from tests.wire.mutators import REGISTRY, handler, provider, use_schema +from tests.wire.mutators import PREDICATES, REGISTRY, handler, provider, reset_runs, use_schema from zero_server.protocol.push import PushBody from zero_server.schema import load_schema from zero_server.server.process_mutations import handle_mutate_request @@ -26,6 +26,7 @@ "item.put", "item.put", "item.put", "item.count", "item.appError", "item.error", "item.typeError", "item.dbError", "item.detailsError", "pre.appError", "pre.error", "post.put", "nope", + "item.return", "item.retry", "item.retry", "item.keep", "item.useKept", ] # fmt: skip texts = st.sampled_from(["a", "b", "c", "é", "\ud800"]) @@ -88,7 +89,15 @@ def _row(columns: dict[str, st.SearchStrategy[Any]], pk: dict[str, st.SearchStra CRUD_NAMES = ["crud.insert", "crud.upsert", "crud.update", "crud.delete"] mutator_args = st.fixed_dictionaries( {"id": texts, "value": texts}, - optional={"message": st.sampled_from(["no", "nope", "", "m\udc00"]), "details": json_values}, + optional={ + "message": st.sampled_from(["no", "nope", "", "m\udc00"]), + "details": json_values, + # A mutator that returns `None` returns nothing, so JSON `null` is never result data. + "ret": json_values.filter(lambda v: v is not None), + "predicates": st.lists(st.sampled_from(sorted(PREDICATES)), max_size=3), + "fail": st.integers(0, 6), + "db": st.booleans(), + }, ) @@ -314,6 +323,32 @@ def _scenario(*bodies: dict[str, Any]) -> list[tuple[Any, dict[str, str], str | for up_to in [1.5, 1e20, 2**63, 1e21, -1, 2**53 + 1] ), ), + "return values": _scenario( + _push(_m(1, "item.return", ret={"n": [1, None]}), _m(2, "item.return"), _m(3, "item.return", ret=0)), + ), + "retry until it succeeds": _scenario( + _push( + _m(1, "item.retry", fail=2, predicates=["yes"]), + _m(2, "item.retry", fail=1, predicates=["no", "later"]), + _m(3, "item.retry", fail=1, predicates=["throw", "reject", "truthy"]), + _m(4, "item.retry", fail=2, predicates=["firstOnly"]), + _m(5, "item.retry", fail=1, predicates=["matches"], db=True), + _m(6, "item.retry", fail=1, predicates=["matches"]), + _m(7, "item.retry", fail=6, predicates=["yes"]), + _m(8, "item.retry", fail=6, predicates=["yes"], db=True), + # One answers yes while the other is still deciding. + _m(9, "item.retry", fail=1, predicates=["later", "yes"]), + ), + ), + "transaction used after it ended": _scenario( + _push( + _m(1, "item.useKept"), + _m(2, "item.keep"), + _m(3, "item.useKept"), + _m(4, "item.useKept", db=True), + _m(5, "item.useKept", run=True), + ), + ), "cleanup invalid and failing": _scenario( _push(_cleanup({"type": "bulk", "clientGroupID": "cg1", "clientIDs": []}), _m(1)), _push(_cleanup({"clientGroupID": "cg1", "clientID": "cfail", "upToMutationID": 2}), _m(2)), @@ -332,6 +367,8 @@ def _with_branches(test: Any) -> Any: @given(scenarios()) def test_pushes_match_upstream(harness: Harness, wire_db: WireDB, adapter: str, pushes: list[Any]) -> None: wire_db.reset() + harness.call("resetRuns") + reset_runs() schema = load_schema(harness.call("wireSchema")) use_schema(schema) # One adapter runs without sessions, and one takes each body already parsed where it parses. @@ -382,7 +419,9 @@ async def callback(tx: Any, _hooks: Any) -> Any: assert wire_db.run(message("asyncpg")).startswith("invalid input for query argument $1") -PROCESSOR_NAMES = ["item.put", "item|put", "item.appError", "legacy.put", "legacy|put", "item", "nope"] +PROCESSOR_NAMES = [ + "item.put", "item|put", "item.appError", "item.ret", "legacy.put", "legacy|put", "legacy.ret", "item", "nope", +] # fmt: skip @pytest.mark.parametrize("adapter", ADAPTERS) @@ -424,3 +463,72 @@ def test_circular_cause_matches_upstream(harness: Harness, wire_db: WireDB) -> N expected.pop("userID") assert as_js(actual.to_json()) == as_js({**expected, "userID": None}) assert as_js(wire_db.dump(wire_db.py_engine)) == as_js(wire_db.dump(wire_db.ts_engine)) + + +def test_retry_on_unsupported_matches_upstream(harness: Harness, wire_db: WireDB) -> None: + """A transaction opened with no input has nothing to re-run its mutator, so `retry_on` refuses.""" + expected = harness.call("retryOnUnsupported", url=wire_db.ts_url) + schema = load_schema(harness.call("wireSchema")) + + async def callback(tx: Any, _hooks: Any) -> Any: + try: + tx.retry_on(lambda _error, _attempt: True) + except Exception as error: + return str(error) + return None + + assert wire_db.run(wire_db.py["asyncpg"](schema).transaction(callback)) == expected + + +@pytest.mark.parametrize("adapter", ADAPTERS) +@pytest.mark.parametrize("level", [None, "read committed", "repeatable read", "serializable"]) +def test_isolation_level_matches_upstream(harness: Harness, wire_db: WireDB, adapter: str, level: Any) -> None: + from tests.wire.conftest import with_isolation + + expected = harness.call("isolation", url=wire_db.ts_url, level=level) + db = with_isolation(wire_db, adapter, level) + + async def callback(tx: Any, _hooks: Any) -> str: + rows = await tx.db_transaction.query("SHOW transaction_isolation") + return rows[0]["transaction_isolation"] + + async def in_session() -> str: + async with db.session() as session: + return await session.transaction(callback) + + assert wire_db.run(db.transaction(callback)) == expected + # A push runs its mutations in a session. + assert wire_db.run(in_session()) == expected + + +@pytest.mark.parametrize("adapter", ADAPTERS) +@pytest.mark.parametrize("bare", [True, False], ids=["bare", "in transaction"]) +@pytest.mark.parametrize( + ("fail_first", "concurrent", "runs"), + # Not a failed fetch with concurrent runs: whether a run shares it or starts its own depends on timing. + [(False, False, 2), (True, False, 3), (False, True, 3)], + ids=["cached", "failed fetch not cached", "concurrent fetch shared"], +) +def test_run_matches_upstream( + harness: Harness, wire_db: WireDB, adapter: str, bare: bool, fail_first: bool, concurrent: bool, runs: int +) -> None: + """`ZQLDatabase.run` reads without a transaction when the connection can, and caches the server schema + once fetched, sharing a fetch in flight and dropping one that failed. Compared by what each side asked + its connection to do.""" + from tests.wire.conftest import recorded_run + + wire_db.reset() + for engine in (wire_db.ts_engine, wire_db.py_engine): + wire_db.run(_insert_items(engine)) + args = {"bare": bare, "failFirst": fail_first, "concurrent": concurrent, "runs": runs} + expected = harness.call("recordedRun", url=wire_db.ts_url, **args) + schema = load_schema(harness.call("wireSchema")) + actual = wire_db.run(recorded_run(wire_db, adapter, schema, **args)) + assert as_js(actual) == as_js(expected) + + +async def _insert_items(engine: Any) -> None: + from sqlalchemy import text + + async with engine.begin() as conn: + await conn.execute(text("INSERT INTO item (id, value) VALUES ('a', 'x'), ('b', 'y'), ('c', 'z')")) diff --git a/tests/wire/test_requests.py b/tests/wire/test_requests.py index aa5e4f5..51154d6 100644 --- a/tests/wire/test_requests.py +++ b/tests/wire/test_requests.py @@ -38,7 +38,7 @@ # Each schema as upstream parses it: the schema and the mode its handler passes. VALITA: dict[str, tuple[valita.Type, valita.Mode | None]] = { "pushBody": (push_body_schema, "passthrough"), - "transformRequest": (transform_request_message_schema, None), + "transformRequest": (transform_request_message_schema, "strip"), "cleanupResultsArg": (cleanup_results_arg_schema, None), "mutateParams": (mutate_params_schema, "passthrough"), } @@ -103,6 +103,7 @@ def _crud(*ops: Any) -> dict[str, Any]: ("pushBody", _push({**_crud(), "args": []})), ("pushBody", _push({**_crud(), "type": 1})), ("pushBody", _push(_crud(_delete), "x")), + # Parsed in strip mode, so keys it does not know are dropped rather than refused. ("transformRequest", ["transform", [{"id": "1", "name": "n", "args": [], "b": 1, "a": 2, "0": 3}]]), ("transformRequest", ["transform", [{"id": "1", "name": "n", "args": [], "x": 1}]]), ("transformRequest", ["transform"]), @@ -111,6 +112,7 @@ def _crud(*ops: Any) -> dict[str, Any]: ("cleanupResultsArg", {"type": "bulk", "clientGroupID": "g", "clientIDs": []}), ("cleanupResultsArg", {"type": "single", "clientGroupID": "g", "clientID": 1, "upToMutationID": 1}), ("cleanupResultsArg", {"clientGroupID": "g", "clientID": "c", "upToMutationID": 1, "x": 1}), + ("cleanupResultsArg", {"clientGroupID": "g", "clientID": "c", "upToMutationID": 1, "b": 1, "a": 2, "0": 3}), ("cleanupResultsArg", {"clientGroupID": "g", "clientID": "c", "upToMutationID": "1"}), ("cleanupResultsArg", "x"), ("mutateParams", {"schema": 1, "appID": 2}), From 81709d4ac649933a73a6f77e55f22242388426a6 Mon Sep 17 00:00:00 2001 From: Jonas Daniels Date: Wed, 28 Oct 2026 13:39:57 -0700 Subject: [PATCH 6/6] test: prove compatibility with @rocicorp/zero 1.11.3 and valita 0.5.5 Co-Authored-By: Claude Opus 5.5 --- .gitignore | 1 + src/zero_server/protocol/application_error.py | 4 +- src/zero_server/protocol/ast_wire.py | 2 +- src/zero_server/protocol/error.py | 4 +- src/zero_server/protocol/mutate_server.py | 4 +- src/zero_server/protocol/mutation.py | 2 +- src/zero_server/protocol/mutation_id.py | 4 +- src/zero_server/protocol/push.py | 4 +- src/zero_server/protocol/query_server.py | 4 +- src/zero_server/protocol/schemas.py | 4 +- src/zero_server/query/expression.py | 4 +- src/zero_server/query/registry.py | 2 +- src/zero_server/schema.py | 2 +- src/zero_server/server/crud.py | 4 +- src/zero_server/server/pg_query_executor.py | 2 +- src/zero_server/server/server_schema.py | 2 +- src/zero_server/shared/asserts.py | 4 +- src/zero_server/shared/error.py | 4 +- src/zero_server/shared/json.py | 4 +- src/zero_server/shared/registry.py | 4 +- src/zero_server/shared/valita.py | 6 +- src/zero_server/z2s/sql.py | 2 +- tests/compat_scope.py | 80 ++++++++++--------- 23 files changed, 81 insertions(+), 72 deletions(-) diff --git a/.gitignore b/.gitignore index 7a22a1c..92f46dc 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ build/ .coverage node_modules/ runner.log +.coverage.* diff --git a/src/zero_server/protocol/application_error.py b/src/zero_server/protocol/application_error.py index c6a6155..e56fbb1 100644 --- a/src/zero_server/protocol/application_error.py +++ b/src/zero_server/protocol/application_error.py @@ -1,7 +1,7 @@ """The error application code raises to send a message and JSON details back to the client. -Ported from rocicorp/mono `packages/zero-protocol/src/application-error.ts` at zero/v1.10.0 -(905d08516ba0caaf314cd28e93a5a47e8acdd482). Apache-2.0, Copyright Rocicorp. +Ported from rocicorp/mono `packages/zero-protocol/src/application-error.ts` at zero/v1.11.3 +(e91e55f05b7a2c631f77238e1dd6fadf1029a538). Apache-2.0, Copyright Rocicorp. """ from typing import Literal diff --git a/src/zero_server/protocol/ast_wire.py b/src/zero_server/protocol/ast_wire.py index bdd19ed..2621074 100644 --- a/src/zero_server/protocol/ast_wire.py +++ b/src/zero_server/protocol/ast_wire.py @@ -1,7 +1,7 @@ """The AST's wire schema: what `AST.from_json` accepts, mirroring upstream's `astSchema`. Ported from rocicorp/mono `packages/zero-protocol/src/ast.ts`, `data.ts` and `primary-key.ts` at -zero/v1.10.0 (905d08516ba0caaf314cd28e93a5a47e8acdd482). Apache-2.0, Copyright Rocicorp. +zero/v1.11.3 (e91e55f05b7a2c631f77238e1dd6fadf1029a538). Apache-2.0, Copyright Rocicorp. These models are only ever parsed. Each turns into the plain node class of `zero_server.protocol.ast` it mirrors, which the builder, the query endpoint and z2s work with. diff --git a/src/zero_server/protocol/error.py b/src/zero_server/protocol/error.py index 7f538a6..6e25e45 100644 --- a/src/zero_server/protocol/error.py +++ b/src/zero_server/protocol/error.py @@ -1,8 +1,8 @@ """Error kinds, origins and reasons, and the bodies of a failed push or transform. Ported from rocicorp/mono `packages/zero-protocol/src/error.ts`, `error-kind-enum.ts`, -`error-origin-enum.ts` and `error-reason-enum.ts` at zero/v1.10.0 -(905d08516ba0caaf314cd28e93a5a47e8acdd482). Apache-2.0, Copyright Rocicorp. +`error-origin-enum.ts` and `error-reason-enum.ts` at zero/v1.11.3 +(e91e55f05b7a2c631f77238e1dd6fadf1029a538). Apache-2.0, Copyright Rocicorp. """ from enum import StrEnum diff --git a/src/zero_server/protocol/mutate_server.py b/src/zero_server/protocol/mutate_server.py index e1f440f..fbb5936 100644 --- a/src/zero_server/protocol/mutate_server.py +++ b/src/zero_server/protocol/mutate_server.py @@ -1,7 +1,7 @@ """The mutate endpoint's response and query string. -Ported from rocicorp/mono `packages/zero-protocol/src/mutate-server.ts` at zero/v1.10.0 -(905d08516ba0caaf314cd28e93a5a47e8acdd482). Apache-2.0, Copyright Rocicorp. +Ported from rocicorp/mono `packages/zero-protocol/src/mutate-server.ts` at zero/v1.11.3 +(e91e55f05b7a2c631f77238e1dd6fadf1029a538). Apache-2.0, Copyright Rocicorp. """ from collections.abc import Mapping diff --git a/src/zero_server/protocol/mutation.py b/src/zero_server/protocol/mutation.py index c63b2f6..bd03e8b 100644 --- a/src/zero_server/protocol/mutation.py +++ b/src/zero_server/protocol/mutation.py @@ -1,7 +1,7 @@ """Mutations as a push carries them, and the result recorded for each. Ported from rocicorp/mono `packages/zero-protocol/src/mutation.ts` and `mutation-type-enum.ts` at -zero/v1.10.0 (905d08516ba0caaf314cd28e93a5a47e8acdd482). Apache-2.0, Copyright Rocicorp. +zero/v1.11.3 (e91e55f05b7a2c631f77238e1dd6fadf1029a538). Apache-2.0, Copyright Rocicorp. """ from typing import Annotated, Any, Literal diff --git a/src/zero_server/protocol/mutation_id.py b/src/zero_server/protocol/mutation_id.py index 24bc9d8..6128699 100644 --- a/src/zero_server/protocol/mutation_id.py +++ b/src/zero_server/protocol/mutation_id.py @@ -1,5 +1,5 @@ -"""Ported from rocicorp/mono `packages/zero-protocol/src/mutation-id.ts` at zero/v1.10.0 -(905d08516ba0caaf314cd28e93a5a47e8acdd482). Apache-2.0, Copyright Rocicorp. +"""Ported from rocicorp/mono `packages/zero-protocol/src/mutation-id.ts` at zero/v1.11.3 +(e91e55f05b7a2c631f77238e1dd6fadf1029a538). Apache-2.0, Copyright Rocicorp. """ from pydantic import Field, StrictStr diff --git a/src/zero_server/protocol/push.py b/src/zero_server/protocol/push.py index 783b172..5cb7b73 100644 --- a/src/zero_server/protocol/push.py +++ b/src/zero_server/protocol/push.py @@ -1,7 +1,7 @@ """What zero-cache POSTs to a mutate endpoint. -Ported from rocicorp/mono `packages/zero-protocol/src/push.ts` at zero/v1.10.0 -(905d08516ba0caaf314cd28e93a5a47e8acdd482). Apache-2.0, Copyright Rocicorp. +Ported from rocicorp/mono `packages/zero-protocol/src/push.ts` at zero/v1.11.3 +(e91e55f05b7a2c631f77238e1dd6fadf1029a538). Apache-2.0, Copyright Rocicorp. """ from pydantic import Field, StrictStr diff --git a/src/zero_server/protocol/query_server.py b/src/zero_server/protocol/query_server.py index ca13063..c65a1a9 100644 --- a/src/zero_server/protocol/query_server.py +++ b/src/zero_server/protocol/query_server.py @@ -1,7 +1,7 @@ """The query endpoint's response. -Ported from rocicorp/mono `packages/zero-protocol/src/query-server.ts` at zero/v1.10.0 -(905d08516ba0caaf314cd28e93a5a47e8acdd482). Apache-2.0, Copyright Rocicorp. +Ported from rocicorp/mono `packages/zero-protocol/src/query-server.ts` at zero/v1.11.3 +(e91e55f05b7a2c631f77238e1dd6fadf1029a538). Apache-2.0, Copyright Rocicorp. """ from typing import Any, Final, Literal diff --git a/src/zero_server/protocol/schemas.py b/src/zero_server/protocol/schemas.py index ad923bb..8b6d903 100644 --- a/src/zero_server/protocol/schemas.py +++ b/src/zero_server/protocol/schemas.py @@ -3,8 +3,8 @@ Ported from rocicorp/mono `packages/shared/src/json-schema.ts`, `packages/zero-protocol/src/data.ts`, `packages/zero-protocol/src/primary-key.ts`, `packages/zero-protocol/src/mutation.ts`, `packages/zero-protocol/src/push.ts`, -`packages/zero-protocol/src/mutate-server.ts` and `packages/zero-protocol/src/custom-queries.ts` at zero/v1.10.0 -(905d08516ba0caaf314cd28e93a5a47e8acdd482). Apache-2.0, Copyright Rocicorp. +`packages/zero-protocol/src/mutate-server.ts` and `packages/zero-protocol/src/custom-queries.ts` at zero/v1.11.3 +(e91e55f05b7a2c631f77238e1dd6fadf1029a538). Apache-2.0, Copyright Rocicorp. The Pydantic models beside them decide what parses; `tests/wire/test_requests.py` holds the two to the same answer. These only run once a model has refused a value. diff --git a/src/zero_server/query/expression.py b/src/zero_server/query/expression.py index f9837de..2324cda 100644 --- a/src/zero_server/query/expression.py +++ b/src/zero_server/query/expression.py @@ -1,7 +1,7 @@ """Conditions for `Query.where`: `cmp`, `and_`, `or_`, `not_` and `exists`. -Ported from rocicorp/mono `packages/zql/src/query/expression.ts` at zero/v1.10.0 -(905d08516ba0caaf314cd28e93a5a47e8acdd482). Apache-2.0, Copyright Rocicorp. +Ported from rocicorp/mono `packages/zql/src/query/expression.ts` at zero/v1.11.3 +(e91e55f05b7a2c631f77238e1dd6fadf1029a538). Apache-2.0, Copyright Rocicorp. """ from collections.abc import Callable diff --git a/src/zero_server/query/registry.py b/src/zero_server/query/registry.py index 60ad4dd..6532c2a 100644 --- a/src/zero_server/query/registry.py +++ b/src/zero_server/query/registry.py @@ -1,7 +1,7 @@ """`define_query` and `define_queries`: named queries the query endpoint resolves by name. Ported from rocicorp/mono `packages/zql/src/query/query-registry.ts`, `validate-input.ts` and -`error.ts` at zero/v1.10.0 (905d08516ba0caaf314cd28e93a5a47e8acdd482). Apache-2.0, Copyright Rocicorp. +`error.ts` at zero/v1.11.3 (e91e55f05b7a2c631f77238e1dd6fadf1029a538). Apache-2.0, Copyright Rocicorp. A validator is anything with Pydantic's `validate_python` (a `TypeAdapter`, or a model class), or a plain callable that returns the parsed value and raises `ValueError` on bad input. Upstream takes a diff --git a/src/zero_server/schema.py b/src/zero_server/schema.py index 9ade04c..9763cf2 100644 --- a/src/zero_server/schema.py +++ b/src/zero_server/schema.py @@ -4,7 +4,7 @@ `JSON.stringify(schema)` and load that here; this module has no schema DSL of its own. Ported from rocicorp/mono `packages/zero-types/src/schema.ts`, `schema-value.ts`, `name-mapper.ts` and -`packages/zero-schema/src/name-mapper.ts` at zero/v1.10.0 (905d08516ba0caaf314cd28e93a5a47e8acdd482). +`packages/zero-schema/src/name-mapper.ts` at zero/v1.11.3 (e91e55f05b7a2c631f77238e1dd6fadf1029a538). Apache-2.0, Copyright Rocicorp. """ diff --git a/src/zero_server/server/crud.py b/src/zero_server/server/crud.py index a015232..109ad5d 100644 --- a/src/zero_server/server/crud.py +++ b/src/zero_server/server/crud.py @@ -1,8 +1,8 @@ """`tx.mutate..insert / upsert / update / delete` on the server, as SQL. Ported from rocicorp/mono `packages/zero-server/src/custom.ts` (`makeServerTableCRUD`), -`packages/zql/src/mutate/crud.ts` and the column conversions of `packages/z2s/src/sql.ts` at zero/v1.10.0 -(905d08516ba0caaf314cd28e93a5a47e8acdd482). Apache-2.0, Copyright Rocicorp. +`packages/zql/src/mutate/crud.ts` and the column conversions of `packages/z2s/src/sql.ts` at zero/v1.11.3 +(e91e55f05b7a2c631f77238e1dd6fadf1029a538). Apache-2.0, Copyright Rocicorp. Values are bound as `zero_server.z2s.sql` binds them. """ diff --git a/src/zero_server/server/pg_query_executor.py b/src/zero_server/server/pg_query_executor.py index 5d42903..53fd2f5 100644 --- a/src/zero_server/server/pg_query_executor.py +++ b/src/zero_server/server/pg_query_executor.py @@ -1,7 +1,7 @@ """Run a ZQL query against Postgres and return what the client-side query would have. Ported from rocicorp/mono `packages/zero-server/src/pg-query-executor.ts` and `extractZqlResult` in -`packages/z2s/src/compiler.ts` at zero/v1.10.0 (905d08516ba0caaf314cd28e93a5a47e8acdd482). +`packages/z2s/src/compiler.ts` at zero/v1.11.3 (e91e55f05b7a2c631f77238e1dd6fadf1029a538). Apache-2.0, Copyright Rocicorp. """ diff --git a/src/zero_server/server/server_schema.py b/src/zero_server/server/server_schema.py index 05e1c78..8b475bf 100644 --- a/src/zero_server/server/server_schema.py +++ b/src/zero_server/server/server_schema.py @@ -1,7 +1,7 @@ """The Postgres types behind each column of the Zero schema, and the check that they agree. Ported from rocicorp/mono `packages/zero-server/src/schema.ts` and `packages/zero-types/src/server-schema.ts` -at zero/v1.10.0 (905d08516ba0caaf314cd28e93a5a47e8acdd482). Apache-2.0, Copyright Rocicorp. +at zero/v1.11.3 (e91e55f05b7a2c631f77238e1dd6fadf1029a538). Apache-2.0, Copyright Rocicorp. """ from dataclasses import dataclass diff --git a/src/zero_server/shared/asserts.py b/src/zero_server/shared/asserts.py index 2f5cb09..18b8c40 100644 --- a/src/zero_server/shared/asserts.py +++ b/src/zero_server/shared/asserts.py @@ -1,7 +1,7 @@ """Upstream's `assert`, which, unlike Python's, survives `python -O`. -Ported from rocicorp/mono `packages/shared/src/asserts.ts` at zero/v1.10.0 -(905d08516ba0caaf314cd28e93a5a47e8acdd482). Apache-2.0, Copyright Rocicorp. +Ported from rocicorp/mono `packages/shared/src/asserts.ts` at zero/v1.11.3 +(e91e55f05b7a2c631f77238e1dd6fadf1029a538). Apache-2.0, Copyright Rocicorp. """ from typing import Any, TypeGuard diff --git a/src/zero_server/shared/error.py b/src/zero_server/shared/error.py index d3ae27f..86ac17f 100644 --- a/src/zero_server/shared/error.py +++ b/src/zero_server/shared/error.py @@ -1,7 +1,7 @@ """Reading a message and JSON details off a raised exception. -Ported from rocicorp/mono `packages/shared/src/error.ts` at zero/v1.10.0 -(905d08516ba0caaf314cd28e93a5a47e8acdd482). Apache-2.0, Copyright Rocicorp. +Ported from rocicorp/mono `packages/shared/src/error.ts` at zero/v1.11.3 +(e91e55f05b7a2c631f77238e1dd6fadf1029a538). Apache-2.0, Copyright Rocicorp. JavaScript's `Error` is Python's `Exception`: an error whose class is exactly `Exception` carries no `{name}` detail, the way a plain `Error` carries none upstream. A class can set `error_name` to the name diff --git a/src/zero_server/shared/json.py b/src/zero_server/shared/json.py index f36a749..3457918 100644 --- a/src/zero_server/shared/json.py +++ b/src/zero_server/shared/json.py @@ -1,7 +1,7 @@ """JSON values, as Zero's wire format allows them. -Ported from rocicorp/mono `packages/shared/src/json.ts` at zero/v1.10.0 -(905d08516ba0caaf314cd28e93a5a47e8acdd482). Apache-2.0, Copyright Rocicorp. +Ported from rocicorp/mono `packages/shared/src/json.ts` at zero/v1.11.3 +(e91e55f05b7a2c631f77238e1dd6fadf1029a538). Apache-2.0, Copyright Rocicorp. """ from typing import Any diff --git a/src/zero_server/shared/registry.py b/src/zero_server/shared/registry.py index 90041ad..7839651 100644 --- a/src/zero_server/shared/registry.py +++ b/src/zero_server/shared/registry.py @@ -1,7 +1,7 @@ """Nested, dot-named namespaces of queries or mutators, as `defineQueries` and `defineMutators` build them. -Ported from rocicorp/mono `packages/shared/src/object-traversal.ts` and `deep-merge.ts` at zero/v1.10.0 -(905d08516ba0caaf314cd28e93a5a47e8acdd482). Apache-2.0, Copyright Rocicorp. +Ported from rocicorp/mono `packages/shared/src/object-traversal.ts` and `deep-merge.ts` at zero/v1.11.3 +(e91e55f05b7a2c631f77238e1dd6fadf1029a538). Apache-2.0, Copyright Rocicorp. """ import re diff --git a/src/zero_server/shared/valita.py b/src/zero_server/shared/valita.py index 94d2ac6..fe3ff2d 100644 --- a/src/zero_server/shared/valita.py +++ b/src/zero_server/shared/valita.py @@ -1,8 +1,8 @@ """The message upstream's `v.parse` throws when a request does not match its wire schema. -Ported from rocicorp/mono `packages/shared/src/valita.ts` at zero/v1.10.0 -(905d08516ba0caaf314cd28e93a5a47e8acdd482). Apache-2.0, Copyright Rocicorp. The validators underneath are -ported from `@badrap/valita` 0.3.11 (`src/index.ts`), the version `@rocicorp/zero` 1.10.0 installs. MIT, +Ported from rocicorp/mono `packages/shared/src/valita.ts` at zero/v1.11.3 +(e91e55f05b7a2c631f77238e1dd6fadf1029a538). Apache-2.0, Copyright Rocicorp. The validators underneath are +ported from `@badrap/valita` 0.5.5 (`dist/index.js`), the version `@rocicorp/zero` 1.11.3 installs. MIT, Copyright (c) 2021 Badrap Oy. Pydantic decides whether a request parses, and is fast. When it refuses one, the same value runs through diff --git a/src/zero_server/z2s/sql.py b/src/zero_server/z2s/sql.py index f28d84e..e7176f5 100644 --- a/src/zero_server/z2s/sql.py +++ b/src/zero_server/z2s/sql.py @@ -8,7 +8,7 @@ An identifier is checked as `escapePostgresIdentifier` checks it, ported from `@databases/escape-identifier` 1.0.3 (`lib/index.js`) and `@databases/validate-unicode` 1.0.0 (`lib/index.js`), the versions -`@rocicorp/zero` 1.10.0 installs. MIT, Copyright (c) Forbes Lindesay. +`@rocicorp/zero` 1.11.3 installs. MIT, Copyright (c) Forbes Lindesay. """ import functools diff --git a/tests/compat_scope.py b/tests/compat_scope.py index 4df5db2..176799e 100644 --- a/tests/compat_scope.py +++ b/tests/compat_scope.py @@ -26,7 +26,7 @@ _VALITA_MESSAGE = "unused: valita's own error message; Zero's getMessage builds one from the first issue" _VALITA_CUSTOM = "unreachable: a custom error, which only jsonSchema raises, for a value that is not JSON" _VALITA_UNUSED = "unused: a validator or modifier none of the ported schemas uses" -_VALITA_STRIP = "unused: strip mode, which no handler parses in" +_VALITA_BITS = "unreachable: the bit set grows past one number only for an object of more than 32 keys" _VALITA_COPY = "unused: copying a value a transform changed; the ported schemas transform nothing" _VALITA_UNION = "unreachable: a union shape the ported schemas do not build (optional or unknown options, a " _VALITA_UNION += "literal sharing a type with another option, or an ambiguous discriminator)" @@ -67,6 +67,8 @@ "81-84": "unreachable: a missing value always has a path, the property it is missing at", "112-113": "unreachable: an invalid union below the top level; every nested union of the ported " "schemas is told apart by type or by a literal key, so only a top-level union fails as a union", + "101-108": "unreachable: an undeclared key reported; transform requests are parsed in strip mode and " + "push bodies in passthrough, so no request is refused for one", "115-116": "unreachable: a custom error, which only jsonSchema raises, for a value that is not JSON", "145-148": "unreachable: a union value JSON.stringify cannot write, which no decoded body is", "is": "unused", @@ -211,47 +213,53 @@ "removeInvalidUnicode": "unused", "28-28": "unreachable: every identifier is a string", }, - "@badrap/valita/src/index.ts": { - "105-106": _VALITA_CUSTOM, - "124-129": _VALITA_CUSTOM, - "separatedList": _VALITA_MESSAGE, - "formatLiteral": _VALITA_MESSAGE, + "@badrap/valita/dist/index.js": { + "25-25": _VALITA_CUSTOM, + "59-61": _VALITA_MESSAGE, + "143-150": _VALITA_MESSAGE, + "182-182": "JavaScript only: a bigint literal in valita's own message", "countIssues": _VALITA_MESSAGE, "formatIssueTree": _VALITA_MESSAGE, - "268-272": _VALITA_MESSAGE, - "361-369": _VALITA_MESSAGE, - "471-474": _VALITA_UNUSED, - "483-483": _VALITA_UNUSED, - "504-508": _VALITA_UNUSED, - "548-550": _VALITA_STRIP, - "567-575": "unused: `parse` on a type; Zero's wrapper calls `try`", - "592-602": _VALITA_UNUSED, - "640-648": _VALITA_UNUSED, - "684-704": "unreachable: the bit set grows past one number only for an object of more than 32 keys", - "check": _VALITA_UNUSED, + "259-263": _VALITA_MESSAGE, + "289-293": _VALITA_MESSAGE, + "344-345": _VALITA_UNUSED, + "350-351": "JavaScript only: a bigint, which JSON cannot carry", + "355-357": "unreachable: a null or undefined type is only ever a union option, which the union " + "picks by the value's type before it is tried", + "368-369": _VALITA_UNUSED, + "379-384": "unused: the Standard Schema interface", + "409-415": "unused: a parse mode passed to a type's own `parse`; Zero's wrapper passes it to `try`", + "map": _VALITA_UNUSED, + "479-480": _VALITA_UNUSED, + "as": _VALITA_UNUSED, + "515-515": _VALITA_UNUSED, + "522-524": _VALITA_UNUSED, + "539-558": _VALITA_UNUSED, + "577-584": _VALITA_UNUSED, + "588-589": "unreachable: a matcher built twice; each type's is built once and kept", + "605-611": _VALITA_BITS, + "615-617": _VALITA_BITS, + "623-626": _VALITA_BITS, "rest": _VALITA_UNUSED, "pick": _VALITA_UNUSED, "omit": _VALITA_UNUSED, "partial": _VALITA_UNUSED, - "834-846": "unused: the fast path for `record(unknown())`, which no ported schema has", - "866-872": _VALITA_COPY, - "909-922": _VALITA_STRIP, - "993-1003": _VALITA_COPY, - "1018-1024": _VALITA_COPY, - "1106-1106": _VALITA_COPY, - "1132-1153": "unused: concatenations other than a tuple followed by an array", - "1268-1271": _VALITA_UNION, - "1289-1296": _VALITA_UNION, - "1324-1373": _VALITA_UNION, - "1432-1439": _VALITA_UNION, - "1540-1558": _VALITA_COPY, - "1588-1597": _VALITA_UNUSED, - "1618-1619": _VALITA_UNUSED, - "1655-1656": _VALITA_UNUSED, - "1675-1675": _VALITA_UNUSED, - "1715-1725": _VALITA_UNUSED, - "1804-1804": _VALITA_UNUSED, - "1816-1816": _VALITA_UNUSED, + "676-681": "JavaScript only: a `__proto__` key, which a Python dict holds as any other", + "722-726": _VALITA_COPY, + "735-736": "unused: the fast path for `record(unknown())`, which no ported schema has", + "857-865": "unused: array length messages for a shape no ported schema has; Zero words its own", + "890-890": _VALITA_UNUSED, + "917-931": "unused: concatenations other than a tuple followed by an array", + "962-966": _VALITA_UNION, + "985-985": _VALITA_UNION, + "989-991": _VALITA_UNION, + "1016-1021": _VALITA_UNION, + "1032-1053": _VALITA_UNION, + "1105-1105": _VALITA_UNION, + "1189-1198": _VALITA_COPY, + "1318-1319": "unreachable: a matcher built twice; each type's is built once and kept", + "1343-1343": _VALITA_UNUSED, + "1350-1350": _VALITA_UNUSED, }, }