From 17bfa6118d6658d2bff53d7de8e2ccef5681714d Mon Sep 17 00:00:00 2001 From: Leonid Date: Sat, 3 Jul 2021 15:41:53 -0500 Subject: [PATCH] Add support for DynamoDB and S3 registry (#1483) * Add support for DynamoDB and S3 registry Signed-off-by: lblokhin * rcu and wcu as a parameter of dynamodb online store Signed-off-by: lblokhin * fix linter Signed-off-by: lblokhin * aws dependency to extras Signed-off-by: lblokhin * FEAST_S3_ENDPOINT_URL Signed-off-by: lblokhin * tests Signed-off-by: lblokhin * fix signature, after merge Signed-off-by: lblokhin * aws default region name configurable Signed-off-by: lblokhin * add offlinestore config type to test Signed-off-by: lblokhin * review changes Signed-off-by: lblokhin * review requested changes Signed-off-by: lblokhin * integration test for Dynamo Signed-off-by: lblokhin * change the rest of table_name to table_instance (where table_name is actually an instance of DynamoDB Table object) Signed-off-by: lblokhin * fix DynamoDBOnlineStore commit Signed-off-by: lblokhin * move client to _initialize_dynamodb Signed-off-by: lblokhin * rename document_id to entity_id and Row to entity_id Signed-off-by: lblokhin * The default value is None Signed-off-by: lblokhin * Remove Datastore from the docstring. Signed-off-by: lblokhin * get rid of the return call from S3RegistryStore Signed-off-by: lblokhin * merge two exceptions Signed-off-by: lblokhin * For ci requirement Signed-off-by: lblokhin * remove configuration from test Signed-off-by: lblokhin * feast-integration-tests for tests Signed-off-by: lblokhin * change test path Signed-off-by: lblokhin * add fixture feature_store_with_s3_registry to test Signed-off-by: lblokhin * region required Signed-off-by: lblokhin * Address the rest of the comments Signed-off-by: Tsotne Tabidze * Update to_table to to_arrow Signed-off-by: Tsotne Tabidze Co-authored-by: Tsotne Tabidze --- docs/specs/dynamodb_online_example.monopic | Bin 0 -> 1601 bytes docs/specs/dynamodb_online_example.png | Bin 0 -> 92813 bytes docs/specs/online_store_format.md | 2 +- sdk/python/feast/cli.py | 2 +- sdk/python/feast/errors.py | 10 + sdk/python/feast/infra/aws.py | 141 ++++++++++++++ .../feast/infra/online_stores/datastore.py | 17 +- .../feast/infra/online_stores/dynamodb.py | 182 ++++++++++++++++++ .../feast/infra/online_stores/helpers.py | 10 + sdk/python/feast/infra/provider.py | 4 + sdk/python/feast/registry.py | 75 ++++++++ sdk/python/feast/repo_config.py | 7 +- sdk/python/feast/templates/aws/bootstrap.py | 35 ++++ sdk/python/feast/templates/aws/example.py | 36 ++++ .../feast/templates/aws/feature_store.yaml | 3 + sdk/python/feast/templates/aws/test.py | 38 ++++ sdk/python/setup.py | 7 + sdk/python/tests/test_cli_aws.py | 58 ++++++ sdk/python/tests/test_feature_store.py | 27 ++- .../test_offline_online_store_consistency.py | 45 ++++- 20 files changed, 678 insertions(+), 21 deletions(-) create mode 100644 docs/specs/dynamodb_online_example.monopic create mode 100644 docs/specs/dynamodb_online_example.png create mode 100644 sdk/python/feast/infra/aws.py create mode 100644 sdk/python/feast/infra/online_stores/dynamodb.py create mode 100644 sdk/python/feast/templates/aws/bootstrap.py create mode 100644 sdk/python/feast/templates/aws/example.py create mode 100644 sdk/python/feast/templates/aws/feature_store.yaml create mode 100644 sdk/python/feast/templates/aws/test.py create mode 100644 sdk/python/tests/test_cli_aws.py diff --git a/docs/specs/dynamodb_online_example.monopic b/docs/specs/dynamodb_online_example.monopic new file mode 100644 index 0000000000000000000000000000000000000000..6531749dd94641272f886b7c90060f89bfc3883b GIT binary patch literal 1601 zcmV-H2EO_KO;1iwP)S1pABzY8000000u${T+is&q^j9oi*HM`Z*S93Cs&=>9ULRI< zgvKzo>0)pJJDWz9_G_ws*M3RQ3=9SvvcX=GC|M~Y!+7S*xzD+9t0mLFxSwa5dn<-# zSPY^t=3cjR)MTXK&FVP2anm-(S4kG;VG?^Op89No z4mm(-fVC^2l(WE#lbnei^3)*l#ZG`IR*BWwI-N0}djNRHg93X+R13WspD)EvKy4F~ zCIZ=dT993?x!M8E!w*Zgsv%?G9^fXXA- zr|nQ|phpB&e!fVP^?XsumE}3}7lQVJ^ld?dDHNzfvdbjkk(cuid5J#3aOQHAdDA2f zc-qRe1044cE+P2u=WY8_un88=*AYw0QzRp1sZdoG`->#4@e>C3kS;iLXfZ*UN~xBn>nVwVhTxj!i!qor7#C&k_tYun-XqJ;-Aw*1VrwL zU1xCVWceiCuxQOQcM^a4_}>I>5uldk;Gkara(;gP`qB>8?U;FK5LRYP9wpEP#~=lV{9uJ$R5ne!4=dOv~f?+R63*jRj8c4xc}}v zQU=e(W!2!1t~7}b#}H>Q7fPFU<#aFspcilzvF2d+5H&hPBm%+A)jDd>Lco?|IIxeV0))?<9@Ms{S>2qwhxFc@zfDjuyj?HJ> za%_q?^=%*Plv?&QxVzhO&}#Yq;;r+_Zf~``J%9Diy6$YX{PbzgA*~$7q zh*cxQ8O&>9d$=ljSCLNk?@y|v5o5S(L_lWI3c^YOsmePu{2D`NZr^ldT=p5FR-1AgXXgRON`M(h*U$BVxU<51yP86Ax-1*F+}Ag}Zp{ zsLPw0$APd(tqf~)eH0+SqdlqP>>L@;n}Wn`0bf|ut+vrzvfcgYxN2+F-$C;`k0w<%i;S_>)necJ50=s_XgD_Re_Ff|#ume9&tHvQebQ>DrLZhI z3}?!&R?dbCq`Mirlg=8)L)vfPr~<{-TQv_Z-fpKJ`+%1nkCsus)iY`(EK|cU5|%P; zO{`b-Zg+L@JGFa{p?Z%oF+|1F$J0O)ZFIEGA0CtUEYDokT|b%amnus`e_0BiE#@6~ zy>0^ah|^uE;|C`O=|h?^yg}*`uudgkf7Mw7rH-GDeo{G_9K-%aPm+=oGht3-N-gtbtN zj1QZOPG8EHEvmXPo=h4nkWML~bU;nP4#=m%T}B@~n}~Ou3JQ6Cw=v{RCYxcb1iCr5 z3l8)gy}hqt(@RBm_EAlJxs6T=wYpBvRPJ$2Nm~BSg8s{3q`EnG@7b4|`fXfO81Ew{ zGYkHiF}`1>VPcF&;Tva|AM=E})CacsLg+g>wUpn=%I^SR3xu~ZlBd&kv z!$TpApl114b1L5Ft6oF<6GN}o$v1j^vG3hN>;QB|YX;$#ax7Wwp|)@rPEusQXnTD^*4uC7=oVgD`Z;;XggfS0JHO1cZl+cbRyy9?K0?*Kw|wl|-}%idp8G_3GFI4e zZ2p0=>7|dmd6DxNlBpf#B%3UY?DxPM;@7d8tOOJ}d*tvlTu9Ma_!rX$ zYlEts>`z=D?_kIVp}ZU1K!|29wC*>r`$fK6^@b#B+QA74LNZ|SaN#M! zip0|az346fY+U4B?1xw2cwk#xHN7VCUMk@Qtyk{GF$b)o?M4u#tO&vDU>lG}(BWmS zi;uj}e3^fbpnpqJiO0g zCs&~AGoN6esNGPyjiv{I1YvEWA#3{X9TYwA91#i*nh**ecm@sJg`tW5`CJN`4hr_q z>o8DIp%zeZ|2Rewczpi(2Hc^4j1CpK0Cr2nK zTaILhPW=_algqkz&h~`F^`!ba}C|G{G^kgmpBuSe6c6 zJ}TAzJf$!*S>RYG*TI`b3H^%vKi>RbQ7hNH-5<7$g+luFA8&rtqW*XYF#pAoqOw?- zZACvp(ebqZ@f@IEWwwPI`N7Hj$6Mx4B<0l)<4e5*|IO(tuXeME!chIkoANju-3k5f zk?*};Lvsa|2(eHujnjbu-)RFt}D*|^BIT&huXjV&*Kk;6vqjT+$7fs(QyB7 zCUZt7h$iz&;1PGj@m$N9&r<_r-h0%?tQFKNk?R?iIF8Sd(g9t1-C!G%DnE#j{ppf2Di2AEzYevZbeQy6h%TL+q zdEqrO8wCI7rSAuqjgi^LfXviG{a+*$I(}e9;pilmi~l!^PX)L@oM7CI@BhX1VUze* z!<02Rg6ENaujb4o)$Koi`7?-_XBexH{Id&ZX3c@q?9WH4b)HIYS+wAm^5ADP9U7;_auCr!>E%T3G@|1)P!;jd{$ zhi-Wi}hu*MI&yUl8s_xKPkL zEj+=YX*1yU`Cs2b#|7i{6t5Zm!P=Womp(y6{-25Ziz4~ZJ#|T25pZI)lhY+y(ky9j z{xL)3(!hreVD`ym7T=hZ&n*oymfe>BOsAW%ZPH}k zR4YCZo1NF(OHOKYzE2Mlz35>#`(>@qX^|>MrVn!5$|7)Bt)^Tp+R9K`FK*lV;G%@T zt&!n zi&x9}h(IOe2m}sxmNeB&7Lms3Pcs23Rvfp#lJM=@md86QB}(pPH={X&P?Dea8$AK2 zyjwfVp2tE0PofKGysJ=i#272>(?QfM5v zhcJg+`CX3~-~Hf}4Fg}b5j*rm5S$J3tjxM{yEGnyj$2#~he=otyyM)b>+Lr-6SQnA z&POwRALnpSXIdXWguEnpRa+m>Jvix0=5v!Zlp!Kw|4{Dp;NiXz z$phCa$=bSPQ@8Zfs5h!|BT9fKK4yec4Kn_V>8hM=gHciB{%nxt>+|#z+9sWx$ zp8g7A4UAljj>{Z%o?9h}S^mowGt=Q8CcO+k^6Nb0K3H2;8%A-*zqG4a&`z|yqB}0)J_=+c}>xaRNlhPUOd0(Ofs+}z~-9MjH zQ|ixG$vHr|y4x>f8Q}jIA{Q?w5+^6b(3xaF1jJxiN*jsmB1N>wquY+0A@li^x?XS; z88&7>=~R*W0=fB*xAwy+JaysZGJ#~K`6y>aFORNQeCr&!T0^v$7ak{+MEcLR2H`~% z=!*tTy%LbC2G02CFTBdfw&DlejD%z}wGm&E32T#jowT=YPnKw@KVH?f_P6=N{sOMm z-(K!F7EBAXP9twe@m!UeR(nt@llR?)eURZJtDppL&@ispQ81qFH!MY73Ja#a!lF`S zY;psR0RppB3JlRZof^xjpT19z>_EWxJz%(`U#>hos-y93V&v@rv#edHUY1@b>6({` z&tq_RT)SW!>-%_~LEtEIvl<9bbU$S>f$j(%iEr_S3=Ac6a0!5s+bQ3M~_~7!KD1@ayZW?hV7b~bs`y#+y9j|olY%RLADUZ zCnbuQZyXsE3sZIS<*qsI9c29@2VO=F@Ht1vIpWbASuA^rF`McgN9L zdp2B4S4DXsi;-WbF*eJZp~JTbBOXb$s+3&(rMwIu`m&}oxpAs|{C&+4XZm;_x_FO- za}9$9AmPXbLTxj+J%LDqWLA8inyssUOO2Q62)KT;u3K_R2aAim#Ag*fLAahZik^dy z7N5yV9bE8%93Dom0KrELv9+{Pw3k#9(TaP(h9OEhbYkDIS+iEPV@ zopd-%CO} zrs@~+aWq{BUYv{3y7LT!lcyAP#INL|u^C*B5`K2LP?xCVx-@kbngGLXCqakQKGS@? zyf2$r6bj^+6;pVF_MP83l%MGPK9F>5W92@zEj?+8p7-XuT_6xwgBO{f=VYOxm(;2z z>_iLJDrfAw7jO=n-FSM~);)o>n?Mw}y4w3+8_eS+mBR^kMpe~6_>B_Bai6$LuB`Jc zkN=Y25_!6f7PUInd%8<}hzQ0E327&AO{MtkB`pezU8`!4{g(HLAS3J2@s%7 z+veQ9SiOkEnmPFXPVja!7AeQUqBD{&vuhxpA-`WMQm@<%C8`qPEat?u8ANS)65 zX=x+Me{Vc(M2pZw^b6&cu6SRk(F#sT%(^GAU7HTF5}GGh7LT3KJtP)pOJ$lZSi&hI z8C{KGG1-dX+YskPr~5v-E2ZOU+xeq%^mb5Pf@rhd`y?Uv?zj_XW4=u)>&HbRrR&%Y!+%OfvP%}#&pDf zLF(SATe@Ok7W`@T%#&8AGXi5TWQjB%eSkv`Sv5~F?Pt9Y`Xr`{OFNW+9l3LGP>V{A zRC<27SUAZGyV)A3I_o+1XhN?0aw6!6Kw_5y-_ve9Mdg`Nos!0ejhIfNI4+BEDmE+z zD`Z4X$8yhbpTIB>zxjhjMgW1!mtT&tv`V$0!g9}kUo_%75>SS|BLT>WklWh)oe+g> zsW$T+#gW_B@fPh#4)viV^ydxidm0#n72krSw#oJ&B&>r^hHk!VI;fz7TRZy|>EKi) zeOqJPQ8*eoR@$I96i=n|MWjzQRPItbP ze}JC>Cqv!}mnrMy$2YUL7D`sT5>1DCjFRe)wb{``=Fp&v@jjH*Ckap6p`SW{V?dvD z3^A#LDjXZZ?6biG9Ga$fV?xl?{vG~`-Sc+O-0;?4Ar z)sqL}ivH9@zV1YU zzg~bPW*@^L&DigA?|xZj^n4xQI0Dlh@lJT`aUC3ATlW$Sgr7eYTy9Qbu%@Mt2gf8) z*wdr349~ulk-oE@6rWmXAjHw7T_h&YP%fY<%=0yUz1r0qd8?8f;HU^ zHrn{npGjMqJoJn4d|aO8vTJyoV!`K&fNpjz%Yuh#=NAjvOl7!^mT2S;WC?1EaO+Wc zY3f10Pr}X~)ijSt48_i2sv0N_4!&Al#LeXYcmo7+zG&7q*7kid+&1cwJKT|yz#(U^ zDwK=Eiw53vnN1^Hw(kQ1Pxt46VdXxv!IAtr?pyKx_CvR5+>6#<}Q|OV-UhDUAKlcK8&mCOd#RyGI&*PY$S|CR##m=?b9X?SJtn<^bpuND< zPckx@qioy}ve}t*75B(|WD)1y35WL_mJma+;-IhVEl>0m^xSh8hyuC+oK4u7b6%@W z49tx?uPciLWqcc zgHnb!6g6^*nxwf#GhLli-4h@}CGH41Y9;Mm$=6-(WDRp~4@F!xn|V-9Mm(2NRSK4~ zN_X$tQ+-V#`RZl5vhK-|Tu4TN1|_A;83Ju3NygDjjz|tlKZm{bCk2*Il~e}YB(rwrg76Lb4~hH7zZ4E7 z(l$;aAbep0BMz64r6uX)9)k6r+sF5xDAO#0WE?5_Ag!ahL1uR-%?(TrfMuyIu0^) zuw2mf!NXOf!VKSjf=FCW+k_d2U3^l$CI}fF0A01SwEVoqIuhd;>>ch8%~jMA<^#77 zra5U+_pWK8V*ODffi}A@hoqhd6jI)^N6S4?wCLzbTLpKv?){W+e*jeeyAP5Upc2mO^y2 zla!K%Mf~T*#BPnx!Xd%~ai2>&+0;9{5PqzIQyrtyu2%&Ef$gK9s)_+PrVrcX2#7%L zr$jv;z6~!7@b6`}jYyXk|8uT;v~vvJA#Xw++XJML4)CGN64DNBz1NB9&1BEYTVzalg;|;mxd94_FTBxjT5+OmhVJJ z;GOSCoNg~7-GFa)qdT~L?Flq0*ATT{vAFA)+&6<({JT2%UkS$ZCQjI4c@1!}i2JK= z&lKs$n6NGYSc`{t3CXwQG($fJ4sDLrOElwI zJ9?>}wB6qBhFbT?*B*wej%wQ(kWwkr0WE@ba3T8O8(SFrUU_WPvixHa498=P&fDA` z&Is6XMklHWSsqAKC)$b=bFrx$_c0|?E559-XnrQa08VcY$8Fej8S5I@89v>k`sKz~ za;E6qw_9XzT*6K>vk3<<%Ie$xGU`QSj2-y8&{Dr26RZffwVSoyR@`VAokTyX*XZO* zK_#(zwF67Jqk!)?ePll3y?DB7d;)$qYi}Aym3n^5iq}nGRE`+nA>N6ypDV_!__THA z`NkTueloP}xM_;xlDf5i?HOmEFnpb7>9*_o74l=xd(%`^?i%QYKcM=h@>3R93aFK~ zK6-#Uogs&Hbzq&WZ@~UvXk!k0R(5ZNq+#l#*4+84>g?@KD=znP&?IqE!SYoYNjm*k z9x_rTjYS05@%i!Kt5;MdwqMyth*PNFAW#rp{qH&&a6KK$G&q z{sgwxBV<44Ztcob!XHls&LuwXO_x|TSE%WvrFZ+E^Gzm8@Ra1@y7(4M*hQ8-&Z8YX zo(as(g$Qkixy(f*s+#Lbo}1i0qnpYv;j&wv?wv!XYj4F4-8RxnuI932zoR4$1Nnia z%S#+=#rqk?thKOHfX)2q=bG3I>v>1$c1C_*eV4ofp$4*pUzr@dsK!#xiwdKbIrIYuT` zI|0;U4QX&YYwWz;x^*bpu+hzX4+P!wLaT$d1rnNy7cpX*w=G8 zY)@k!fr$A&?R!9-g;(mXGK&ldRae$I^qsS;=v&HKw9T*KSPO%%o_r_5m-#)`s;cC(Qddc>< z%`+*yfAhT`%vwmHA82Cr+c|&|cKv{Jfb=%^W;xlpor;RWn^9$qOqsjSd3wnbS`>rz zC6dkqI}zrX=&I7)?|JN2Mcef#h-n;>Gz?%O`k@stJAl3I=c_v)gy$F@osSmh99YeY zKV5!E5^5VHL8uSJQGi>^^hjHM-LkO8suWRny3woEP8SsXB~YRxa0tGB0(Vb1^!f+J5r*UO&W<|LrN!{#dq=Iu|i$p~)70>@wPga>k;?x&>5+ScmPaxmA>6>o`i z$yh_1E~m8)?TKfJgB#%QP^5jDPC8H&mwDN{4l6D67BBn6^Bn7VQXs4kt0QE&Fcs=CA)T8wS8EEZOHbB;>{LEV=DW%2`@mLL&w(uNSksz%6pt+?S}0+pNi6C#~h0s ze3E4~U7RcwJ&+$7YUqEHym)_mZWBjtfCqCDNUW%La77XBTb@^pCZ0x)aQK=R$7hWc z3n$eK9syFcwNq}?pX6%)_Th zCTmj$o+rF8wqSEtZGzaZSJpfS#D1Oi1f1sIR)aB24b8E(cJ&rd1NP#?A!k96bI&x4 zs4V7}yT@}LMKHas??VMl77TZ!Xt*E|kksf>9CC7}+|z1T(n!HhhPwVbkp|jH?#4-8 zs@;62U0B`3BeuTyMvnoGpVUjWcdf4+tnw{L8xBIf>Xh~)NCJHF&KY#ROxPW^iVqLg zXkT-!Xxot@Gs@^cA~UftYOHZsq6ODZ@$MH_El%~{wfrNWao7LPeD=;dRm^ti!&kO; zQy!RAMjMTtc2f+a!r@rfzXDDPUR+S0k^>Iid?H?5_l?0!$+!tB1vz)O=qfyq)ih&t zn>^T0@yx4&|?i(#M-b2;;B)dGM{0IxEk zUz@JZ;^V||igjyZM=kngWGBZ6SZ2=H^Mllp*M%q7(mx_f$+h`Hcmg7`5x-GmSxN*S zOZKgBV>A^)K5I{4GM>4&u)Ftb``~@%dUO$?)MFavqnkm-hos`h6JXNj$|;IMl<*QU&oUt=v{#)Fm51XLzIBGB?Q?2~easvO!| zlm2dq5_mTWv0j!|862Q#v5otE1v(GN7};NZEI8^MqE!2Zx)B|iI?PAJy$9q?n=+P` zn{2Kc)Ct#S&uhg2n#6~1$5$3xIU3Ys6@;9*F&S5SlkN?A<+;u%jkp2f$Tj6RevR#K zvAG+k3e=?aRfQ5uzn!;hdu2%4cv39j)!0S=ge^dDaj%QYBesHvKWTk*MA}Y1)iEFW zAS;J|{qSwrLrj&L-L3djRnI41KR@IY$4m48BxeAmuD=YmC}c%5B_-mrG|nlk4bGjA zR7+A0x?-*NvTZt*Lal{i75q6b6Tn*hwJnZ$TULX!C+2m5HZ%BB!GIm4H|=>ZyjA%$ zhKJ#07>HhN(NRzL;CDaNVC}+$;@F#F+bC;S9Lh&Kg&g60-lVk}4}kln(zyIO_#?^d zZx2J?->z}|vTD>waRTvfOr+@^cSdpGv=|CDwuXO0-^+JYip;z1J#5wXV#z`?SV%-2 z&Cj4Ti9O`nsf&8_i9CY{E%Am!9+FFKc9*4h$KyRTM&NaWp|a;yEwyI!a__7JecdL_ zxU?#b`@iSQrm@J^#c?~(u)PxHA_Xa5Dv1QKRmvs5Jw!7C!@7vB-eY<-6spCI(zRL>++}zQg`ensn zrV3f%-T>#aSW_FrEz{O=1Op@*7{uSO6B=e|mE_>uQgZ&;m+*4NjWCL>)M(BIRkYJt z@CF=1n11+SaO8q-+@bs?IMVti)vzUu?7~hXXpAgxnlkhWnT>3y`})%&4PLgl{eF)V z(9$I%={wTn94kixaTWu?U}ICL1=3fUA%-DW*ET14bWGVd&XV_Xd6Nv z?$1Y~^csnpK=0L1h$t7)dim>@0?~zce(#0eYV-OEzT*J6M9*5PAdWG?(2vUe>O1Zq z7u}U=k0}pxcppL^1Cn`pQ#W47zfIPj^clM+M+||&L58VYbO>Kfea|aIC(5}7onMbtH{GGC}W~IdhvANu!Ra< zw~#GFBAepCPC!QcLr2N!;+p-@DLicG*HC2pj_n+a3N5+=bW7J|em)1X>>V^EXWtne z*SRIiZ(bWLmUr)O8z1a>1yX%}H+Gg2!Gs%&Dbkj+dio{u3y)zWv}4045L!N2G-V1d z`sjNd*34+Kq*mo}D=pbqHb;-6&x7rIYs~O=0X;e2-Equznx2 zKa5S?%)UbnMXLQ#@SF9T3lE^bhG;LO=YGn@rk;z`klY!`WQ4C31~3gdk{@&!%-GQ* zp3ku1J**+gA3TWdW)rf%(W5UzlW5Vjiv{PIGb15Rts+@8BH!U7&gOo zywbqfn<`4=O35eUO_gaOI?NCsu38`QTyKhYnDWJ#-qXy`Bs(*>5bW7SPkg_PGI;i2PZ*U>728 zYYcOw4tON9jSD^Q6?x4%y2C+uB_{Q<0%2)jrEsOJ93NFqL~{fa-|X6#h)fZmLSFS>ET)l z|J+>>+XymyN2JDxMETP3UTrCftdzTjkB;+09NYj0SZ>bE^emoLk2k$uK9Tv|O9FN( zgM(Yo`&0cO79_`;jCWQ;i&POhO0-e8-{tU!^Mfjm4{mNh56wYwmn3&l4Job%CC}dE zzzL@;z2+vlgY(TdDov}fw>jrV!9}Vv>gr#fJypbqPEc#Xl;Sh+F#5To$b0wl2p}3= z@0HD5(|;U&_JwG5c=b~a+0tBAZj36(S!+^KUq>L&swVKL+)<>Qez$>SPjbjpDJa6g zQ)66tzaNvUD9@(GaBWJ04LbyNrHaMW+5ll`YIxiP9zvfF!i?kHS|$17Dzxm|^FDop z%*n{*E$zvXq;}P8`WZ>a^Nx@S$Z3br=+(Wmc{ABqQb~!dW*mG&tmw!ND&QC~cYG!i ztthtN#Mr5uW@topr_NEK?r*j`#h4d-08%PoZ|hn@=r_U*2a4_EY7k%BQb;lft)PJn zo~!P1g^_d$ZK>3b$$2-pD=E@xFQCEU+hBtH>}U+g+RHQ^vsC{(x_aNO{vrTOfO7sd zFp=tc)NlnFwSJHK_8frw1y&+W+b6!p4nru8Jp9$%&}@fJQ4~P|(+PlDtu_erj5;DD zIFGZ+|Bb;A=6tm&8@~ZAkI^h0Q^ZGdgCTpRbI_Kp_Km~xQL%Xl zO$I_eU!uihW)DSn@qWZSn0hf*$kENSgV@~S=>$Gw0LRdOAW2>!iG;S+27@)=E8Ayt zKK~fL!Hy7kq$Z~t1m*TU^%(D7=>)}s=w zfhH*ZR=-?Y})m( z{qfi%1z3D{vO-hYDNee<(B6Kxr6XnM)FCm67lYxR1V{trPcPpG*_DO zUw5FR#{KW(rW~W=LmkW*ls>8MO3cgBn(Epdml-`=+f`Un8zX0&W*tSY16JG*nMN-* z;PyQ?GI*oYIi%-6a}v-7t6w2NQ8>Y3xry0rum&pab(YPPLj^wU8VYB$D&I?uoblrVhH)b2?k<3YR->+ZoVDD4J^K7Jjm=1Ez-dO?F*1+w2}WZe2opA*y5Uujj4fA$s$#uo1 z^CX}h?o(PW+a6?Zs{xIemlfI|v2uoiCN%H`D)Q2Cx`Ko-(B<-OP~!op)#$fjGtO7} z$}k4H<~W*SxVrD3>9&LPPEUB4Lhm}I|x31 zdfpJl=xtE}2=Gn_IbxAo1E6DAaxZ(lZhg9Z3&6i$K)S1Mcn3o*CUW^~*tcKCIt2Dq zA(t#3vn?8(N3hi;#PuBd!|O5Df+h?{Vl4wlBYBp0zV0i6ThM=ijDg7Hmmm*x=qSDI zPQKn;_x^b~Jn2h_PG~j2lApnqu3NQ>U|l~Rd7()&-F0`d8{4)WL&t0l`DXS936aqG zBW8i35Ztcj2T%F5;p`9gq=L*MUpcEL4&OxZk0@`4;V>NJ@_%?~H**PSF=X_Q;zzo7 zTl0Zd!YB2rd+Yy%B=^Qkg&ZG%<`W+GVT`v*x($zKO-6s08JITX^Kkc71ruxHlfOOv z{Df1;>X6^FqG4!Lbm;Ym`M~9eORhle2;_z&c#;NyWxQ|qA1&{;p4991y4CP3=vj=y zx5;g*_feYNd#g#z8rJzKrgk#z(?!gv$dk665AF@oC8UWP$r}waz}tmsR+}7*$p*6N z&R^QDS*%Z%CSmzC-BPce6a^?+>4L9S;;vY2eUPv%n>N=ULa)JqfDZnuaoBdWDc~7Y zvNBOMj^cO&=*g%}Ibr~`o_Rc#Su#Nqtb_c3fv36X&L<4J9h!!Z3hh~IwRnaSGory! z4t8C$#xWXT`7DX+A)FL`Tt&xO)z{qhQpl_dw8q=E`_^hpD+un=ZgmBd2#-Ex_E1}q5cp(k{fkWeHhRA)6#ER2fiRd~AU<@;mxg$OE(A875Ii2N9BP^#L*>QInDGx?f((02d1J>p zGEj~f1cFz9c8z!wTPXYw?}68B@>@;R4%@8CKr%up2ZwHZQ3Hb4{L2vjRAnX$<+2$& z(M-}OD*N6iT41C)Bge~k8(3po37s{Y@VYAe(5m)EXuULSG`?N=_g_^GalP6Ai8&@U zLk|-)Oix0I)86D8hj)6u4?t7bH<|Rev{okbHIm(OEZN=cCq9=04G`qVrnhaW}*lXzU)dLbA)ev+tU9@)wM9;@%s$$aBU90@s zYeF_>?3ITXHH)EA&V{R8_ON>Np_^!7>NP!mP>*W<(aLS#v>DCh_)pO`0LTFX@WS1& zkbbm!K!KMR-oyHji|=EA2HK;rx&wBw+ZzzGN#I{+Bo0W-S>ULo>=EsncLowFht{&09wdA|(X83=<`2VYn$AzbEf+J$o3%$-Grr ze+Vj(VDM)^WDH{^YN^?Wr9p*Go96T#U~i?KQM|rqT`N_MA=f`A;7k@UDtL{XEdOgA zKj6hA-{HR)lTe#(`BY`fZrq3AZ&xt8_=C#|&ev+Dl&Q9G?oRM>QjOhmk`B-{2WtC1 zTuOfG9Z)V%-3?VOFf+mDu%-myX8UKmQavQJ=3l#}{E=zLvL#V07V9gDwdxd2M^fJu zsh1^`wLWSBWsI`*@vi9Nc%`*tf2S0RbBy2RfDXu;&YpO~4}U9Me(y?CF_pi2Ls zOn*7#=*s5#JXU=&!1YCZ!T0~)diTFZfSw0B=>;*XzF%;;!EXq$8@*Dud(ge6dcAtAUB6LCx)?|%eayUx-f#q;Fb8z<7m@MjSUzlzq*;{>CNN>8 z#%{X+s`@fN`)`oFqO#3!mKM8!MwdhSsUonMJG#lU5dIkt0~9cN2Mso$gUax!RL4Z5 z5|KkS)}jwZ32LpGDgCjugM}Z8p0kE5xGifXPa*mBIe0WNl6zhT&S{Usbc!$U9e0 z>ZNFWwhqf8F~kiQ;NoI{SkkTQb40I-n$h(vSp)>h0p}x&XfRp8$S92nL#%rt zfROzAjGph3dk-oc`J?0bvOAcB;r-d$`}4Km(quK@Tnl_S^UM92nch?#&&xe(K-G^r zgbvg2J4af}0Q+_3p)`qeoDhf2de_(53>YjzCD#4rW)N2-l7S}BEvM$QhTry4M)Qsy zrO2~1zX$f=+;!>)5NlwE?)G)Q#d@EB2m|X6hlBLIsDcEdKm5~L@{e@zmu^0Od znR5Z>cg}mD4B{Yy$gAhw!D(9n-d+CVUF*{$NUCB6Fk^IJx{&zXtbHs(`5^@mcjZ&u zdrP1?A&H^bKEX^c#L-t@gHQu9tZOS=PxiuydH1C zvh$iUb{$Bx8WU0VB0HIZ#Zvgy-!qfX52{87cvidu$$Qfr2Hr#|+jRsXu{-a*_w1Q1 z$V=brYDXQBD>4sl3C_iJlPE$F=SrP>;g7wC`f(K|5(FfgfoB~Q5c;*E8ll39Tf#T1 zunY$ZIaKuMu<9XGw(ei*rOe&6y;As`9#vU>FiD4Ae&ppiI(oa-2K~ zYY}&=2V8Q4AGK_@3giH}G2FZ!>jom$cc8YGqlV6j;kdcKx*;XU2n=)~zf&N=?-2Wz ztIhS2&;$Q-g9D&0OnDv#`~74F#i7vW1Sl6HI;C37xv%g)tU~{DM3i}9SU>XsNuJ?- zfU+SV)4)r0g?GK%&65XQJmVj?0Tf(`eF?!JY=Lnd&<9y^x$C20h||;shRiAo92z&L zdmiz*Djz^-)7`{BmTFtnBxW{j2+Lc?zT=JoTD#HhNRQhkK=ZBN?6TtR>TfrAY$rVR zEULO5xg$u`sy1J{7gM7rJKD*KR($1>l}ak)A+yxzY!kz(R)A)Tw)Q?V7($!jHEwAs zW~G!CV&!!?<07gxbWDzK1y~MOK&zvFNjKX4D@Ffge^_Wsw=HP;DyqL$#Q*x&-{Ccs zf8vt@ZBi9rO`6yQ95y4flfvG2#t}I%*oZtp9ZmUpGsb+k8LRYmgIe;-&gq<4#vJx@ zvP8^ZouqmV2wLunF11>`8W?q(X8Ed+(^$(`$IBMDiaJuwi41>?6J%4S$=*y7EnT+?Z zyNWmp=+wcV;uPjLoL19A>C!cwXaYf&OzUP2K!UDQ2?)|bG#|_KM;(oCvs!L)=N7pS zRHp4Km=nVBp%?*5n89dOhj%Q^JG(amt|NTlKETV8>LRA`0pwx>X>vK#^mNWB3TCSS z3{0B-RMqu%+cVD`brT@7>I2MD41g2Jwf>f?DT;rO&WipN;B70mU+Z}Sm^AK&=~$*% zfa6H!t$Gaek(*eWe3my(e}c(#D+P44?+;R5tC#2t|LbZi3gb>`cYoSD=m!mJ2yWd< z&@xph`a_EPgxWvtLw2#P2{rbk1zn3Vys3{)Bv@?d!iA&T1y+T)l0x)vmj`P ztCb@adEi)h4zlhNc@m?0rt=t9I|%G~S|mi-6dKf;Cr({uQd<$$7NB64;EjBm;HX~wn_eQHdaU^SZvp(jRMUH0 zz`Ob(A>CBq`O^O1Tjm!eVxSt1mm5fEb^601c)-c3Yug8l)~f-n86V`J%5fYy6(!j5 zmp*+3U*;LxR7ytn!2;Pk2T^j!f z00gE48n^`AW{){`z!4|VaJN@P-Dgd)=d|QJGe^IA@CK1eALazW2EPLqv5(C4t1t{N z_FB7Vsi%+#>B~*A zE&T215;=|zQ~>}B|CP=c*ctJsv4ne$-+;6p`hOF@6o{fZ$fcH3JMuC=q|Oo5RvsT9 zgN~y@X2B!?cNNdG>?Q%U$qZap)73D!Ky`bqz@0T(yEj*r2Ursk07n?!r)G~Dg%s}L z9~%&WRw?=T3Qh=+a~ta?V}yvIAk*ObKHi!ob6R|F0xqo5?bMKgHE@+CKCnTwOXRre z9By8|5C?uZAF$yGeW?8Dqulcbs|#@!=sry@c7A`fL$KjuP3E=Fe)d3;FYeD)$SFs7TE$FR&6Sfu~nXN?q?lPs{3{_Ay?i^J^+$8 z89(DQLvN@|rJmaW>bIF5NK^42{+;cc4g(rdEz=)+Mr;8ACN}@MHYemn@yppI%mB^L zn+5Bt$f=e>U7>sSg!F!(792)&OK71GnqcsI1C2waVK*{^m;cumr+I}wd>D+Rht7tN zGAJeaH1B>Oj+;6#Y1LNeq8mK27J~pJ5YG43Wvn*f+xGdWNS!#Hd8^n4Dsvk7adtTK ztNU9LxWwJXa)ZudT(Ucx_5k<*js^YYS2VPp7Y2<@9Kn+R2VYh=H06D+fpL`nCmg{J z)AZlbD2kM4M>u(0Md-2JGMW6BOpHlFy9wO zG>>rqo0v<+@Iz{2GwPY0ZhQHfGdDB&#sA0GS3qU8bzQ?FkCcQ0A|+rVtsv4NfFreKC=Lh}sBYv>OXiskm-#uol9EHc zqP(81Uoy*)`K#mvdHhZBFsDFgtxWjPlFMfhLQvWrkYjxT#f3NzY-88-onqa z_f+#%4S}C!(a55^vofmNt6*~K=a&H}a6N^?;gXX2)*}NkdV<&gErZ8YrclP5LvBp}(3HwQy%)b0w#`luc#uXtdp<;$Ia#3w@i(*%18!F)M%?_a@@Kl|dCr6*w4cgtn_($Ke(8x9ud zEo3*cw!<_%MN`05LOyx^%L|f^v!b1kpiNord;~84`~y&<%0Xzt1a1d*s7$2bv*M;G z0ku@clye~A3xWE6^(4tiKl|T&V$gHpB|Y*W9Lk7$zZiichGy(TlZ*OW>@PdFw?>-v+xPw z`I8C~i0Sq+lucyD5}NHsr1mOn+9x2mJL?9{tIv-YdOzJY z9Qg!S-T;p1kLC>QH*dAV7Ip_%U6WMM*~%PLC33WIiwf?|oG*g^ZK8C&MWW%^m**!z zG8uxyU7`VZZelhm`ZUfrclD#gACu6l;o`mfivz<=H@pLyXP$&J1x;0h=m<1 z^G4waWY#i>!RQ6*V+Jr?UlE762R2bK_E|e&*75_36t#j3foOcgmpWzGv7J2Ak&nDI zl1ZY>if1nwcBKXhKkos9mTIoS+oUIUq3k;D=NAxeV*rZEHK!eOCRtG`;EA^5IXC`L zC-rPd(-^{L`G-|`eg`8_gKno7Ubm06JzpY>0AfpK%`GfMl&FQQRwb<)5^#PY-xF_GYowu~J;EfD<= z`+cslfklsk%Otc*sTx^4Ce3Wl>&-VTy6!7~1p_ppSseilZ|9Kcoh|S%@*EZx!R^t7 zt?-}qVnzO-SDA#5__7uo%0QJ?Opv+^xuiJE)6?9JUIlx!UF zV>5C}sHyqc{6G7{I4wcJ7`wJXuQ!tZ=v@8emYqNS2 z_`baDo#re+ZmnUD5KjFE!H4Nk8he;I6c8qD&Ly}UcDR<1n@MT) zyX{CsM2MzC3fYci6u&AV%&F7J5~y0-n<(75T52e-KJ+|x);;@so^k&)RL<|lV$qgf z9N#)c7|17LKBAMV*j`z7%H3A&@PjCbm!;4!Z4v3f6UvFml@{1kOwoMbXxiYb{MQd> zz``uE#}^6H{k421f`pMG0Q})A_2W%`Q15+aov`ql6QB^5QO3J?j^Iy=SNTDH9;Vcc<%engi@Byq_k!1eF7a`JR_5 zvPbZ*H*vTjIW3_UbOwhekm3UvTe^{ChHFP4pFZ zB*~DIRoS1q?Ii4skm^!imX#ZRSurMbC;ET3{oMNa*pm{!2b0N72ykxt+?xLv{t-S& z(&j}KO5%=Not)_Ovd}P{^6x8|@B|mOglUJD{(I|SWOmfqsUs^2FQe{f`T=(yBWq#| zq@msmT`h(R$TO`yXkJZ#qL*hZ0QRdlX0+O;5y)+e+7p%;_q%x9mS_u(9i9 z#IOu2F&Nr_OJt5UWXd=#%N7w5p5A?siDNPV+>vTP=v~~|U#Jk8iZ?faSePVW;rKIw zd@dRzi+|(kTrKC{AQ0?ij1SA=zpE45RKADAwr)RdVt?p@Yr=We%bh<9?{q;uaU@jP z-&)iit@7Gh$c(XaE(eT=+fwbmUng`(4-eop&byWEAKSx@AB;w%^nH#g5*a{*F%Sk|YSJ1(@4=}|p(rkH!4 zqYJdZIl^X5LNH3sQ;04)2a@#^lIRwo$>m6_kQUu zEY=c&gy__n(77gKFyj!m#8C=3G)f+ZQ;tx_05m>!_d)uENs05p>g_%lq&)_r@pVi~ z&=421^Bh?@Fe;QvLnHYziWWQfy-JeZj(PdY!=fG!5XB5vdhO;yYXinYvjo4(5 zV*%z+1R_!O(?c&j?8FK>HcW865rp-qeGST>_>!GrL47(kA zCtah&=KC7)nM5fH4xK;G0h0*&LI^GU)7Huwptzf;`CMoJU@G|FjvF}VlmarBZcc6U zw6$^HoSGH#IuY3}Sl(@SW1nKLzi@C?nep`dbAiyOa2ke^r#A)u{doWR9slDqWG6t> z9I4?iE8Cw%^Y0(vJ45RU;YoR3&XDnDel| z(7A>553$-tO%9a}ygokllej>>H@r;02J^uzYI^;joOah98W7F@x)ZW@aaW!x-sKb2 z%i+;{=>F*1&xPyu?o!3JmT5`mfPJvzz6Bp;DAn&`B7BwjuU-(CrYE;y_4vjLq#yhB zi(R}}u`PFTaVNBS%I*d3>#x7A{$4nAszr-s7SpOri;as0+NOW5xJLLNDQ`6J$XUI& zvIu|ub5J03s6n~okGLYnFJ9ul^jAl22#Il%`TGs;^@|GF3gX;~{y=|F0Q}0bduV|5 z*X8|r-2dalERLcKdHrv}!yk0}&kxB9Sg;CN@J}OU@c1O+n;z6aoso=0@1WU60bkLwKS*({KVTY^Z~aa zmPPmLyNk#kjJH-_`x&@Lge^-^%}J!TY7@DVbJG##U`F%(fDZy!2QUrTRIVF!-hnq{ zns7`2hyjR4B=`B#-v3%v|NK+91?Ru@3Opd4;wT0m;Uu&=`8OGHJ{~N+=39CLZBC~H z5kz1S3T(*A1R@@ujG`$@f7-Kj>J$PFRu3?{0n)P(KZoH1E!PhiBGBEYetJt@)7k%K zd}DAUpqmK*nWra6CMq^yD_qB7d60WYnt%0H*frgkb+kmp7a;IUehUPY@mW@9k$Egx zF0xQGa%FXnek{980!v)*6zOt%%hoS@nlHK@BYXpaKvJ)T4tFZJ4>^<1W2FGV&M9SB zStA+az=Q z<&qDLP3Wt2Kva!#w`nF{EW4b6LVJGUW-r{C3I9_&>N8e{_a2X-Fg2mwq}CzN9b_$C zxmg4fipS+YwsQJJ(pr|`MsnZo1;f^5MCvzzB+P`%gG4Hi8m1^p3==ZS$WcIum66MG>w~V&}*O`!Z6U9UnOULx6^-K zxqp73z(wM+EMPX@XIDpU>BkR^YIl6>CC}Qn_qIcZv^pP%z6-e=?GJwxmi5veuYHxH zM|l0!Q7S6qNq%4Lagoq77jC7@SS_t_JQW!LkCV72GO0Qdu8u$^0I~wXuFP+;x z$q}TBf#}t*00Sk&J*t~sB!*_3UE-a*_>|jm8aj6YpGBXHvf}jCcTcnBn z6wp#56~fvYlZ6Bp;^Dxs(Wp80qgMHlk>)E}p&{|xd^*lV)6zv6imcoPIPS*_kw{J^ z$;=etYKKr4_Ui>O8=Z}terf5i%}RU|)rn0h1TA@taNNcBq8jBpWB8X5WG}k;K7ekj z8G7Ien06LaC5aO9+|q9N=3+{Y#Rd&t!9`(`uL6n32`DeHdp*x7qGM2?e^J8A-HNBT zI->o3Ik!VP=Yb$`y3|PBkFzP)7LE+3kkDa4n3%j?jN;GBt(f9 zcm$KTG~etWTpf72Lg7WX2+#VY)1_jo*>~LsI_KVItv;>kdji9y7kqqS&;|Y&LOAxt zq6d`Eui9;VyWNODjG1Xj3>1lJMq|^HW2tZ_Ex;SlbHOl^nDC|}!D0#YC>^!BbhwIo zsIcMhL1Bdb(!Vjmf0bg{d@OBg0=;2(YAg3wgSb;^|LS(@gs%#mZN9NObUs>ZKh@p!4)WV>0D&!v* zZo{CD7=8uvmGEqjmAXYb(zcwOfx{(79A8B{lnc(#Tuv!vpxn$FKL|k!VPhe32 zG?bI)A-N=(<=j@kT2KO*G8^ivT5L@1y8a44-3Is(Mra!17;SZtUi8OID^%vqqz(t4 zNp~*c4&0?Cj@HfzicHg2Z01k-_~V2|=$JS8A`OqPzXh^m6HDYdj+@dOkChYQ_U4fP z7nN%^JVdhS$IPy#Wb)6`cwkaLRs48+}~h5Aob^8d#*uR z$8!@Fwf(zp!Z#w1M5UT(VB=6C!9@`n@Cv{SibQx~y?YhG%AWH)in|}DiWSGeX2@QD z7gU+)S(8xGwzUXF>sK5tvH>D`v4Ko#NOa?3;AmuWCM1s+>!$?Eq z(HzLSvceUJFFZW4OC#ubCWQFTcqEITOu>~~E(a#ju&G}9DQ<#Cc^W4jVpEwNovGGp z^ojM~dOdA>=Q%=2X~<3Dhm_FkfEnd;bZ~xSpMP$V2o9qlan&Q?G(lz`GE6#CCmu+% z^X=49nW@lw@S#q}+>defG9l|Wpk}_;3fnmCd*cBs>Eo(H2ntsk$GUW)OJrt}yrpKe z9cawvcB4k&thUtqOb7x7)6_AyHjuR&cp!OH#@#Z_OjTA?SZ|YQIqNt45@Ve3Cb6HK z^&{xIRjn!so33HQ*!?#_4b0=G)l76cZtmmUq$;?3%Ya5C^f-3qYt7hK+aSvLT57+J z<<42oWW^gu8-$-r&?VNT;sCQbq<>J7#i740n8|&FpI%86eOJiL;B@L~8cq~_j29KR zdCU(den;>)C+s~`p;)bPaP(y96cSt79=SI^-pilgo`J&Nno``rTl zm!1Ukl&=OvLJlPPWR6-c8$+&RSA~GaiZw&aju0WpdH04%26y1pm ztv801fiKPgd#tt40d!6#CtdMYpkfiA?bNr|dvq8Y(D;wyIAHQrdKb5?JbnY9Xt{DH( z8~k~1W7bZ=PT&oxq7`_gF;bnNph&yF0S*aO24LP4PEm*QczS5pv&7teIkWyeq9sIX zp$-^vPiZP2iJ2yild&mS*p?0vHzU2LmM?wa(!u@xT z6zL}XL(=s9-b%0*ebIxv1Ki1{K-IJrsn+1N+gARxq3M=!_@S6jF~B`S^z6yhA1-NxPlkETro^FWtage z=|}d2$Y5heL?&4TW~v?_Vguz@i)S{$aEv(U;XlCwKX85 zeE+s{sRr0ju7wssjBPCNgMCqb$2)w9?_j-`>5dUOX1ufv>=|KtalckGmC-FS1Cbr< z0JphZS|qO7kuX9_;#1qKY-Y0hM$WOyYkSwNx$%%l;1p^XmDTI)i(n z&p}(P;+T1a_g=d3Rf8B>^Ue{#b3-nQOr!6o24p~qCFA7h4}i3L&yfFBjJrvs*_7>y zZ=9jGQyccK#f*L@!LS%e)*6+sHicj{yac4G+x6b$#Q&U^wsnh@bmXfqE`Yp);Zmt_{t3bV`8nt{My|z(mD>P;_CVp&ZpFy{)@PYj zFF-!}z|v$X%WC^+M?xbxRY#K1xc5Nk$85DAtEwc_nlJ;bImoKiqfA z*(dkbpYJ2K@&;%w3<9)l`gHF;so*g)Cr(Dx6h9S|yO_=v^AGUodf>+`FbWB8AMsY)U zH2832q6MSG%nTd`0OF(Q{f29#rduyZx5BM*xc=mO=THlbCL!c@_d=y-Z8d1|We||x zU-JX8VMXP|KLu{&61Tr0M7hn~SL{I)wdei$ALhBzkN(>W21(U^}&_L5`tybF_3`Wukt_Uhr& zry7M$7E@2%Mol8>ev5_s`M>5Xe>R#wALv*xan1mPp-bLGKQTK{l%xH&6qM;o46&ES z-LJV(lmUU>a{M#otD`W9B+s7(q=+K1fvjMWm|(d9D-rAZ5G^kCTxsH3PNU9xJm=Ru zQ)dvGl!hQ3iit{GAJg8*;r=ptV4m$W{<%sLxM2+aFh@i^$)+`MU9_>N(Rn%?TCd%hNNa-qK@JuE-T25Z&N-8DF?II{%^2hUT>fZQ|J&L+BKN``!! zV$-7{6WSCdI_ev=iRaxKko()vJ8#1A@AXU30;RG7OlvNgtO%1asKc&73I;Nf6@K0Y z^L2@O&?r$+c(S(im*Vo zHu*{Q!6qW(GS$zoIAE$Zx<>VR^OEtH&%dz&ZVZvS6w|Lt8M0wa#aJtP! z7G1kFVS4_AyXn$RZfp_qTyiMG2l;BW`$pfCX<5ex%e6?zp$ zR^lj7R)lo>(PMQz&wOB((q2KkD=K`Po{Y0vTmG7#=&*?F@h4XejHjvxfjH?_JXeeM zOq}H0TCeoAS7Jp@))*CR4)Uc^`(KBe$lk>f_Ba=GT1o|rU8Vq|?Cctm+K!d97cnGlk}PV0dK|;tD$wTw?bZ*izwvEZ~G}{F0X? z1$fFOFrmDq{pMBT3H-Xqi$Ubjjy0qS^-=y0l<0PiP8KE&h9JwH2p&0LE^ep8=&c2oeNSEP=5~iW- z#>nx6h2lVhU$72G1T22UZ_eGi@{^{=(P&Qob8{}<{EG{R4I)1!>x#tfzZerTG1uj| zm*=#?CoM_?^~yhW|N4?X5MHtIIUjaTw&V=-PcO^-Erkjc{EG>P-|PV%!j?XxH2Fn& z3V%i9WRFWSZZg7qLEo$TOOYgu21OfN0gdd>Yb~N7XnOT~paBV1o?+$jWZd`l-gd(7-jK!3v(RZ=yg1a%)#z91-yhSW~zQSFI{I&v*19eYmG z0TrOk%=%ylIyPQSVofRJ?4-@T_Yn3ff)gH>Ra(GwHq{z!HvfWa^A{(7GIRl%t`j+K6C}Jr8YuWLq&RlI2U9pOzb8gfNC#yIf~Ev=lL9X9wQbL z^>4GG`Z!coR0|#FKY8fX3$hU`_jdN5$C|iz!ht@jEu?l{v9;U@_h9RkU*zeI_`TTv zfRf)I8lG4DredD0WtL{cms=zt0+RftJN_*jEHlumPY z!#P9xc_D~CDS|xr%aF^_Gym=`&@D`Yv?BeHAWO}UAIiA5~@_a8B1Sf)jjr#TGT=l~ws4)qOy48Zh`=&&sM zDMCXPq{jXry&y+-XA~t@qO-jSBcTW&L@f+Nn~qyjKr5}bcZf|g+o`S zZN-LU_*cKYJEn-&L_l%v%lmCm*gXpg&_9ASPi*~)cPJ`FC6gNI;t z(MKF5AvLi@5#R)+EDD@M1}Us}VoyZJOA_m5fU~%^tAkn&pCwmzg4$5I?8lbcw%2F( z^F|RFarC;m!}pq3mgv9;~HDyU5&s2Rph$ttb8 zHt}^y0~0@bbDXR9$Y7=b=&|8U@gYQw)x*^slCU)+h@4ULs%1Jp-{|aRtw52WPVw?` z^<})y1z^~g$`~8(V7}Smh!egeD(X=(5vCtSVTkDUOgKqDOijthEP%Q9t5G=WnuCoZ z=YPb^j zj(&)#fFoyQEI|k8b)!W0nMkI6@|ft`l-KT5j37<>oR%5$j;-Nz5ezY#fft*&Q8a>- zyD^ke!xlGWy1PGf7QtkowQD~Roy4=dVp-pMF@D|X4p*}qDQnVun{cEN^pFZ4zZ1rJ zR}7P>nTO04=^gN~yyqEIMl%-+m`X13$43rl>;@qAE%Jla{*4U!`GWz~(Zx|+w69Z} zSU;Ja1RkUJd|+{UOwyFKJce9r1Dx~o2s9lT=^aMT1;}(KzU&Wz%R8s?I8|Xxj1a4{ zJK}~sfSDj`Tde-Q)=%yl4MPp&D@GX(M!w$qipV))+8j-1E?WxXM2_E}(8I&yRrj?EP{(aq~OUZWmU zdP}XJnq=B@cX=yz`YPU*35+>SpYx!HNDEiBJoon~_awzCBjU#jn2ebZFS@j^8s-~j z(4$L8W8^TTF{zrdH(>l|Lwf%22c z5jtpUGAl({Nl5R0UCH&NhbhI(LTIJpm`0YwkDk5q!%emv*bAu8vw$GRQPfNEWGl4I zcN0berWh2sqNYY0t}{mwvDz#R@VpzGEcsN0$5bd5O*37vZ!5$ctIjFY1&sQr4Pul& zeDh^{Dm7_=SIa88ofMr37WFB?xQ3DMfY&vYVp|eU6q^IU@KyUU=F2u9>$5t|?qtFf zOlH36)_}Co2M{%}lv|u3NbaCKA}=IlL8i;fIDd6EVc~1-%j^qeg-XFwpe*X|9bLE` za|x?{CG|!Boy-3$(7#LdOR_fdszZEd-J1;DB&>Vy;2VSN)xwQdh_)3@b2}ZjeOq+w zO7A+1unV=tGb%#LPS663dGjiP9`^>gU2Ith-X_xzIrRZYH{c{i87!$P-H^K`CUT9y zKk37Gcq&2>v05@kT5p1lT@QUMXf>hfJbJxK7dW9C4TtS*{&Q(WWiz4XC1l!)+LD+| z=%zI?v?r&3Py-Bzmb^FDtC@^uxCr8swG-T5-WaqB;4@V^oQ7}?i{q5uvSO-;U{|Z< z(ynkY%J;4=(5F@rWEFBN7x*uHpeB=4_+}n>sL^K!T8L}NluV9}MKe4n3iYncfV+(| z0Rll(yB?=XejWPkczbJ>M}b{YH7F_4;^qE;>F3SvCHt;v&ch~g#w2ybdesa^p=n%- zx`w^UV+VYg`lZ;V5a+3h;_~A7D6=EKx`>!jaw@71D=@FN?&6Fe1JyIR5PBY`GaiFN z^SZs-Eb0HLKXk6bk5CP{vZ`=e2?+D3aqC^4{*CGO&6SVIYGLWRI6_}cK z05eK6M5ZbT5&{qH5F3W-8X_@nh#jc^)FAooK!Y)S{)&3%XBk^}3Y&w-@evg|UdnvA2uKA9m?&tOC@Ufp@+O-vIj?LvFe)O%Y^WV^O~KrzYq zFoQj<*Kb&?mAg>R{Wdvtjxqw9f&Cx- z6!B1-=Y%WLG?zGk-1@zY|FN{6Y_0-0DBuZ_yO6!L%goU1Mi!;tgk8aTLuE(eD4is*u#zZErQR?wHgS}O%s?(U z2xa52En2+G6O6~BT=pn1CH@1Q7b>8*_9ausj3yqhCbZ z=+f{UO;DL=sBuOsegD8y5r~`A>PKW{*usI17++tSrKB8^Q}sq>7-3$Z49P9Y{ca9v zp+&p7W0lppPZU0b{-?kBeS%|xE?Up}s7sunCZ#DmzO|%$N{n1%VrxEhhbhISENbY- zfJX83-7xdfN2=m6!B|u22`z2p|AJ>QvG7i|v71v~k`)s@ar%Li-aYv&iRl%$G$+hV z_>P?O77b?<Xb;-y#_+f)@0_OA}BfFG|cxL%xpN1~|X?swhq{W_>FBVrW8A9_f{(D{!EDun_cBP$Q*&V*(#GVFL zFw;VA0*a9NAWp5Wqn1v7iAL6vbbOO5St+|E030B`-o+)ZC}*q&+Xy<6`5o4Rr2;0| ziJZz@(e>cZ;2T&vgYFF+ozy~<-~AaesYJoW1UWHLM0T@#2!b>d|GF$#Eg$w~G{zu< zgo(kfn&3t#%rc8bNl!4xdvdS@yxtZyoDCTdR;W~U(KmID<6hu1WExt?^NCb(qefrnBHH%gYE7xJop2DG^hU10Vv(L5 z8Avd$HEE<(&8M@iPZ()kFC#MtJ$N?jfI_efuCll z$2xwz&~HzPGSlbrc*dLAVhhOB@Ea$WOeEj)BRbhfNXM|?-A?l_Z)txGfgO3E~ik_?Oqhooc;>SS^HcnFO;E*uIl3$j98O!LGW;ZLONhsCKu;bY_GjQSTf?@?LX{8#beNz<;*J;DIT!8&_;Amvd@pIfh zLYiqUzfzefejXJqa6EbFmXAzhceEwy=r1bUghsP{L_7}nREMtn#YIt|*MD#*29sEx z!b-j6>Z%+xz!3i8|H5}VSxq-MPpf2fgmk7C-VxB6IFYCeYvpReekv~V*1Qjf=L7tb z-)+8}ynPF$8_srny8Vq8ax8e^gRhOFvG~|0&Wx0G5)xUftfBAmSAzCvxg}<#8_u?* zjg+Fe2bw&UtSGZsfP|(6s26Zlql{7vow_Yx37bmXXGO#1E-vHKO6zq^p;xO7oa2$$ z4YSl_TY^{iV9};%woKG+1!iF$MPK&TIHM>Lzt&JggfZGaK4uP)vQ0TCa0O3&ToY?u zR*(^xRDxgMvYcnFtmd>@p>=b_<`)u~!pW(OX)5Gk#u72K|Nh48Bdb=Yw=cGg>+96T zL{*GbWYB74uJU@Zt&s|Jh5$}XzV9qg{fT3Ax z5V4o^fi5+Q1@_-d6$)BrwvmVD{;*1}Fljp*NHseN*wQ`T2P}YYbrRj{IZt9G>Aa`OqQ?#3+v{7_(0-b*YJU^wY zT1HGO=ct~2%ES;}9{N03`UsS=R+W@OftrFu=tp2-Uq%V>EtlqelYQDy+HMRe>Gy$?}m{(N4T`|x(#MQ21vNq=?4tGlgG3xy|1zE*z9oKurWe2B6cux zVObkQz^NbQw01WKhOJa+l4nzrcAA+xJV7kyUC<{t(B;<*4-OtQOuQgRTO-2HxY7N< zyg1|6l-C=6_7ASH3t_;3(QC4(Y(Lwe8k=`^ys~^4c_`q_svCPHG~t8()yR~@{g2v# zTTE|~n{MdprOXUz!Jcl~cT|U6MatbkW*$_Youargkj7~%VEZlXNKW?lwq&b+)OB5~ zGHBy7%ICHr_lHc7e>Er0Va^g1woQ;Owdn1$@oXcAtX@ek5@iTO-=zK0IbOzVa;|qU zO=wSSYzCrJM?zmZUt(ZSO8;0i72e1Y*MVLBWA#vDdmrSI)|H&-5zgG$JszDZEZgT( zCmcb5uR+}0v%2i%G&R;5AjkyH(jvYZB$dbc^zQoVYheN0COsEST?=cKadM`MD&=2% z2x-FS+4Fxrz_xhQfuNE6P&gk?t3>mlJg+BG6=hv9%8&4b8OPo{*iPR(Bzu~cL)b;$ z2h;J_``Mp?g=I}UKOfU}{MpCE(O!g{Dl=COmg7&mU}dMlY*h9EM%_IGGMD}1cVC60 zZffn!4;iZ5Aka_pi@cfL5dMKh>ZvqZjkLZ));c^yLRAVch9MwB>7~2Y}^xcx1{Rv6d`cyYVG83LhC$8}7pV7%sbYsb_3+MDdy?1H zU{~9~8|0nRAyG|fl+3St(~v=Ga6X5O}XwbR)!eLBM^!2_C_GH=M z#mF+M3HKROT3CW$_^J=J#hqrX5=OPpYidccLP1YcT{+-wJq>Sbt8H}IzrC%Ufv!{d z?dHZc!L{C2Oj}OW!&WFtd71SfIpP(R7h&d%|1^!;1G0LmmG;LGjQ8Fq{NlRU)3A1) zai&eAtrzpNb=eOaTt@(^up#Q;kg1tXv!q+V__NFEzN*-g4^j4NaKdW+WDc3A%D`RvsnM* zcII=n=GebDAlZv3uY(`&Zj5xnao0WfUlB-%{KS9SO1LZg21;L*r>*TZQ`gJB8U6f5 z?Z`I@*DAI7=-BOWG`1aJ{TF0Pc6PgY==_yxWe%hT3Nw%cvT59Q?B~~{BEM4CP_b&e zc~ppMRg2h3t9{NSol~8chPrQ{lmYTb4Jd>w;!GDa6* zC1X?Ifa}ZDnGo|LqRwds8hI9_wmOcmg_dx5c=+`m^$z6ZsmdiJo@@iU4i$eklcLGs zDX`RYFn!FGcOsvMs_2e^nR#j@`Q^U=N7xO&qLB7k6_OpAPf0Mpb*gCkh$Ovzlb8hM zEhhl{;UjW`IH;%ojo&RSNt)eT=-s7-C)I_0TFExm^PXK=ILvK`$60BBFg~8%qN1n zR3rdtAwzy#he4zzB;6HBW@=x?hkNl3?!`Os+x^SEkZnd$3YtNFfob72g3n1sah&D; z*>|QQIrh53y!$#1L?C4}gsLS2@VTl6axyr8-A4m9&9gaeH}S<@e`9y2T#~GgzQUvn znAf^`*RYJ1pd-;3nEtA_yb0+H_;-60*tKEjy8&yctVHk8G;{4mrwp(%75hc3a1mf9 z>EJi2K4vy1z)f1eOtz_($dsPtJ_{q&c6zj#!ll@vCi++s+_LBv)d7=&~V8CWWG)C4W*Ec z9Ukw%%=&fMlGkBfMu4=aw+WV%9yCd|tp=@80@YHD54t(xb~Bv5<&8ztmuE%|AFcYk zguuEWZ-dJgt}Zq4~fsxVrksRy&O5>?a>eG zrx|Whq_s3!i>vQ3Nlz;@H{}zqNDfsqBvqX6k;}Td&8tqTqPW7S$!QxXq}1v)Yn?RNsYtB8b7N+y!qVZQ4@`fDd!w{s>bhVCJLmimRVHf6nvQ0=8kwhoCm1*cj8XUtoSr0vn z6NwVRSbnYEdu_jQGo!+9>IZT{XcMI78JmOHWugsR!P`4BoD0#w zhI~zkUC-}*=&OAMir-O`CZ6Awh}&VjWd=L^x<)B8h2}^suX|Gx7*)ezXBl2pI_~d| zzZ$z*W!=G?_5@EmnPFWVlPU;Y`c<2C%a&A+j_!_+$;1jrvF&{rCOVof2%6V$tAk{x z?+5U9O~FEC$8+8N5PR8&h;~VdY~e~>mc!_N$~x0Xg6kx5%qE-K%0A4SX0PE_yK$pG zBN->@mVskM@Jogvl-`VnSf6=XdiG2FEDfkcQ&36$5#eWxQ@cIC3p5I6E^9nLf&=Fjj@y)K|R8%kW^%htB zMr<>x{c7qlFGQfPYqGwQ7$V`Sl-9LErqAzpq2lp+()vSI*3EZ>DRPcnklGlVuN zQVxVaEbnJ#pZ76k*9kb>wFpDmCow{tC9{<#EA#!pk zGd&|>VZ{#+S1FZ)JSGhpfqwD_$LK=1)3F^+mb`U%OzCeno4n_Db#;m6#9rW)g3tun5KZH*og(uST*#^(C~+T0l-ju{ zV;s6SniYv7Q_Nc-qP0;x(`g|3<=K{FD^O#l;tyyxue{i!f60)qRpN;d+m?%-#bL`s zY?P*Jxz^6U-!ReFa(wg38lhtcp92q=J740vu|;t5wlE8%j8^*ugCE%CZJ{WKcNw#9-l4T=c#c>W;7GN@XcjC@_p=@U@h z&#lz!D*Un4xbo#kjp;^@0&T!OFzs7tvVD{|;N^0CSJ1jTo@eT#Wo5l!?sm5%Cm$qB z&4@O?uH+flquN2CK`Op^xCc- zk%x&G-Gz!_wu5J<9jM)F9_;d*v0}&u8 z@shl`FYdz$f4g14EPe!nUv7ltnjU+QQTC&OeV+fjo(H*Ve_kwU$QKf>Rq^Jho2@Q~ z9lgS@k-$ogZa?oA9k#){Wj!WidXoar><(C zWlme3eJzIJmMlEv8ghxD(-$zwFASpSi{VnP*!cd#``?(PBAv_*JP+!}NYjhQUNU7< z#?cerKOq0UdWc9~`{^`64CMpiI z2^!Y#`83Nwd=ZJ6DX%fs!)P-|RIV~JAnv%B7s3MvRQWX*bHv^Dw-KeD3#W{dS(omz z{6_qK_1tB$=8NP+cDs)tom+Q9{+sM7lAUD_-0-06bCAkCB_`df+uw$MpuP75N4XIw zU&7ZZD@Dl4Mvm2i$5!H@#E-3q(RuKe+~C&gW$vv

X0)?KrxQD&9}|$$$JewE6MfK}wJ>*a$ahXpuAAG|>jr5HMNoy`S zG!mn@D|P?ixiSF(ft2-&>`3Ll2dhX%e}N%1CBDn|)O#!)=Tef2pK<6lvBNC-9=|jd z*@+xc{NtJTw!ikhZ6o9hj8)r*r6?s&Yy%l0rsYDcir1INU>HWSu)Xexrc!~usuN}i z@*Q%p?f;*REam}vfm$wO7S9wh@r&Hp@H#~1_vzCQaELULbv8MjB5CK?Q(nKnQE$M& zN1&0saGqAy5w`j!5aR?XCd5C1mFxeF-FWYvCWUMn;wUnm5dt{J)^*Xj>|xJhqC#iAm{$G{3!4J5(G5CLJptel+vtgxrmT{Uq=un zo6oA-X};S!S%YjdJoaD|JYQ{WRF9ywW*Q_fr6X1p+hvE@vA{BhLifttqz_KlbgNlM z$@lkZ$ql=s?;#(gGyd%CD0Y0KQLKXY_+Lu?x&8TGo=Ev3eNq=za#91Cpw|L`rEP zSKT-(u^XiH;ELIl;uTgr41*<(BL}c*4sMbefRYEvE1q5m|Gt=$V-@K=q!1&*A<{X9#wL9(sYvcmCLyd{3e}4#A06_09vHLaK<{7T zIs!oD^y3;Yst)4`+SKdq%%xZ{U zonmx2TU}Qhk~d5)pp5wS)$U_F9rYr&iB{FsHC*B*L)VagX}NDj_^dXFq8IX3$z_j# zdQhj6vP{^;Fw598xsBmgg~qWXGp%|1yUzTsKnU%0{ag&jo3e`Tkm&|I#N4RP-E3jf>M+TwIAOZa9u*X%ag5%bNL@TP6s0r`4ju6YKK3Vi8H!-W` zjJk3@mZ44*{f4fwp=u?e@;Symmx$pvlff3pns;7eSRr5zyqok?(`;rII`fa2QY)Re zOZ#Wi+-;^zdI#!fe`2+s|BAkoY zY7Ds(u@+%@UZnCx^7Ni!)Qh&MQ6wt@u6-3Z&>wgRY0Ez-K&`hezRcR5PpBlZ7>?f~ zl9w^|rA&%r^*K(exk7sHV|;F@c6`f{@G**UfsWdU8GrS}+!*8bgQqozR4`1LXfA!Eq+Ui-&=@RJ|X%8VrX-^*odreX_{D?>|9+i zeUskPE2MWeXS!+y>?Olk>-uN|l3Vm?w9C6Z_?>SGAvZqjmAeP+7VOS6Aq4V370bPq zwOQu$F_iU{lU!ohb8mhmRC}N(gv48xjdeS>NL3o@aY*oolBwsP)1jv2CCgsFxVQG2 zHS_L{le!7bN{oerxAX%dUx&*p^zpRV;Quz3Dp7yx>9`i&n0B_&AvZ1y`|fkF2zN?O z=O6ifW`$xCu@G8~cbjg3!o?8uMnFg7CxpWUYV6jkmRQpg)90(9*{^V80T#10B|MeK^%9*hN9&rF(;C}=0Hl{i6&Q~4$845YrBD8*7c2u z8S(y+8nrtL*GZqYL{ox$>JYBxvX+slC%SnvYL#t=*w zIJ-_${FaJ1L&pMqM@k~-kxtE&)xCS)`3E4NQhhj?5TecsJYif&VkbgNAJR4)z$`2k zKMhoxwrDI-&fHVja8>|?)QNwx1pN94XpO2taS@#(e2`2cgT*)Tw!PO$?b-!Jh~V9x z6Vr|_l3=J4IpgV{`)v^HMemUmG7Vs3!nfN|78NRs8hKQS<>CW{#zSV_WbFa+;qQTR z*Z}w?zqd=vdlV2Y)g&A>y{ZPNVg)1#{q>+beo62!zJKD|(VL9H@PUlOY+HXkB~*b2 zsa?FoSq`puz6t&|nwywBwhMFJKxTESPo06GV7N-9L}umZM%cT8WW6!V_onJj{r0DS zq|^U!HH3(8S$vd;hS|dpN~k>%qQNbPy)y~%5+?%h{;bU^wtvADnP)F;Nte|wrAbh# zVKXgX_MV0fJ`Eh1Am<}8Y!Yn2AGHA`p=)11y|gt}1GkI3LVfrndoG*Q{+A@Di6EsW zNGcCa;&TnQK5ovm2+ohzpAG6?(_9=uBm;mcNM?wjG=lpBBUd|y z*Yj5`IOAVY=x2o#s$WEXF5#VDZSQXE3lOu3uTbxI@MlkQu^G_dJM+79amXzU1CZv8 zy{6u*M~)Gs-+lvA9v*>E;p!l&4NdwdH>I z)(dXdV-!}hACF$Kh~9*32`c|_+XZ==z6~dS+})_jN_9DZ3Hv2TSLu5bP#7R#-g+NG zC*#kWh!l9=rIYE%Im30Y3mM6V(i+INp^zq8m;GJakduYT1@5>Ep?d^0CiF)s^hu7% zH>@J6HwYaq8o`XgqF%c`&oi5@1-DM#!wOYA{jCtIdFW?E0pTKDuQj+*; zvtfnmbjDJa`+GZRnY_=JBSLXbUk*izlD&u0UrhmPqm~g>6Ca>LCgJ;7owru#67*k{ z>MuI>JG}BG$$7ozpg&90mR}$DV~dP`5l#QGF+|xjW==4ETMTLN?`dQGeRsreV&QI* zd_aw)epk!s-VPC)2~?0aSEl6bef9S}0g!RtJn#>75m=SDG5L5y@2GjeJv0EuZ>||4 zbiNkbP#Nn(svTd~7zT6^h|K^AWZ(SoB}Hd~EGlS%@7F}x#(5E2C_5t%#aXGKv7kiP zSIzxF4@^qx3e4L?Nvs#wB4#DI*~;4;5;I5^HLlI-FN`dW)gCDVCEhC*C5TCzkAO;U zUUXi)n`mZy#Ui|PU-{@#eviZh=3=u~ct76$6$+0)#<3!Q)g7;=`UX@Y;kb8w;+X!s zlk?|Q)|!tI{NMCGsW1{MdJAQfg{lwG5gtvS8(P)Pj(0=}fVe^1EOc^<3?MoWibU=lZ3)b+ z*l|W02mJ8UZ{{wxtILoWj8s-|0}#TWHQvf}p72d#TwHZQ+~EH0fhsdzg!TwwJuNYc zM_3?w-)q|0DhGGK8Yta)*D)xFVn}++JrO8%mI*pn*pLkMCgd~y@x3Qj7$+?Q*D8}T zNET!Y@)eA`RSl3nL1o=AoxvUFF&EE}j?(xHl1dv6d+!0YqDE`Njd#AkI}t+F;9uZ< zahFkaJ4QTE1!dREtfm3Gr%*Z~;2u$H`5bPXiOaRJYJ$9bXn_p5kB#WlQ63M87;qh`ZRSS+8}07w&p2MDF0V$8 zY2IV-{CJkdN)}M1WIF3bnu{SgbR5-&u(6==FGWPN$_D%zRjunc7Df<8?Ll@Wjq+Y9 zC$T;khh}J2d9tzaup+cYu75kaP`y>WOqOx}Zp3b0!U*~19hH#$)f(fJ%PnSQhUO&! zRT>T3GTd`ASbx@Z|1oPq#@Gn6=_D|iT1LsOb%HV1gUCcl%jcM7A6NLI7EYYhpi z$_jGqVTi73BoK$WST1GG)ANkTOE3}{_1p-b^^(bk9abjk22@iY5GW<2(c5*L=y=79 zPv~*zXJ#U`x_hqRvisp*3I&?1aDb=sIyIzoirEn7fVt}wKwV3GYd^u>(+PG#4^j7; zGV*W{&5z$aaj!6n*}$}H65Mc)8FCzCj)CjVOhs;eLO}e(LGdmGPSnC4Pj`H}M z>$**6wWcO4L=G8<=fTawdtG&YrEX8cj$}y%inRWyx62WYTk3Ex@GkQ85M$v`LNZ6Z z3^+qI6Q!fb`V?<7*hn*>vs`x@Id0cJ+=>l0eMhOt%9lTZL>L%pxS@y|uGEfuZ_mpr zE}M#tWvL4FJNswq`HQDi zxRKntHYBT=F3v-|SYP^L|E-dxa|*4aEU7W{oab^wy53`3=`q#YoLL=?*!TDiMt9#A z282wepc4Z3`W{iB0_hZ*pcB)0PS$BJ(0-(&kgezTkKL3;kzlgel@`M5g`vJ2n=@&K zrWvtRY?`8-cb0k3QlzmMG$_sAQhIDJ!S!t1+m@h_$&uRi3mFpq4w-Un`wE|<$82(U zil8%q59c2T^UrOki~)Hfi-DB{EhtD1<+8@zUD<>Baq)z=_ff4Wbyl)m6p0R-{Vs5~Bp~ZhkVJn7nlUfL$}9medA1_F3_(Uu!uwl?R6y=y& z2;-O_CSPl+&BD19_|=980}*so+Ntv|SX~IJ)azGwa_Y(%p2m+6;W64&a=cqhi=Q8X zYv@s6MG{p~ZW%6JLO$E{jy``!w}gR6N#V3!J9xyi-iPmHJA;nbf<2NeMj^0g)ypH! zOh4}ktM^QIEqH@S(?=D{X=yWEubyNo)q{eF??7NTjd6t;FMS`|pmgv);AraJ{BKqe znb`oESo=VyP4KODG{@EYMCk=KCTte~pn<%$xmz~ezNGj%Nk-SOjPKUeZg27=sJ6st zfn%}iTU~Y{aO3^$?03(JZ3fM6g^d01&=8W0iq0L7R|*3^un)yMxvfk$R0`3bns;!& zC7>U}AWXl)DRuZZ5!R*ECWNg}%rY|r0LUg~-X9f~Ka~V|?w@g}@Jy8|{ajQQx94q>kR>hsmG-TR|-G;%b1CmJhL-!B26#LxW`^k*d^ftiOPuz{nHwz)|VN@PWD z^6ZL0FZOjc#GNsI(4o$3%&&7CQ9l#5blslk3Rz9VUyd1pR&Ih95`Q>2BqaeqT))2Z z((92=l+*@wc@8+jXReHTC_|cy1G0Iy^l%A0;!P{L9D0Hz}w|T(U=t8!85)>lRLNik{ zz~zf+#OyqG7aUZF=E6;82ae7#bs!u*KKVj{2J8LZFP5$){&0GlR%=#q)f9v8ZAff= zoY_o2MG=zd+R)ivE>BPPMb3>(!vz?Ks~*F;^1f^FcjwwF4{C27jklrB(Cc>?Gvu21 z4via+*+WLaC0A>eaC0cpH{lahQYks2UMJ9_9~~m!1ZLWpAN84p34y4PV<-s^L2gR0 zoFVtel_AQc{^3slY(j?8&5oH#Sqyb+%d~0Q-(yDE;wRoA?Su{xZ8Mk4e-qa+PuYV; z9%J^%D9UCtXa>A>m&v7?4rcvxV z6tn!BJ}XLRBcK;r@zl9XI$Lyxmo=0PQW7qd!6*&5Zbgo40E=l^-G;e~YfcY(^||x< z;XcjTx&bw#{V#{zmsWQxAydQI8(A2&hlw0MbdPbSEfy9LZD%71e$G^$3@?PoTzVBG zXzo8uK3AbiKdFhOR-?XJdpBND5;1@6Zd$=a)iK%30`;~7`(IyUBDJ9zhS2~+@meR6 zB{xQA<>ZGw2xw&Nb}gyFt(2a|p5Tvbn{j#Ul**QY7OFDpNzo3ehMAMfXv-c-R=e;G3U?g~-kb^!gDuw|84~S3t-6;Ep+P z#hTT6O!C-Xi+P6}9;pY+gOdjOdc`Pz#w6?8@ULi!ntbNP9xm@oy zg*Fy9z?Z*l3$Tz$gioD3B_>+iROi8?pOu9E zENyA984_(yq}E{yLFnROq5S@vLRVaAo5f-NS)yhkdByk_VPInJ7$@HT5mw?$JMN#i z8$FE3iyjT>-jVB3we;skimHXMoB0=8!-VTFh})4Q1MXw_FVsI`D9wwwy`euWOKbcG z5#&5od=Ts07URXafF!Q|j#WVZ`7rT4@Nl!PZNF^-?6F~ceCQrOlx|3QZ-%2=w;=|8 zle4cWpml_dlvn1}fXP%q!?e|i^LmNacuEg2<_4kf&x>2~^D^Hz!;d%_XzfByXtq7i zcT0JD=O}N8Od^=BYI~zYf8-@{!#MY{b1O(3`FSzp3?D!$ip{1u&M)c+qgE9aem^h7 z4CThWz+~XAWlcG^wwZ){YWDO(LIz!UBu$S>Pj zaFX>t>ayx|(M>pj0D|f}iJz> zkZ_G00(i%qz<`Zi?RyJI^)yRH@o-oL&DCiN-umILR@Qa3OG8~lBs8b*Jt;3=nyE|k zdOnl>?56kKH%X?J8I>fE`XW_dD`Glmf~g(|i8Mi@tD!EWBy4Lt`3`H`wILUH`=OH*J*mpyj@(wXW`fxRXg3iT|2dDyf>u5up!BMj8}A90R|Gh|v1$THo+)AEm<41g0bWc}Vt{O+nSIFC) zAC4^kVd*!;KJ0#(-MhXYjcn!3+3$Nr?~R_Xxw-uwA5bk7y@y=G5TOfaZDVg!Sh&{h z0YNM`B&J!{$#G`}HV;;^!t9hgmKs?pTXLD`83sO7f^MFypSNbmQUIAnE?kror8h+* zZ=rqZVpExGx~f!=9Oa!V4S~>%*bwgq)kj`Lr##+HEZq7L0QRpXA-TJJHs#n7{x=Wg z>>uE5ZtFBR7;-ZHFDC;6>r7@pjHL)T_X1r*MHoZQ3;pOTI%Px;=FB{qPN(%cEPoBk z$^DTschX?YV5pT2^C(a;sBjH(8CkJR0FEova8v#|zi~xAg(TuTJl4waS z)KY5mwpiLhh^}zChv0(iqBwf4Zax2zG=ClNvtBL50C{nJXKvo#ug)hz_}ECEu#3yD(GF&45soFn7A66=e)T{ZLT4ZuI}EK z$A4NKT3-ZQ-lQJPV1DX%Lli5931Z-54sUONHOzthpTTl;+m$~an^B8}uo~O0Yg^&Q z{%`L}b)gj;?V{mA0hLpzx6*D-hcdaMim;Wq680F zQ66A~|9Udn2FeOuF30qL&!PYIeNbGTf|B&i&f(bY|6Ps0J@iDV01t-0#w))4+m!wF zeNac>Ap!LJ)QLkw|2h0rH5h)uh;aYQ0}kL{E`MhU)OdJY4}BB(ZJ+#YWU})x{M227 zc9-9G!LO0UHbbSeYx9WE`hO1J02%(gZNS=pu8}peM$MFmO8;~CcgtY-22~#U|Gh?* zutwHGdVr< zr<39q@W&a%b;~&*FN8GzQ3EKx^O6|+*=19*s6UJ^Y#iLpZ7<}sT{_^jy~UXCFR&yB zaR>4txr>HiuET;g!!KD3|z5X02&f*mO zfFpRqpAlW&xrTndxX8v@K9}V2^E&?9cXav+B}qp$0iOAFUK!v|IU!}xTq4**FhkQg z`eS&C(f@+evtc-#70EvLzYacFR0ElZz^lRLAoueEbW;IvZZMCC#?N0IyqGgGVDxu~ zEFBIkxBk2cIs?IKhL=WJ9XY~f7;eP zOt>0G_>(^UZRY-V!Teb*mO`35QU-48{nuN%Z9z-XJ=gWf^Z$8){5z7sO&$wmrRSut z>9@zr%ZUEkfo?#dKH4SOs`~r9q>4O*f>LH2AlJIu9;*Dx`#i+9TPFPNO%7pDy8$~| zNddSx-i5ipY@zIDm@fLwg;vhrrt7hd8?d~W8QK7)O(?dvAK>+03+RS>5~hWk z%U$Hp&l>(&UF&-|1g9xa84e{<%*u1zUC)s~q00)0JRTMmCWzu<&d;e>Q$%L@S zi7YNYKKfsK{D}|`JT^U;?Dg{m{Tj~Sev+j|)^=R1_4sdV`|BIQf3t$e+Dqi{ey!aB zKgHgF6LYWLKbG{r7itFP|61?PXxL5tWl1Z)H%HtdMAQS)hFV)?Yn1=iL)RJ!%N(u{Vffp27OKO`2U?wi z9Kc@^jVRvpr~dep3xMpGrC2IDbeK6B^Y^VpbqVomin<{LYE>luWhLm9;rP&uQ1pBM z=PqqchF1}LcKzp?AG{-CUn9=&_n|oB|K5K7xP(gvflrSd{3$=bOHvq=uHvA-URML@du zHt^rt|Mzy^gvTbVQC7&W)5!w-{U= z{5A@XT*cYlv{wT+x&C}9svn*2nB_MfUOix?kcbT%>|4fdKfu6A4YTw7vCcwKf5HxV zT*VK_Io!lTV27Mm)9o~+@5H(xrN&_q@$Kj3?1}kDn27Bs`rmUlb2>C{D_qj|mvQ{H z65%R{q!}kjRXiGJ1qt4l%AnMM0-%PDZcLQxEsAP7O+^2j1pytB3c8d-JB4l24Ln9| z5MI&c1mYPsK!%Z+C$9L|00k|Ax$pzME2IhY)oG~1+Wr8IB4x)-r~x#AxaZ^ol9bm1 z{KI)}&hpoV(Sn~4B-~}m(1*|_p#|E4 zL!@2UI*)+BxRw7bbY<8>WhYVuSYVsT4AUPW1gJdpj@OL&qv)jeb05g=1m8Mu|6ylj ze>WzKUH|nabW053ynse|ODGkSxaQu+=f7C0;U5Ix`6i_4T-gU@&;p|EbwjQkM!;Cb z4v+&=MeJ)4uM5?~_cjfKw*WfWa%-#PJqDik z$|gV1luGayq*XyDzG!od)rkUQU&g}v(&6;AU!cr=fK2HXw3%kR)igl`KO)Bx8|%}o zvi=KsN#2h9B<`3Nd4Zyt0YLpkeV}&eDNhG)+k9aiPtFQ z&KR}!x|O&HvMXk#tp8cbP&mkh6N(du0-O&8E%x)ddMjByNSDw$^Bin%JFTj0PN+FJ zTZk6w;vj7-Oyt6goU~t65%#usP02`sU#F~T<}GLMlb6bXn@~&a1cCv50U$|0_~AMPu!CWU=)0y%I;0Cwv}TH^b-_s!QAN7b z<%r+rwm7WK=_{(})2yqeHbbFR7kI`D^qhdBY=eGb7eIVgQyqDHP&*?f@3rYOL?{h^ zw4DK9_FFCq{QsTY&W_vdE6f(lrxXztTqQ&anH{Q|dVQ=yPHzI`lA4O+zeCdj8Tn0% zvOsGRCjCW^W;rGq@-rNWs8g}>v&IHba#ro906;K3wa&Hr@y0+{A;rY^tK(bxf<-** zQ1BkA=xHJ2w{3XZh1NOpWfE{*=3%i=idK1t5}#ETugH%%#!DDqCc%SlM#*3k6> zQoyoOKk#-2Gcx5FUTZBbB~yD7#T1H6*S&V%Ueoe<(2x!Y$LcD2S2&vP zLCet$cfDy4<9r&4hJrrcTHkvE?HJY$MBv;HUsbdaQ0zUVyjJS6Ek{W%ZbO^#`8lK! z>B2UmApw1t3}Hca!G-Gmd!l5D(81{4zM2wxJoFS3Ns7S{qlNy4v-!2>#n!-B{6LYS zj@k^BMbD~|s!LqW@#OtTz#pD-ILME}q$7jtmbcQidA~y>#qEPFdi^aKrFm^)Kz4+2N<9l&^2zxdy-?xxzR2^5FnLXwzZJ04u=+ z$h((SEr88^wyw}8%SojjD4g&e4qzDQ=

+7Y=`TcG|;o4%)J-bBPh$MF+_6y_1Yv zGVd<}%xOtTaP2S9so^bAbU&>GiiH0qeZvPt6mnO0eHi+A-}$kSm$Px6)in}g?2Rc@ zFCMB}VJ^;Aqn)fA$Z}G<>^FQ5P;WEC##F6*fJHS?-fvTN0`hF9?DOihlMZ7J%R0M} zvVB$^QbXmgW4n3n3d*UsD69k9B0WP?EpCqs_zZjt87ML}B>)9(O8`%oajzLsbwloU zW5~ZUY4vJh)R^}-y-a~44tpzX55shV&BNZzNE8Qt-R#`Ng;HBf#^g70fXNz`<`(a? z+~3`k?GAm;s$(pK>V1SYM z?G)WoEDvteW7ka0DQyJHeQoQtf`-e-BkH)c`Wx3Y*w1L#o#m^lIJXGSuC+$^C!d?M zAbW@GPk)rVud0~a1uS_Zn~-T>u+!F{W~ft_nc4wjiO6}R<*?)fLe}k_>}PcKA&pTV zRD3ZEcAUj49YFrn!7Lhl4C?#c?%fL z^?d(%4&jHTz1y;>No1OAd8+ScpzhA%#rFw!CW$e@j(9-2Ku+)?n9jRg(6LbH?d!e| zmwZA_SYY*}Jil*+y4jegms89G{FE7w;w_3%-zbUp-pwPssd#IXxONH4ozDQsNTx$V zw#^slKxxH@&z455T)VDTb&=z4noD7wM2V~5hZe!KZX7!ly?v(dKfKW&S&_RbPb!7b zIw=Xk-+ikQnhiqCvcQCQ=}bBr(;CGLvIPKuD&-EpD@@7(l20@ z1sb2(vtTbaV`HS1H3NeGlC%`lnRgiYHlds$#<9%W997EKBW|E=QfPrj^$DWm1TCXy zdt$%`DI2|$S(#K4owTG5#6uC|qQ(7w zhlCYolyi@s2q}xe_1M_F%HCnE)crt7HA;Cyy10lfs(doo>3XcUwGJDQ@|leH2JX#FW4n z!y}W^gQvk;8M$c5>%3)UTf7(Nnd@~AxWE5+hRHyP*TFy^nlE*ZP=MhitYJ z?tgTlBcIFD;47Qw>|p2(&Nl-XU)fi7pZoIM2ucJ!OU7BtFrEFnFvT_e>+Pwc!?Avo z0n2r|mvrVxUYNw7zI=}uDvPNy^uXt=?;WeVFyC2Xbm%?m#F-RrrFQj2Z~(9~<2GMA zwW+Gwgr+@=ne6<0uk+Zpjy{XttA70>XGzJE8}HQ_GZ#khvI>OSrU6jEB$Fi~Y_$Ke zPg~QsN)i=#p%}~CmU|z3qi?U)5=@bd1ldF9B?bw~nFt^v)90XbWUbO?AbkQ_Q$MeM zK3e&u?859sX|!}|$=(iscI_dt%&$t}0}4F_eW9eN{rGJ`i*fOuGvtH>3-+1Zy2 zhemv~4_G3$-b^ox9haw(*ztWUb>WhG)XOV*z*UEZga!2FWmg-)xyUs1ph#ra$}7!nwg=o#+3wc&YpM`% zkrb)z=A*53q@mlvukuMDIzt;2bLZFSz9krsL5B@!bfohpMOD#j?H9Oi1d{TGD6|GU z9cGTGAUT(0Gn=r+so37xVVSbGJ-e}vFoxzdimojJP

+gW^1k<|}8F<(8^u)4ZVz z+w$z&?_Av)^StwV4|>t-XnBJ*znW6@CD(nU?iZ$a#w$wrt<59sXZj0( z>F}7be(Dn?WcBpf^wjnhfsaa@O5mGOw3lX#OjK&%Y+Xcq?^JiQ>(tidk+!`}ISu1( zQ!~@Ag0|LC2xzWuTd{tsx`%E}!1IXu?ZdqY+jL1^lcp=qy84Te6R%Elc08|Igf6ir zdr@!pd_iZrgYLJ_7-Tn=K$Z6CT&I~fr^0*uduRI|DY09Z` zd|WzEL-T)9SJqpH%2p!M`#wMcEdGM4l{Y_Vu_3tTYpvyR(quRW zKD(=zS$fEvOdbu4XLtl%Ec#;*KsUcibOl&aF{;Qvm+Ydm`d-YR_wXCV%)?c|u8>llz z>`r@s?rKrQA4@v5$DOu{Ea7zcvV8rNuyyx9)e1B z6;sE(at^3NB#Quq1Mqz+7O$gVW%tCyD2ib z7e6*`l^vx~KfegXTk)AKJM!J3-qM||Hfz4@>+P!?d~q5@)$!s(ld8*o_d$Weq-i~z z&)%Yy9p4o=y94iWJbZtsXyhoiJCBOEBR~Fj@+^hf`@^r$n?#Edoy4w^J41Sl)8!MX zB+V|_!uj1VqoxeS05g`UA_l*mj3`k3DUx!sw|wUu0<<2I$y^ z-c5Ve$(K<+pD2mTi=YjWaiHX_yYU#-E}nCc-BJJjo@t~{JxAjPIj`$;xyymiqqv%S zI>jggf2@&N?eopSe(qh>E;@z8v;0~O z!+f*pdF~y~aCw{9Lpu>(VFD7P6`MVdYi$wf-zF>#yuB4Q>$=C_*u8I}7?rrOOK8%V zuDYkew3=>&)g$&Rv-;Oh$+(BHBF{R@kKX2EJ)BtKHH=u0aHN?m?aA^suQ1ugWl)Ap(cksGVeBPqq5vDUvg0prHH- z>GbvGc|+MR8-x%mYO$TXu!@jT7F8m&dxc3A!|f8x^Ud$Zu)m6G5G3K)P4n5jIEMxY zs)YL3*C|v{blNbSJT(s3&N-mC5q0CR63FO;-9?yq-;KU=ziRV>3(>ftp$IOJ(%7Sa z9~Z)md&Un~cQd>_f?TSGeKSJ-Sdt7_lCKV17HYM!slE2OP5XJamq$5+=J%^U2U1ge zHUwM?s`169Mys+1TuM0}C!cjE{!2*m-DGy!RT2jgZ+Y!IckfQp-C;c^!m53K&DD9a zNUy>$qhg)gaS@0$^%l_;S%xtM-E4v}1#9M+tJWh$3gh^N!S|Qh(GjvHll}JW))3a) z5Z|(jR*BYSbi2%DJ#76YOs&8m$VKqTD&Ae5v4-At(PMgMGO`^luz8ia6|SkhLJ;YJeR^mLRqwVX8`jK|+Z^`DO#C23irA9#1EVX0`h$QZT+HT3RK( z=$LG{R_OcDiLeS~TVto^FoiS0)6X|1^?VAd`vopB&^g>I5zb?s+RbG$*vsU*a_Llb zids_GX?3H^ArJ4&zFgV-Q2eUN$K{oiDRnH_i3BjCxB?%SOuaWxU_&2k8l58Z7x4v0 zgGCQzq3a_)orA~NDETJKi6!65__|V-TP6(@B(S&tuWH>-t+qXC!aFaX+IbVr>`4+Z zOW5HS4}oSHdCvD0t%}~ffynFTHG1}J`u5OF>Kr`vTFobt;hTr5MDSVNkciW>u5kle z;X79$0iD;_+Vh=>6}9^UNwuXhes>Wa$gX15Y52w$yfK;QN!D{F)DkL9m5?Z{zV`wT z`Z?C})H%1VzN9HTZ*O$G5hCCIiR;g-1jb+Fc#=~T0Z-UkD6h@j618i1SWATOEO=&U z{Ie|Yi_E`@m7S0dkeLNm!Q5@-u|sOBuhuLVC}#8dTIS4AdCv)^l)=v;rcvUzhvd{P3n-p!{`KU<`WBD-3x_iQt~2(Rj5 z0HhA-&)|9K#hTYl>vfYitW&pdk2Z~YjjW?5QF$T$uDluhhhkf105Ugk5qWcK(Zevn zs+eW}GL_x~77s&;zce3zuINt@I!cVVa@l8k_v03rX{zxqxSdRC89Ci7KjdG<38E{epjSJT;KfAbIZ`1)SV8}@Opb8G{4W^N2=0Eg;|=-Z%U4Bwt0 zs^l7(D%6{*^HlNoH!J5H%KbdD4;{u}UdQNzWKrrob-s`+n+;H-7?u!aHk`rY699iP zb8~e5jpEToZxAv`DI#0b6|nn^{pd3;Gm)8r-DRPhtn;Xo?kCH|-IXt)tJ7*5Jam1{ zLK_<#zt*yu0cX_=S)xqLLij1o1ftX?Q;l!F+xlMqRzu_+-0d#kT$e_LDfzbivh_np zdkc*ptTM5oL%%{h_tdA>zLvNmiO1@6CRp;auMvlv;)mz07` ziW`DC>WT)@bPTGoNb@M&LK`ku;--5*$j9%jry3Qg>jWMXes-!i2k`=W6txY+Vm?+U zvMCuPWA8`f_76M6F?ToKd*;JlaGP~GNW zpSh00^-65frQNK$Fdx#tD{3*KaD-M7oj}sknf)x@G~JG;$J@zjV@TV6{pkcZ-NsO0 zWWw4u$%51844MR?`BSM8S)3K#5!iZ*6g8F;hGs?dFJv``~30 zkDoK}UYIJdN`uyp`^j?P&)@yiAU`K&OeK7NhVFF$%PD799Q%%9x<99-ejJ$%l*M z_(17qv|OZ}8G7(Y#i@uffe{0b+jXzpFx6@EJ5KqCamLJ|v+Xw?zYHfFH=3u9W zY6vHg@qr?c99eK%PWq**mdHy-^U(ntYy@y@-rn2WHU+ipTcufTC5P-{yOQNZK)HD0 zYBS3eIhRRbU!I=cePO2BCwIPjeQ0!k8e*NeJpW}0*>3?C4&$oRUC=yo@cY#dViJ;N zgU}uB7=#?GmXnh9Kt-pszqd=qNvN)F5BE7sz~18i9&>HF7asM`$$5$opPDn)r2H>$ zLyb-4qXg)guDsdmW0aj=LE1J!7ZsklOCjKNKbY~sW#v5GAaDvBVB|V*6SKPRIT;^2 zan)0@={1dS8YPorh6)XXVtN2zDLw-o6+JMGMRjI8Q2J36D5QPg3j;%^l|hSh0xY)6 z#IQSvzI^Kkz2%JAYA1X$_JEyDv}44UnwG!d-p0Ephi}jZ(`o`e zXhToCN}*08G5-P_lYlEIeXe1eoE>r`h%Ngj~Jgx3J8SSo|sai6(ip8T`ZkKaT&_y*>F=Hm=7{U535Es9K{$z3R zKN9ZeXv!x^Qy7y!P8V-@YfgE=S-qxDgT0#+hjI;>ShwQKqwoGr%DNX53G!lAb3f^M zP3JY9n!ePz3#e;p?k9D{=3jRR@4E-8?E|UARu{8!?uSgE)E;pTHq+72l5_$th%YmG zUUya?1Zm8o=pTmiS0%{3mi0&ANr>b2P63}1hm`yI5nzVWSQsqX1%6-Uc0C*`;q55! zq}QR6xOr8$B`b>tdy^%z<7!(=R@B%iAFvdyH=AUlk>mr%(X}XIT^yTC`^ecmOzs}% z_xz1vY)OS0#b%Fk8xONd)VCJ~hfW$G1stp>OK5P-EKDg8pLc$`Vvl3%nxdv>a5je- zv0&OaG2%cBf^#41!qjTJr{4i2!v{ds_&?x|9E*g6qOsu`1-$aHu6yy~CsBQf zY{0kDYFyP(bG>J3@)Zu&2wtV`)a=UnV-NP>CDQFj-{YKj`S|{bRxOJj1YI0AFIy;$ zsXN4+?qGhcWw0O;-y(5oTLQ&%ajZt*$3e%&q_v}|Q5!%1FK5yl6U0a9px#%Y=PC3m zGDEFQak44w9rbZ3-lyu)a@g*n6a4SG5JvQJXaY@4p|a8dl?o24(RAg!yJvD3GTz`Z zm&*d(G-(+&I^mMtqe@70OK-2t4=9PfJa6oyG&fRJh0A|`1YkaNP_eJ%=aeS&CbVyQ z2x^4cqHK)WF88A~$bwr`zdmIN_(;_xymX^j^vW%TnBiMbeO70CLT-5BmITZHDVgpa zeiQpM*bIIC8UnKF#;$71!@|eGEO1olC6XO~MA&{@Nr4MURIAa%jJ_)Z@ssw?v|V3i zvZ+K~CqQ=6hl_6M%f*i~!~X5Uu)(BK1TA$*QJSZx5XvE{5h9>BGLbJf8`CC1R4YPy zy*?5uP&u952G~p2n`4`CRIJ?i=QWkotb>T1f!eh>^=V5+tGLC=H@Q`H_o)|bA@`&C z>_6aUBx`A@sUuPw`N?3W=}qisU&W+CgZ}WByBZ#rKuI!xf-j-H1`)v)%)vvm>$l&; zCBB-hpz?@*KIN6|aa~NtNY|CNTQezGS^Qdw!}(kjx|B=AXtEDYaHrb)YPm#(PtlxT zBe9H)3_#wm{mCKz^7cdYK9_!4=wA*AVp?uqI8=>CnM55q1T1P7p-_>UcYV|zZZV8t zUOfLN80NJN>l5kimUbk@z1hN&yj4dZ5gYaA-*!YC2rAiXN%5QKfXK&`aiGezYOXKu z)^j3;#GVZ|GZz+Y5$QNdypE+?RW7@jG514*-bo=vI$`n8@WT4EH<}#hdap(80e{^Z z>*yF3f#M4e;p;a&bs_ZS zbUMnoW0sGjMS^dRxuV!Y9l6M!UWWUpE&g-PzwGtHI8^JwC%s0;6iTi1QMat2UD`YU zLB7mq0I$VKVLVeyrd7#L?!G0lv<1z)R(v~8l26xpzDrnM))Mp0$#E3ad{lYKQ+$nF{R<465oQ?eeT4zgiK-N4^8^FqEycN z2~WG3{@A?#H6Meiyqntew@Y;+v)MseEhY8!&R6c^>m(b4D9{&O{-x8JG zmKGA zoR%i_B)KmyFMoX~KjSmq#L5U(@>d zxRT|fOhHGtwXQBgXUIWC9apvfq>qz13-;@! zbO{*=6k4-8>eaelcm2g5#)1v@)3Ve0FSq}n=SZ9o<1*hDF!EhxpGp|*oSmV-q@vx= zJ$sS63XEj|`FBaulUF*;B}Wk1k{Sx{enHp0o&GbM)ipT)KMD!{hymLy=* zi|CB@v7~I`e>rZClizH%M$*i41Xah1%yj)#F#S5gyeQ{`J!zq%mseHes*q9^2$!U#>}iyS>B9eZV3L<+74ml zd53S*+x?F2**;5?H)uudVhSMH88K?Nw`0EvKG+IRz^K=@?~a-@Gt&x!+gzhlEj9}u zm%y7sO#1Ely&C1D^~$Ai#LBUrhE)oiHnGe)Ad*7d0%05~t{$*7*4Dlx%!-S#Q0l|x z^YXLngwBC$(VQtmM<>Buj-=x1fcrT?v+gQ*WG6jS)_5RA=Z=v!H6~* zs`WFNIG%?PiMfQE&rY=`$xss2x@Y8o>q<``+=8$TI}n!Iv3@c(ccrf%{W+#c8+;+GTJOrgcFR-F2oyP4^*fVa z3GLjBW**;nM{cC=lPJURTk~g|oyAe;*ZsR*dCkeyV?xMF6Z*hX*NsVief=+~XJX7z zIjqKTjvXcO#FwNO16S9+wTVDdMS+NSZDEK3+~zlo4+_O0U3~B17VsS1AiX?3s0iJL zL7<4H47#ybz+MDG!b$&nC1lT1l}>p^L(&&3^0emJ@>oL6WiZ#d(00EGz)l_Do}v#G zF2>QtCV9FBV3t#M%%oYgc4FW+UbxRywf*qyv3WvaQ81L+z*zR{D=mQVwz)nXDY z6F!YDXA72R-eAi5Gs;LER)V`3k5mb^wKgi8<2#h78leI~E-(008i}(!j-5!|a3^&B zY|qAhOjMm=#Y^e#=hrk?&R>TJb2FkT=li?5{2o`?3vNFZ_#-bjtJ4jYAOSs@Zz5JcUGSmzgJ_ z5v1OO?_&nsN(bNUk?9Pe>&DT9oE$bVOe(jH3-F1R3nWSs|KhoiQUwGCUV_NQCUA#q zsuw)UfXPQ2s&BquxQvNhS8)WjQOE7GAOCT{pGiG&SJE8OSa@-e6GtV9@u1MC?L5d_ z;^yf&vCHJ|ssVo%a!|cqW}73@i6Aj84#$v;lFKvgxUbOimpYE32lu0T|HvXxEkN1_ zau8X?Rj9!Oa}Yx8KT;?@;P$w(`=D5~X!`eX&(KmK^1I$+jb4A<(>G&;AjPEZzn6dR z)j?x@g83yx2n6D+mtk)|811P{q~X_`MBE}!e>eft`SfnGtS|!YPLX@( zt=&jmp(KBQw#EyOV}-i=)fJiZJv@r(_v6q?sdsOKpR$n3MuHtv?6hg2T;Wil{rRjfE&NsS;CY~Wg(I|rFiRT^6iP+geZe6Wa4H_LnQDn)2C^$z zw{O6;V~e8>%*wAJA+@n8Tn%^}T2P+7XHNUjg9Q%>j{V6v+JAU2c6Es$FsB5e^%XD^ zLFyF_oBjeWJ9bDeX<}pfYi8+d8i@v|24P)Lp_*-b=L0xkUbj?jNCdFLsefWk>jda9 zZ78+b?1E;*k>}5Cjzs@;w&KOX3Os+>`#-zj=Ra8~-GE;2Got+fP@+Z}W0}ahZ z!q&^uk+fh}UVZc@pXI5D{(o3|4`{6W|9?Cpmz9f1Br_sK8OffNnNn8SduGOk$heG> zRWeFMLXi<=ZyA{(WbZvLdqw}}yU%@he?FhP`|~~LcmC&eI*wCZ*Y$qCpRechv7Uyc zN6%z`;+fBZQ}D5no!++shgT~i4x@4z`BmG0+*-^XM_vv;ongYmN2=g&{@llhlBZEg zD)SXSR$f8tuDs(f#N5YA4PdOh+U_}U#_BrVWJ{Pqvpjv(BLbi3F9Vx@Dex1k>>oc#1Tf-TS)96#!T!HE7{ zbza5|k|&daro2dll~z{Wg&txYphJ2^bi5vF%!16VLU~Je{;PF;Hz9%n$8SD5I8~Ho4-DR}5B86{jR-|dns0dJAjKM#VI?2iJ*3|pK`V>^A2pDbys8e#FQ}(6!MO8vo;>27E zW52@s`xl92wTJa0^{~D3CJw;?1uWX6N|O+TWtWV)1x*8DT<=Frl7nV=iDz6F``tm5 zZ7H7O8lSH3wf#wtNv=@pklooB%5vrBFlJa=)IyPI1c7}bQ|D72-CKvq=}!XlpKsDw z+kGd1Htdfph$h7+vph!nIqnyz|L0TjuRC)zTq?)tQ!>ETMJuoaWo{X|nIl2!w7Sj_ zd3OeS?KD9?oc$>io;gx1dyo!_DM%Gi!o8hj(@Ge2@ekY8WPsFBC6q}re!lK_5_)t1 zz#sxW8(imGwbu_@eD0Y8kQfcdtCPcVXMSN1@}f|K+e6P@`G@OSaLS;h0O?7|l`N|0 zSXJQ>E&RSA1mN>oKcOD*6$_|-C8X6c$mbut$#L(c?uhXDkt3yp>hWf?3Z}h>R_J9f z%EY)zjC1<2v$M~ff6Rnl0tD(TtigH=W2+MQ7w*Qu3@m>Ratn~O^KaRPetXNAXCHsa zruefyx**5I?vaYU-!doRQhWrDvI!#&8FiPqt>%58tSZqBV-QZdgiLN4NH?cGznqS` zb}dPc`0M~MTQCo?Nd^Bn%HnL`gHO1I&!}+~3iAtsMj98eY*=*wXB|vA66aYhUWb7$ zvg=MgshJ~|kbVkw2u6H<%H78_PW%`6n;Z(PQ0KC--EWHtmAnOhOPfD9wG!Ojgk?{- z3qgPEcGkDx!`(TJVtRaV(1@Gpr#d@=c;}m2cD(0?woedsQ)bDE;yZVs>P(@KxQF5% zpoES!g`20tGLE7ISOl>1z$(UW3pc`NRD&vKIdqtTvwmn|ZBzt~J_wUn=!3>xOEK!q zOm`UJuliwm1xj;o&P4sM@BGIX;b?+KLfz16I7=c+eqzq~4w6I8!zI}hkU*}%?Dh4{ zjVr6(>hEr;LvejwSYG0ZuS?6Tr?YC9xCq$kQ)n)Am`{#44Oif4(vCO#?!j_D{N;4` z$EVFkM1bbY{5Xj}atyXKovNSs;5?)DMk^;N^8=`#@$KF<@)Rq_yU%hpOPlS)uvJ}P zy-(NGlXjgAS#Yc4*uc&Q8$NCMYEGAMW#yMZ zi@5Yi%~zugd^T>rQDct)* zjoEqRpxxS@><;YCdUCc$6j;{Kt{{^P8Qr~vrE6j(zw_`Pr7`c|Ahqy1hk zlu`UNT;>Smv4B%y*lH8XLjB4_qbamUYJl&am;wjps`b?DTI(lhvyq0=9T-x!hNs+` zFU`D6bLL$)=C!rUy-E>UH4H_1yU@|paO!DhB6KWQv-yra|2=#4uC#k92>oV0TFWE0J%54KP=u49cDuW+m)Vb z<2Zo56=2YufPRFcSW+}xD(Q&*j}(Qq0mu9Q<94)E90qGa6y%F3fT&^|GXE7i3XKG} zU_xewBA^jLlJvA&&wYlS?wo=5(arg9H-MEMv04FYq7jJ$K3z?S4SRDVm4n*=Hb6_! z3*9*bpJc~A9Pr_g#yO_n<*{1fHVzF4||YWc4Z_ zb@&lBH9?e);4;RfuZd8_BdstrlDovV$8f+DK81JrR*TNQ3Ps|&G&S58Rmy_7 z3i?*f#99Q$+QY-}e)rV;>t2!SMA`HgTsLcte>bRn z{kXtEiZ#d;|7O$;=fGJQD(=2JI*O$%X2zF*O!js#58Coucb%bmsB=P$)@iaS=M&G( zW{^O3u;NTJcBIFv%IvXIbQ>GvPgLI)+=Ns58=&!05e*qT5cD1nRWfbsAPZ^(+==y| z1#PI)1Gx$20KAvBH7C!qI=h9QPnPLYja6fEWY`LV30feaJvHPVEeZsMDH!qW@3 zy=ms%$V4&cgQ0n=0=Rh_cweF+2ti$NUDP3WI%dPiW{<4>=_N24h5OV`Fu&3G zE5+b-97i#%UYr1|tg9435GxMpsUv9^7i`8rCTv>iOL+P)(jengNFS?t0fz17L5i!a z6U1TFfbq?0-AGZsetiYj6;NUt4L!5L#@TWB_#47ETX+dN@|1Weo1uaTv)u<1WDw5(Rq9v_MkF`IF`m8%lSYq!6ud z7wr-puV3vg>J72s_g1G`yC>`Qu{e+Vgz-H4!ir)dUYsFX*Z_TI_|2FA_CsSdZ$fUp z#89qzSSI=Q`)tPXeBUWm#qGFk7+Lds#{jKu<28@erTcUQB?#@~jb&n%E#^+ls*7}6 z4=`RWBos_j{DJ^MuJg&<;X#?g7`AbH03Nq-c;W4i*nY%O9LbH%g%M)+gAifPY(8`& zd;11@7zMZPNKw~2ZqRsrnp%y0{ThSpp8eWKVn`K{``I#LMJLC||L_d}b|k|@uo?`I zbTFw8kk+$)Sg+Q?r8`x>N7%}LuQlZ5gcYup&@qeK=YaqAZY>pCGX`<+Z3*pQQ2X=->dc5a{8&4;ws zk?l@*d!a!m!M-(^q`B$$L3SHNG84c6^p1m$)m2b1u4=-P;5w>Tui7<9$_IZ1y#H^a zwH{r4F0zK-blGV@(r`G^NAf1zB)(Pj@M$aRBhZ&i0a=ycOs;R0H+j6T%39A?^ z%cYF{ZwD}3Mi(=;-OSX{My3^-+fYbH=ucDe=oUF&KTm#8I`Tld+G|B($6Lb9+y*ZX z$16dSLBWNCPMC^{UDMH;2eW6p3LK0}DY)F4g{Kr6S0RSuGP8vA_cZnNC}s-U_*YV9 zHJ30)g-eT-`(U=66+buVkkZf4%zuan7iHCz5gSk^YaurNp2RF?mS{;t3xbGGs>X{| zMm@3F24|`I0ht1sp!YaxzkGHwM(3-+k>+W>w=W8Rh?`r9mre7p*oMT5>7^%dK%Zv9 za@>RSMBvnM0#&g&F1nJFEIQRnN4AF$M7NUo8jhqTK=7ZAL1N_t@hZ4%uA6D$*3r=Y z9>EB+%n!r$y=h{9p@P(cjk994Do8MeTo3B<^KRCKsS~H#t;f&#UU}XzN0QVHos+KY z<*L_!Z*Ckz=@V6^gEolIdHPzhSQ|0HBSfx{zO%8AZn*d8qEDMGiub{KrC-yy1c|`a zjL+!y!>xy^&{e}Dk#YH9l<$50P2~o;k2xAhlQ{&)5nKtfJcr_#+=7JedU&JbaMx3! zEcJp-C0FN@Yv%hklsszn!m9D^M$n?Nti~L@$1RgwXLot*WDCu)R@s|>F1yGLz1n&=V8dLt`k$F-K z4>N=L#A8zgO@mstf%Jf+*wvCl6y#b!U*Nmz3=?`4yDXFpbmp=>_VPM-NvrK&ng+w{ zAMj!eFF%S5whl-0BDx}SLn7$|@p_l%Wn3|2@WCrx*@3>oFH{TkJ2d#rhlY=E<{7lL z7-}#xyjYfy>AwGCcc>xMfFRPK>QKo?15_~amqyEaa-yYEF_b0f(-b3{2A7?`lGcT3 zt@}yk11R&N5H_TT(gZP=(^V-y3!}8v3E3~Ka3$XynkW4P2kQ+ANxO>`_LyhcLC3NH z0K1r0_cFRKS>Z|EVacYglmkgZa(t^kM4V)a3M#R}?z?(0@8DC?wh9om15VNFwg_My ztr`!{c;D^!CJ$PK0iYINA%e!!9)InB-*Q(CK0T!IWw?fF4v5}Fr5~y$b&XklU3gSW z5A+_)3kGDXBP7Z;H2e-7(dGWpDU zjUiqdmG~e#7JUbCKb>{m4^o%RpxyPunU)Eeoxrv102$T%+C7u-h|L9?9_GvE{LW#$ z5VZ~?t zdizEbp?Lr4Oy?uQD{*E-gc8~%6+(A+{ zWsfMLmE;c+Szxp8)hbJHI@yMT#Ue4Dez`F+wT^Po1m&cip>-aC(I#zVr!S$Xd{&-F z*F*189Y{f>lbPF(6%rs1IYqVd&XCM_QbHy}T3#_L+6X$ojAB?s)7jC0r5CUUX#3sp zF5Y?4c$Gx@vis;jW_y{Y=X|CQk)R z{pqN~OcFYmL+DMrg;gtB`Wp?55_82{;d7Jg|6%Jb_XlMBpE%vmsr3ljTQiXIE_#-t zid4^xN!L`@ePSqE2PZ51GaOT zqs%-6y9A##pSEv($-KhDiMkD)+YQwd3@qNxOM|-N?Io*piM8WFD_zeW$MN_Fl`Q=F zwXC}`zD%NfhEJGCAojp0S-DGBFQk4dN}%*qv5?I@8l(XmpdLe#ig*fV2HT2mC3^M? z*F$>MbJfkFX2RBgmgU`;t0u)AkLby@uuy9am=wa$hC_|G%Qa)d$~PHOi+hao1&_!IYCnY z@#Dv=Gnt;l6XR>=-U4K!mB_*N;+UQYa*|dk5hZnkQj-fe=y2-+75f@h;&GOg$;Rkb z?RWTOB$=ffw=@e2DbUKTP_Bsw zPIt!%iA@}#SbZL+IFPUUJ7_=(KQ_XfSjkLJOq7n9;J93*fmzK1kel?zvsCtqG_oyB zF|fUBy;1!sqZ#YsH>IR#7IQ|wOlLLib*IENaYxHakGgv@u|jPZBO=Ke#d?k=GkVOQ zFBfNSJx(T^S+>7p-Azq&m?0d7W-D#zzI5+iF7Qn~B34?G085drb^&@<1D?fnGvu!1 zc(3m!t7Bv-us9ncY98;Q*Y!Y+ef9T6pldfpF>+UraFeSLGvm2eR_)Zm#mLdY{^>N< z9V;Erc&wq_&kU&J8@2E4UxOECt-Gb|Gb)wEU}rn3UjvIYtW&6MMO}isR(BF*Qx26 zSy6!fZI|MN7VNPV*HV<}JPU_F@B#rQfw{Lz1BRsq0s7Q*yIkm9hsH9F-v=jG4D3%* zAtcW&dnRc879D6EbdzI^smgp1dUM}wEN)Z=C1s3>k2<5B6b%{*!B3gSz^Sg$CvF`* zqxEtsXE9KLvbPmG?*VGT2_Vqy7l!CUuhG&C6Uk_?YBJVz0brO`$1XNyfW{(eJH@mu zA$7OY#8k|Kd~kC70UkJH2p)317p;O)?BHl;TNEX$+D14Aw1BZECB&yy z`WQw;!59fXxZ7DQNu%GA;Q7H`_8y9AyO{HfWBzYg2A|TqBAi>3$CHvg0>pqKzR|4! zyQp6u1w$}N(QWJq5*5|e`b&;7JCz${317@zJ2H4g=3@p{^ zJcZ*_eY;k03#G&|I#9sL4B#wbRy@*qmRf+=CSY6Eq6lQer{{4InF{S&Nr30Nx1J}* zzC}Xd1jk@6$hx*{jEERF%0K9wE3U;?+CY%?aArB^`)Lk1Fe>BTLSw4~RKG=%A-Th& zyOK@B$6>>=H=bBD^bW7*ssyphK?d1?1RuhwJ)HF3iECidYMz^jJJsd}qx09Xw*dJ1yj^$? zI7skvuWWtid(%de_!$_t^_b_YDont=>%!#ZyMa~>rr|y+7{9J>Y3a;T~j=NqOk+}n0+>yB*ax7Za^7}v!_dv14ucfW8`t(TRzb{xM76&@EUFy6ATi8 z@UUFwdrT`_BYMoK?J%aQ%lV+c!9EH*q21p3FVvVU6*sF;Gi9f>P(ogTAnkKv$FHgi z2C<=UUkhuykGtn*ew3`LUWvCfd1^9{vTE4YL`LG7h*OOd@S(<~HLC={cO_qR?)ZfJ zZ7oVo`-cbWn4+$4gEjGu#aD>iDBVmE5B0e?O7|%qo}6U2n%uMK@4PE=-lwihqYjXaW@au19xY z7~cmuW;85W)1qU}V~MvM-}n1q+ow_5%uFUqK19Rs0!9Y)9;dF=IAHtT@U=7Uw|O=Q z8J_)pQ*FYCD1?6aghl7Uf@Rf$kW)MRRi%cQnNpbJ$X^gGv0|wGtx>l>f}7KGJ2V=H zbTgJg^1J>5nA68E`6-=Lsvy0ejpg*QcQJ^K%hx^Wlx)s~N z)o1xt9>sP6buhW|XyB%ks@kW+xys`c=zdZncjO>OXTddODi*C1MN zhv8)%`?-gEa@I)tK;lu#aB^RMAZMBH!4Ix7uVwtFq4H#S3Zg7xG5s7#>~VO!HR%%( z9`?Iz86Q3IF?`%1gmja;3M|(6e9x2LsX&~!dmJQzw{N+tx z{^`CWe)zlp3|ChrS{H3-+CTs%Z@e?q9F>2l`*SpL;sLuM-v0FJX9c z_h%6O*Y7Ve??3X#{cGa#2cM8D$=)ydyPMiOSgO!*Vyy@yLzydbh4O2A?4LbHC4T*; z{VV_SfbE6Rw*LC&|M&t3<&PJ2_@jU8SNoG!p^N_;lE2RL{|}NM&A7YTXYcI`5ViuC zYS@77kA&7u7~x(KeKMkP1PM+!oI2JcP~}WvxRl@W{MaTO7EZjtGx#H#vS(RaAToOOF)*f9A*^aCts}EK6JgvyaI0Lt=IS-ylmP z@#d1$F{3raG-{g*eWx&tT$oG2XX+Y)0CzvfHb|5CPg*Ka)zbNZ!R0fk`oBKs;odrL2KWEM}p*f!D?~qyhHP?36e|QRuE2e+C>+ZRp3W79&;o|=$YYEXv@v}spwIlaT^{5~;MW{4ZY=$$IRF7NFXA(Ku0^Z?V9|XS;&eWP2q~3? zyji9-PM8gmis-RYvy_DVCx&jZg9*DlEYl-)tfodP2?$uF);+h){r4V-+e`? z2!pssvjbrCdiW61!3(!pJ^}k6v*#Sb%D@ESodCUyVjr+i>dek5ucdJe(0g26{sRM# zjOuW(p^|q6T}I4ezBlPF_04~L;WJDCy2`U~7rzN;T$$V;9t;*7p#neh^Y36LeDAgA zj<<^7-a_P#g9QBJpR@rZe#t9GIIjK{WCa!diV_K`h}ON5(iX7^g4#I>Qum}0R|ZSn zY4x|`&q!=370Fo%#qJ@JwYPPj{?x36GF&Glp~hJsQK&?^3YZanqgU#={oT-y?e@QK zi!hXEqP-~GQsXwDyZMadOTdT}4!$UPFmjGVY1dGVw)ys5{}QVz)e2MPN1Ja+S1_+I z)g_fjI6#JQYiZ9%OiAmw9B~HGW+Lo?oBc9sJ6BQ*s?`B2T0u}n8` zd~dA`47)nA!tUjL=u|MOX7NL`U~VR&@0UlDP!<+4N77xdEi3`y*-Wkx;h#{cv%`bJ9Z*q`qpP z)SUpvf#L#IA8Ab9nbwoKfJ$smm_TZEpepydTBhLl#frVGTOZz1pS{&-clgFR)6dk~ z;P?`t(1u1r5j=tVqSS5dU%z5#8-)vh zd)G8?@3j^G^=X@l{AKNt&-bFTU&T2rHP=x|-qYw2F4sPj^V!^Os%JsjgW=sh3B;zi zyIv`l@?wH!G`A}FTQ_IEp8f6_!;Y*W5RSZAK!khcBGg({;a#_7!t9{f;q}R>6__+o zd3Zt?OmQpK+Prlx1$W$YYYXoFc>UuYT~rzPjyW}XFFlp@8P3|#b?Z#xZ=M1{l9#a- zE4G!YQaRWAN@GXu*H)@Q{Etd1BPU$@6P>h-k`EiZnQkKzT~mw(PKV&Vgy)XM@kX{f z^9L+mDrXXv=iY^Tc-`g9uA;K?1a-Nmh8GvpB+(V6Wa6Gd>e{#Z?hCE+9aQ;GiOTf3 zV%Z<T{ReM?ccXZ;xIkosD#W7oHdtrtd<6+I1f@v%^t=8i=b z#Od4^0uDBP0d~pmoAl*8CK&ZWG`HuH6W9Cvf1Sqvd12^YR6reIg~sA3JR4RPXU*@# zKPuaJR3d?~sW^B@?6P3Jk%VAk^p)HHhkvQF`5GRU#j- z|AC5LI0vdbm z*7x^CuNEdd&$O>V#<5j3CEI0Ar|~u45mBEh@tb~E;@QvC166tkv?sa6+D6egv-EVc zC`rx>?}9mpkkK$|`gQ!1%HAp^NBGG*yPqZxIt)ge_mxc;eq8j~+g+RcyGwx!%TK>-&?-d@79z>Z&)=-G_T66aqYa|*YEi%A z-@2;J(wsLWOCdh~b{8n#`Q=aA*sb}d9F|=1Hcrgi;t96!expB}rv}-3bT5)bZqbX|{JSf)g8~b!qzE9Ay z{iN2BVusC15{I+}IP}2Xs#LrPMA(LQQf!wtYmV-s_=DGhnxF<=@bKR?^>10@sk>`S ztX|~BogAOuucPIg)K$&@Wp17Ls0QcNclmNs3;1?ItD-d|CoBUTP8fgV72lZZx4Fou zHw_7c=LGelHI6AJ&|K~iws}8QpvtZo*|uvyJnm)tE<5N}H*nbHBIcE_nmlRYQTNN- z7B@X`OWK+fJGL0bKauMHsUUa9XqV6YLxn;lo*pPSx8WJXQi66JzU7aBYkIt5;Rfn& zZ$?NlqU*!P)~cj#E1kU2%7GcEkZrWYVT)$prstvDx_x75TAH6)lg7()Y<3~i(t+@U zh+0$wC7^kt%U4WWBd8?1L|MlVYmb-3Gd|}?FV@*qq%7=sBC+2(%9|ax;#;|Tnm)jM zALOFXy2;{oTXc*-Pe;i;o_zJ-<;n~YhZ+&rZ4fc?Tbf20ByE0v6!u-Ufo>(Ha#(@3 ze)t2SpLr5HU(qvW+84_}Ey>Q;n=IYw&cPfX2i@-jYf^&^vlVn4ktNs434iT$xAn~Q1$AVxvu)byY ze|VLzM3S&Bn7bW0DVj%FhW>2Z_EOproa_%K+`?^`H1?w_K*G9CAQN)qoI+?Ar48B& zR&!2tcdon*hwkGf_q>){^!FW_v$t-Zv6ILVEB`(VYXUNiuur+=^}M!C$*%`ozI-5e zMfjUhfc_LGbxiidM6wS?pGc&8E^uo`l=WYk7L0j%MKMfoS%#Et%h0XNk~ioGn!_-v z&({-MK~U}bb>+|#a6!#{SpCDjNmrwU3XBR2)zY&=7!2HOyGkbD>sk>UOu} zy+uf%46jYa(NJ*|Q#vZp@ltTtywr_vZ-8z(Pk}jylm?w?aQRpVX=@nbbiBdp63<~5 z$}J$*l7V;dP?~)%X4wc4pLP!go7BzySPHL~qIa1q%F`J;`+BEs`YCYyIn zg$}P02roR6)*&^~m{vG@VN~9tMef z7l)}{OwJexKFG>Eq)tm5t6AW|j=?v?zY_tF~4I40e)Z zUrZ)SS2VtLUtMA-J{Nl==%fbEsaVPTWythI$cq`167x+}0L$Q$nHM{!vxdVm% zYQ1{NQz;BdVI8cxshZva!$IM31-x4?V!O;HZc}6CSg5VQ-S2yqh?3NohJg3p5=b92 z^}KP1;zV6Dk@Il3io2P+Ggew36g%rKa<{*WO0oGg$pNxpR@B$e{k!2@>LI~w*E{Yl z6KYvh0f6iQd2t|p`3#{^O%$SuU2k@Zvt*al|{X>7budbr2fVbl0a z<0M$lhhfVH$R`KKrp9WhHlXjm>^$d7|Z9 zC;M}pJtEki!*8X|5M}3&f08&}&%b$iJLFMJRQuMT`*^!dkK`(`sov7D2tA5_BSnV2 zs)x^S0mxXK0$j2;q8n_S?ao?y%kPVJY^dyL+&w=kT=N8`l5d{I16g1$5OG!9REmuMmDkN?W}*1;P=+j=f3 zmK5EvL!Hmo+N_Qf>bLhhyY+e<^3dFSk@2j9O^i*foebzBM9;ly@>CGQEwlPx}C&ycK@&|x6W&8-(xzRnYyElCiCH~TBSozq;BgMvGJqO^YE6$Je%055F z(~Ue)jwN0O zhy#kE-fomp+}(ob9h|FjPEsr-!vG(xO^6fZQAli6WKk?KNn}gK)21{r>>bJ`!8w+J zKFj4PWyQQNXMD`v7{OEl0cdmI|P*~K7#$J`gQk7niy3| zH+v&fCoO6Pt}JPRra<#N=>+EJi+HPoA+(#Kwh=8>4pSD}_Pk&d;lHgfV!5!=CrLO8 zd?jm+9Ex*JS>p+Usg)!xCwu8TPPj@_HN|@N>C_+u^mR@~hK3NjrMsBIav(IM<)v}nsp@CQc)vR=p}WWv$LyD6mQM$N>P}7_5Vy6*AE^37xDt=Lz+aNUoRNI;5#e0I z(Fv3*3OEUJ##@jLMVkLy-vgUQI;lI}eeoe7=zXH#-oNBf^GJ}os&nMvmtwEI9p8Qn z)awTxwkxTo88m_U^qAMe)wBWK&)=zB{0d$ZwfWJp@HYS{e(uaqc>enn2NEkz;=@4j z7^vQWQg&>xpSMh755aKh23$%d;Fgm?@J!6&VrnzGoFjtW!`5*TnE&6rwKoCPL#a>( z;bidH7%maVWAnC{ua#gNnl+CyrB9qTmL7&JPmewcfQ5@s44yc;{i6;(zin_&qQ!q2 zo-5H*AyF6t<^bl@V}IJys2=?EAFCvjJN>>phM(AQq$l4l;Q|O+@2`++|C>Xb4vYL} z_;8qof*b#1NdI*-`)HvNSHV?3xYB=>gCL?m_y@+tlil*ahs3V4{xmJW0S4kaeMI$N zGm}*B&vA7Ur`YFF@#UXx6zEAGv)C{83nRY1^h0Yk{-u?YY z25?ZgHvt@I>Lr0l%yk|?F|rV;MZ~cmn!r6Vj%2g1YG?u z=AZxZnUQCfJ#x`u6dNaU_w=O^X2Iv)1=%oV0^K41z8V_)%gAYDi?AYqI;L;kD>K^8OarpKCHFPH?zujUKKHQlFG7yrI!MiQ4p4kXbEQv%mqztDq4_k#- z@*t|VT=Ui_AbabaO%qoh%_OxOzK3(5#}_GX1o0XO4+J<_PkIBite%V^Pv9H%(2?jc z-hx3v!NCFpp*OqG1gWF_y1PNf&W#Yq(1<7?Dk%m(a(JaOr@{r-Qf&UoIR6t_Or-RV zZiJP1;}1^7quw37&93Av_9NIF{2nXt!{<_=6TF2QsgE2vX3H`IMVSvw3cLFTp+jD~ z&1HvOeA^yC=F0U-Qr~YNQvRB24Tu`l{vQSdT2~?5g9XnWa|snX@Y_KQzB!t3plS;2 z+47~lXm^UjSGdRYdMCfVFVug*(!{tz^g{B!UiCK`F0~qpia+uGfAgrLctBqO3k5{i z6$>GUTrh43Ts~i|37u%qXtjMkB}j{tsk(=eP7t)#c%zvWZ_pBXhJmLgCwvpdD0Ee} zKnQ#GQHsxbEq$kEL9fE6O=3MlGDGSaTGzwzlqM`EC6YFh=CB>e1BGE(pBKBd(B>2!b zHV#6{HnchV8e(!S1TsQrJwlHGY&pYtV=tECJr)%%T+t}diJSB5en{z{EF6O*FY8>M zKqBjWP%vD(Hh`vKT8N`28ia&15|Y4)+$7Zl-8i7W6VaJ&%w!{aqU2SqHQZS&u923( zQ%TTF1Ij#Zbpe$5<#7fhD`HTM#9KjT#slPy^MG|w7^-fX?8lUj9555(FI)#5j((du zc!;cRc^&c zVKWyJXPcQQ38H5jcV;d6#D=fTO~IhFPm}k*gNv#1BxnWFuTSvtW_Vyoj4Wa3J9XJm$W@ zMbpHQTmMrTpo%#lW(DIYFHmxh3siRAnV9;ncR?UPz=|3J^<$i zg|1sE!-QW|(jgfL0!4Nu^K2f2pO&O*$5NWLZK0_xZ~OoUXn529S&G@i|CTV@ytUWE+=PXX-Hu*X&&KRl=yGl!UZ0q7`-k^rS$X@Hg}c)m3U(yqU{ zdB%yxEQyp2GosB6VxUC^Q#fEB2FZZx=>rq?3Oxuz*H9)QgV+8s6a=bc-~kyT_fI?- zWgv)miDz3PwH)7q+3eY!6?hC>Y>y7!sy`UTcIEI)PtL3KMaduAVS3dEQ7nWZl^$e? zqe-@?5R@!RG*#;f9A0j)V&U-%9x9%O!!d`R;--rrd(C+qj-&!JEs!OjWgLJ5GPCHB zy$?F^Na3k>GWP?Nq3^Q<{rs zICToLBZ2GKk6Dhh9bIRL$JA#9W=CNLao>ggarT?0GU8Rw9|NfcEgyABQr%;=f1<%_ z=zL(Nw6G`vQ^G8%Z4f>tC*-?GwiF}7Wht4^EU-rPiC)0hf~QQku(+)PvsA&>M8`^b z7fgm#EN{)QB9B7Q53kC4y^lOY#lsasx8OeZ^#g>Fkc(T^Ub!fHOl)0^%2GK&i*e+| zQPT7qsi8vx9Qu)N5^?0{Bnq>Yw#p+8p!9Qu2F*P8 z+xyfhY_(LCL0X@5kixMVhksqlf*&7(2Mhkk#pDyf8~^YY0#uOvtrh69EHhcLBh^!2 zXDix}atjU*CS`4hK7n9RE*oTHRxu)R$EC@pY9E~nBsMks>>$VZva>|YHPp1l6(avq z0gn-YBr~_z$Bp9DT(lm($6iR46aus1d|_;YFtHuOD_o6NYoWlR0*Qe$;k~avUVmxc zEJlH;ePMKV(rT!zG+?DhSjJ{l*n5lg3)=8J2=Rj7DqGjSLLXrEdR+^;lk@=}U2OU% z99zU8Bsp^M`9p>WHB4Ke?Hhv z*DN|tCb$J!0;^I=RLR9hw6p|AQKTGkEGzm*WMQ8U0ydfY$TU>mIMX}Gj|6eH1m={w zKtSC~Urnn*%oNFHFIFFGLG$n~BnMPiRT>i;)xev}8bHT`PJu6%?)s;O191lR>{B!) z%A!G~a5qGOinRgE#E1v$mLMpm*KiYjzrywUhcSmWdrLi620n<;D6 zj@K5BKUNZ;X6hA#CC$EjRl^aHkc5A}2ka#sRu9u2k=mWjY6eUr_;b|RxpyR(kSP~f zn0qVVZb;%=p5=`S7tVVSc9{$@p&O@Otr8bS2xNGlnnD_0^g+s6>Ud`u-+ZPi;eh#H z0ZUg=%wlw8Kb6)C>SO->}|6=+B3FOGZ3R2tFY46KHIN z?N)E<4<24yW-?5JP3 zcgc+g|72y$l1Rg%>(34xV0TcElfF3{lFwL@0F{jc)X9o(l!`hbc1Rt9C@fmZDc*tl za`rAXGxLs4zEcDvJ8>g|ly?vksW%y@#K)8}xDyb`Is7^(jF|G_QUX`?m*)vh^ORTC z96qkc6Ojdw2Nr={O z4f{VI?+0=R5^=*3jw|B|KJ`wpOggRZ6SOO-eb|?#B`vM zwH9)kcZ6rtUNr%S@hBlct>KeZlyRtVt+o|)FQA=P&USm$j}`eWBX6)P7RI}Oq)C5# zU$JBhI}8o;v5<$sb#Ef>^P9Igx28M}px;X)_y*U$-%>Gh=fm{qdXdNNR|mf4V`iqO z?Il&chCtEL6hz7Py)MI5Jj{K9+kLVHHjdv5ys;2nc;CzO(w>kETQLcH{9a)Ro41q* z-et_|)lr+@tWftkgl_8q3D8uZ|zct$6{n?;hviBBJ7%Wcp(4IcJj*b z`0)>~KBU3E^kJS?AD%|$rJH>bZ|{7d$a&iG4W+Et!dvjNisx7^#n}yHBr8t1TbXq2 z4sJ?p+5pd2eW$Ynrq2PVaO%LWR5ezOSF>vw(^FCnNYu}B&|XccsO|K4GOc=U>^6F~ z+Mb$mSJ>lz9iKVT+UTijb!tf1t32z~mjtM#7Zf*6KS)bxHq$q5>BEIeu@k!Y{75uE z?IxZ1uif_lxdHWBF+NBe1tB+E;90v3oFE^LTL@iBnVCk`N^P?LfU&pO_Tl*ul8)$= zXJx7ncoT4HXS{AzDpFbqG(mTy`*@+6Y2RMJ)F5%L{5Frj(P`)TAhu9@1eHP85-BF?pvTf>dOB`nXXnZ`Ch+*$oC8B1qmankko4dNnFIp@FX+X)L2fSd z^+;t@|LiqkuqDvy)W|oH0(6r zcdjW0S>F1XcZn@efc2E#$008dr+Sva8dsWu#jDXtmDIx0uc8G;6+dN7LGJ(ln`HC( z_jhyNn9F3TJ$#()vG5_<6tW=p!%3;C^r*!{IX}0>{?}gu85JPxn&~>+jw*%| zriiO~f2&TiXlAr?2B7d~QBg;`@2l*B>prdXD0r&`o;}O_5}ZO=0-R1W9C*zzwoMg@ zLZ}TtBpugej>XnjU4=XTWg>w*MzC`sng4g|7C)THhK}zQJi4$#rWwGUV+$IiN;eu_ zpM9!x@QiKWfLmEg|LUB}8(oz#A&A5fQZZ3uz;L|DrobaWTb#k8(gRP>3_1Sae)hF% zFJHZ?KGD4+5xGdSwU#QmK*b*>wi1N+5&(AzzHK}Mi9SLgwr?dd1L>?8420UdTewi~ zuB2z+(|gY92!TYCF~*?|b1J1ymp3$WF{ekkXrCI*-8Cg$CWc?1X6vKob)RKKy>Byo z`tKLj=OKwrf58{stlZq(r0xl$PlUH_-wsLP?+jLpml`VbgqRDR8-thVvp-Vl6$lK6 zyjDUn8z^q?nI4b(`wzn-cy>kVM)@1gZ5io5@19v{95W8fcLpI|BzAeM&fKgi=KA+>lDIDe3+l|{ zNzs_;#6UhEb(~tG;v=ncTW{>;3EuJqW;*GH%@Q047Im z5_G)vJ%WZw7s^+jW}W<~{@{h>Zgl4)B&fWYf$T8qCF3yhDCb*kW!GBU+@+;!Py6?m zpGvsL=hP-mbjfSk{N|RXCXeSe02Iw>o}*Cs~uN)xsAu0 zvm1fWegC?KzrJcCz~4}Ei3UL9V5x#QGamnyhP+ zbOQr=u_IA8_aIMQ5a7=X;_hqqtzEG1E#$JYIJAeCUs(w}w1W3H2|F4V97}-(dj>$w zH=DtdZy_jA_!k4En2m4d&7gCb%6#)hd0FkM#}M2m%~;|iO|WLvR1elzOuq-#H%%Du zu~)56dr>p`YJI5*UPao)F1F6oP6@klImE@&PQu3%;}h3EKM)+>FNNge$+TCYe2}du zu;kY2@kphD-fa;UKG$+oHTh9USRf)OfI4CvWg6ceM?NQ0!T3>G7%wUQ`BzTwX=?R%YzgC z*Q>dNWcouSm{zncOOJe&aSCais|!U9VRWmlft#Et(UNZJU_`p`nL2%fQeCJ^N;X?T@go zg!lT`-LT4~rxvQOZU~Gjuq6m6UfSyr(IjH9m#Qq7zloWIR_c zIeG`#h0?VYyvbH}VwUO_@1TpgTkY7L-USZ%?_VzcuDs?GDsGd`Tm;%F_k9=nd;O%s zFKA?YagSxiXljvdNc$II2vfWsZQN^kL~*e|B@WoZlty98J`c!y8CdU}1nw{)N1)Npo=*gu`*S1*e?~7rrH5AH=|nbL(76FmKt#^h}jC8nNa-n-!)obO+mmH(x{=G_Xt)DPwzO#CSdn@+?v@z48 z#h}FEL#pz)S_7q?0*{yetyagm?VZ(o@^mkW+N6vIJxdN-9}qnw2Lcb`=) zdaK2?%oxf0?4}Ee7a74GX@b*2-#C@X>6KmHU`SW_mmpI8y#02e@@*BT#6!)NCo;VZ zgn7nCh|Pd(nw-yV4b^|I^hlfN#y!qd@lUCOwKww?pLIR!wM>1{1bl8>uFB5qpyXZE zuNI>E)E*_^n632ewD^GZ#?>sb5)mes%6(OTUcags+YLvF?K~%ad2!OxL>`j3NgMna3)RqE zLuY7j(X|;0VD84(cA{dA=Y7>>;OU;ezID?g{B}o{7|kNBWu% zgp0f4`qbZGHK#o(-q9|pKKuXLyY_gf_AP$SDIL<~r0#KWlhP!VIptMNB08Ss5Kx15wBJ*3o(M<-3BGbZPZ#|%S9)Gd!O9(fH?XNWOP%rF>_yLLr=GPnEB{p)_N zzqb9^v-fYWy?*PrzTdTeYjF{qNIbtO#t&H-Ui#*=%lM)xuJ|@Qo8^lqW(!s<6r9nm zps$lPn0P&>1S?f?)bCb~Z6}|ss)KZNp4J$lXKFp?Hn?1V(^}lAYIMw%;644`26w@G z4g`(xSS9TX=YY)K{ZxzFDyfrYevf^}rcBnN(}`?F1PFkY;lXO=hrO-!?GX%!IKvvaW5Xcrz2*>G?!BV9DUm`?V zPGc9IJRRE)D0g_>=SwK!P6jPMP#Qw&%t5b`-TLCIV$u~Gjfh!B?98ho3<(;*IF4Wdx;c8+!1l764yeQKz3X1>-i^n0CRn3yTyY<@2*< zS-P+=x6;&A1CPFaS`0Z#g#N79?zUixOBPZBTVH|=+sWB^RXN$U^s}=rmXR|o9iHV{ zuZZe>N!Sy08)B|C%s|}l8BUOylCIe0V5zQFe}|I*^U^Bu*L@;E&mD*=SMFVu2XSzV zt1SNb!$q<;AV1BlZY7Q36?4&;7m1_iE(*!M{;K((hfu`8k9ou4_3ZS zT0_L}R_}IgWwlOc3G;s|*M{wu)JOxkrfWX*&`It<`#4RuaO}Z-% zKY)xvJ}8{R-fu(dxAotE@)dZf$wu>}OXP{!>R zj=R-i-#2?upmMhnwj-v7C)iaA*k-?YP`0FEY#moN`n8Fv&%`E|k_Hq#H8+j${4w8$=&-%_ z+89+P-1XUJL^hev|4K@O_;;zg|F!1m!FxF`6Mio;K|I5jutZrzv{uWVn$j&R?|b>_ zuKH;XV$NZ`uNPa&HwgCOjZXmTAvz{z+_QDbGXy-IJuoyCbT%hF!1ATQpIs#HLp!Wr zD09Uv$=t5nE8`Au0}Rx<3L%}x&gizWlb9?#9uI`D$er|bzr_OxW3_Iu$YWY44&>Ns zpLqr(?#*EEm1YW@RqJ_C0PVUw--OSEb&N&PDZHI)2t}@bEC5Xem2<_sOLRe_74uEQ z)@cdEGoA=3a)-i#V^X99Y18H?_hPncCeI zrk&l};{vir6KBL(tyI(l*m7AqX}R?;LgNFQMLP?KXz6Q1Q2X)<#=vX4$*K*-bmM}R zn7*9)#t8Q=TMcGrS^3N;xzxX~9`CkjlX?v5kq)i8BCqA=WXpYPcS*$qlq_Xh#7r06 zDWL!#dofV;D#ajV=zy@XDDin5U#b2NYr4j3l0MGK zF`V{VS^WH;O$E?@Xt(HDyX5FHlB4x+*M379e$hr>0qkT6ZcXH8fJ^mP`TZ_1A~!gw z`moySA``4Jr}~xGZbsOwqAwxdF}url^7KOY-`?m2cr<#@g=RBB|3nA}Z2rq}my_=k``+l8~FdS9n-qq#nA z+D;XkIYU!M7@Gl48Nl;^$Lx8 z^wI-R(PqSwcM_T2mM?N~OLrOQYv4V#^zW_c$jn#6knXYFA|5+5-|a#AMwdpjXI69= zqSRm|=+PpNORh^!Y~F_9QtYUQ@rcP`FaAfwEK+Bb52kqcUZ3e)$i=;hj_Q^6>wz8B z2mJ?YbB{DL_1`w6tdeUjnf%_3gg$hZ7`sg_rPbueaW&PzQaCQ|J^&<0gY*VoVC9| zNa41tOJa|cNF;^QKAXq+an}pnE}IV5L#u{E#~}^oWKjN_(;3|lUQDd{77gC{Z+cS( zjOeV*M@`depv2kZ^<0-|%-W6GN2<>40SMXE50PfAqh?!z@7Y;L0@@-h?jPd znfdHUVKCCKsi1SQZ+X)`o-C+d+V+PB&NPVh!$;hY6$5Kc%kst054&%R8h=|0uFFwU zP}d$&;7SCMPMqVQtgC%m*VMK#A1YuB?#miNt9L7$!D@4Z@W~rWqjd1_f9Um?C0orC zRUeJdcds>WsrNzaio-j{3NBH-28ZP9{76d4mCOy&gsAhT{XryO*16TwY#MsvuD0Nb zNe6U#Frz3tGz!9FvSNk4)jdf1G~~X?1w#8QrR3|4=2;a}Z1fwC72d;kn!jfh(INiQ z(dM4U%%wq>P67mNZ$MQY1;WZLki?*`H*ny`d{%jXjMf2a)oG%u_P`E;iwgk>F~ndHm6yqT^+i3hcBpIsYy(?8&aGJGoOv>VhO`yUv^Ua ziPJ8Fg#nD3>2z|}K1PkBVR=PrcJzHCSr5^}1ymj=GfpNB(pzcjOSzKhqh?F4vAj@m06kxoZ`?oP0bxGVrtkOP z4+@QJ88jp2-zUGNT*t3Eb42Iwgu4mNsaGMVG0%R5?#Wdo^xgQpa!JQoMUqa$toL-x zgnLnnD_$cI(pk=1FfE0u2-K5KHbf><)y<6!U71g9o4dQ;6RDBHllcAOzV&yy(6z{ige@8_6r4J^ICz(1TstMtx*MGl|#z9-0@JD{;1lxWX+^xxPfImI4M|uHc zbjqksGAJmhm;3kn{(gPS<*gGM{_@_r&sWy0~@mSSSwU^B7eo9h-j*ak^)8 zD<6E^mQk*1*xUfbUoYpnW0Cl0%z1gin;^oGe`58Ys&i+=UVr)RO9inbnSXAs55yLN zqeYTpx$pVUb}C+;v2YdrC?b_pMp)^~Z}T{vW^cugWd721Wnp3?Lmxl&pwp>%M{Li| zU(Du5DefIqdaJd+`0^??t)RYxlVi*0wM6{&Qs~V27Zv-+INJ9ry$-F#mtHIvH<$Z@>|9`&e zv17-c;nOe9X;vBL8{*WAaV4jVs!y4_)hBW+)BFZ1Fo$xBii*lLxlJ49ZhU$F`-x0+ zU`H*SS+_1qbaZsM2!O2!W=R`=(5g63IWSN1a;|lP-3^WlfAvujZhZ~2R`*WE{Yy%YGXgvCw=lZ_xjCVV8(#wyB`U)Y_%;1^1a`c2 zPX>|v`G64tI7=Y$H0ba(sPzv>e$&p#$iR-tckZIQuW|9W_};v`hX0Gz??xl8ner|v z5-GyxBK`<8WkH2*EY~*?=UL$t;~-z%eJIo<+zrwRoKAjJ?fAkm6$>x%Wdb-N1d%vhCk}5Wc66{iz5kmlc yBG_gcD1lRjNmas^ROSCS`&9Y=%QAa7`!&ZZ-+pl)#s4e#v-rt+cfRqVU;YI=p|c None: + self.online_store.teardown(self.repo_config, tables, entities) + + def online_write_batch( + self, + config: RepoConfig, + table: Union[FeatureTable, FeatureView], + data: List[ + Tuple[EntityKeyProto, Dict[str, ValueProto], datetime, Optional[datetime]] + ], + progress: Optional[Callable[[int], Any]], + ) -> None: + self.online_store.online_write_batch(config, table, data, progress) + + def online_read( + self, + config: RepoConfig, + table: Union[FeatureTable, FeatureView], + entity_keys: List[EntityKeyProto], + requested_features: List[str] = None, + ) -> List[Tuple[Optional[datetime], Optional[Dict[str, ValueProto]]]]: + result = self.online_store.online_read(config, table, entity_keys) + + return result + + def materialize_single_feature_view( + self, + config: RepoConfig, + feature_view: FeatureView, + start_date: datetime, + end_date: datetime, + registry: Registry, + project: str, + tqdm_builder: Callable[[int], tqdm], + ) -> None: + entities = [] + for entity_name in feature_view.entities: + entities.append(registry.get_entity(entity_name, project)) + + ( + join_key_columns, + feature_name_columns, + event_timestamp_column, + created_timestamp_column, + ) = _get_column_names(feature_view, entities) + + offline_job = self.offline_store.pull_latest_from_table_or_query( + config=config, + data_source=feature_view.input, + join_key_columns=join_key_columns, + feature_name_columns=feature_name_columns, + event_timestamp_column=event_timestamp_column, + created_timestamp_column=created_timestamp_column, + start_date=start_date, + end_date=end_date, + ) + + table = offline_job.to_arrow() + + if feature_view.input.field_mapping is not None: + table = _run_field_mapping(table, feature_view.input.field_mapping) + + join_keys = [entity.join_key for entity in entities] + rows_to_write = _convert_arrow_to_proto(table, feature_view, join_keys) + + with tqdm_builder(len(rows_to_write)) as pbar: + self.online_write_batch( + self.repo_config, feature_view, rows_to_write, lambda x: pbar.update(x) + ) + + def get_historical_features( + self, + config: RepoConfig, + feature_views: List[FeatureView], + feature_refs: List[str], + entity_df: Union[pandas.DataFrame, str], + registry: Registry, + project: str, + ) -> RetrievalJob: + job = self.offline_store.get_historical_features( + config=config, + feature_views=feature_views, + feature_refs=feature_refs, + entity_df=entity_df, + registry=registry, + project=project, + ) + return job diff --git a/sdk/python/feast/infra/online_stores/datastore.py b/sdk/python/feast/infra/online_stores/datastore.py index c623af1c1f..e5328a2725 100644 --- a/sdk/python/feast/infra/online_stores/datastore.py +++ b/sdk/python/feast/infra/online_stores/datastore.py @@ -16,13 +16,12 @@ from multiprocessing.pool import ThreadPool from typing import Any, Callable, Dict, Iterator, List, Optional, Sequence, Tuple, Union -import mmh3 from pydantic import PositiveInt, StrictStr from pydantic.typing import Literal from feast import Entity, FeatureTable, utils from feast.feature_view import FeatureView -from feast.infra.key_encoding_utils import serialize_entity_key +from feast.infra.online_stores.helpers import compute_entity_id from feast.infra.online_stores.online_store import OnlineStore from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto from feast.protos.feast.types.Value_pb2 import Value as ValueProto @@ -191,7 +190,7 @@ def _write_minibatch( ): entities = [] for entity_key, features, timestamp, created_ts in data: - document_id = compute_datastore_entity_id(entity_key) + document_id = compute_entity_id(entity_key) key = client.key( "Project", project, "Table", table.name, "Row", document_id, @@ -236,7 +235,7 @@ def online_read( result: List[Tuple[Optional[datetime], Optional[Dict[str, ValueProto]]]] = [] for entity_key in entity_keys: - document_id = compute_datastore_entity_id(entity_key) + document_id = compute_entity_id(entity_key) key = client.key( "Project", feast_project, "Table", table.name, "Row", document_id ) @@ -253,16 +252,6 @@ def online_read( return result -def compute_datastore_entity_id(entity_key: EntityKeyProto) -> str: - """ - Compute Datastore Entity id given Feast Entity Key. - - Remember that Datastore Entity is a concept from the Datastore data model, that has nothing to - do with the Entity concept we have in Feast. - """ - return mmh3.hash_bytes(serialize_entity_key(entity_key)).hex() - - def _delete_all_values(client, key) -> None: """ Delete all data under the key path in datastore. diff --git a/sdk/python/feast/infra/online_stores/dynamodb.py b/sdk/python/feast/infra/online_stores/dynamodb.py new file mode 100644 index 0000000000..722a081f2e --- /dev/null +++ b/sdk/python/feast/infra/online_stores/dynamodb.py @@ -0,0 +1,182 @@ +# Copyright 2021 The Feast Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from datetime import datetime +from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union + +from pydantic import StrictStr +from pydantic.typing import Literal + +from feast import Entity, FeatureTable, FeatureView, utils +from feast.infra.online_stores.helpers import compute_entity_id +from feast.infra.online_stores.online_store import OnlineStore +from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto +from feast.protos.feast.types.Value_pb2 import Value as ValueProto +from feast.repo_config import FeastConfigBaseModel, RepoConfig + +try: + import boto3 + from botocore.exceptions import ClientError +except ImportError as e: + from feast.errors import FeastExtrasDependencyImportError + + raise FeastExtrasDependencyImportError("aws", str(e)) + + +class DynamoDBOnlineStoreConfig(FeastConfigBaseModel): + """Online store config for DynamoDB store""" + + type: Literal["dynamodb"] = "dynamodb" + """Online store type selector""" + + region: StrictStr + """ AWS Region Name """ + + +class DynamoDBOnlineStore(OnlineStore): + """ + Online feature store for AWS DynamoDB. + """ + + def update( + self, + config: RepoConfig, + tables_to_delete: Sequence[Union[FeatureTable, FeatureView]], + tables_to_keep: Sequence[Union[FeatureTable, FeatureView]], + entities_to_delete: Sequence[Entity], + entities_to_keep: Sequence[Entity], + partial: bool, + ): + online_config = config.online_store + assert isinstance(online_config, DynamoDBOnlineStoreConfig) + dynamodb_client, dynamodb_resource = self._initialize_dynamodb(online_config) + + for table_instance in tables_to_keep: + try: + dynamodb_resource.create_table( + TableName=f"{config.project}.{table_instance.name}", + KeySchema=[{"AttributeName": "entity_id", "KeyType": "HASH"}], + AttributeDefinitions=[ + {"AttributeName": "entity_id", "AttributeType": "S"} + ], + BillingMode="PAY_PER_REQUEST", + ) + except ClientError as ce: + # If the table creation fails with ResourceInUseException, + # it means the table already exists or is being created. + # Otherwise, re-raise the exception + if ce.response["Error"]["Code"] != "ResourceInUseException": + raise + + for table_instance in tables_to_keep: + dynamodb_client.get_waiter("table_exists").wait( + TableName=f"{config.project}.{table_instance.name}" + ) + + self._delete_tables_idempotent(dynamodb_resource, config, tables_to_delete) + + def teardown( + self, + config: RepoConfig, + tables: Sequence[Union[FeatureTable, FeatureView]], + entities: Sequence[Entity], + ): + online_config = config.online_store + assert isinstance(online_config, DynamoDBOnlineStoreConfig) + _, dynamodb_resource = self._initialize_dynamodb(online_config) + + self._delete_tables_idempotent(dynamodb_resource, config, tables) + + def online_write_batch( + self, + config: RepoConfig, + table: Union[FeatureTable, FeatureView], + data: List[ + Tuple[EntityKeyProto, Dict[str, ValueProto], datetime, Optional[datetime]] + ], + progress: Optional[Callable[[int], Any]], + ) -> None: + online_config = config.online_store + assert isinstance(online_config, DynamoDBOnlineStoreConfig) + _, dynamodb_resource = self._initialize_dynamodb(online_config) + + table_instance = dynamodb_resource.Table(f"{config.project}.{table.name}") + with table_instance.batch_writer() as batch: + for entity_key, features, timestamp, created_ts in data: + entity_id = compute_entity_id(entity_key) + batch.put_item( + Item={ + "entity_id": entity_id, # PartitionKey + "event_ts": str(utils.make_tzaware(timestamp)), + "values": { + k: v.SerializeToString() + for k, v in features.items() # Serialized Features + }, + } + ) + if progress: + progress(1) + + def online_read( + self, + config: RepoConfig, + table: Union[FeatureTable, FeatureView], + entity_keys: List[EntityKeyProto], + requested_features: Optional[List[str]] = None, + ) -> List[Tuple[Optional[datetime], Optional[Dict[str, ValueProto]]]]: + online_config = config.online_store + assert isinstance(online_config, DynamoDBOnlineStoreConfig) + _, dynamodb_resource = self._initialize_dynamodb(online_config) + + result: List[Tuple[Optional[datetime], Optional[Dict[str, ValueProto]]]] = [] + for entity_key in entity_keys: + table_instance = dynamodb_resource.Table(f"{config.project}.{table.name}") + entity_id = compute_entity_id(entity_key) + response = table_instance.get_item(Key={"entity_id": entity_id}) + value = response.get("Item") + + if value is not None: + res = {} + for feature_name, value_bin in value["values"].items(): + val = ValueProto() + val.ParseFromString(value_bin.value) + res[feature_name] = val + result.append((value["event_ts"], res)) + else: + result.append((None, None)) + return result + + def _initialize_dynamodb(self, online_config: DynamoDBOnlineStoreConfig): + return ( + boto3.client("dynamodb", region_name=online_config.region), + boto3.resource("dynamodb", region_name=online_config.region), + ) + + def _delete_tables_idempotent( + self, + dynamodb_resource, + config: RepoConfig, + tables: Sequence[Union[FeatureTable, FeatureView]], + ): + for table_instance in tables: + try: + table = dynamodb_resource.Table( + f"{config.project}.{table_instance.name}" + ) + table.delete() + except ClientError as ce: + # If the table deletion fails with ResourceNotFoundException, + # it means the table has already been deleted. + # Otherwise, re-raise the exception + if ce.response["Error"]["Code"] != "ResourceNotFoundException": + raise diff --git a/sdk/python/feast/infra/online_stores/helpers.py b/sdk/python/feast/infra/online_stores/helpers.py index 9c42c5ea00..788be68b8d 100644 --- a/sdk/python/feast/infra/online_stores/helpers.py +++ b/sdk/python/feast/infra/online_stores/helpers.py @@ -5,6 +5,7 @@ import mmh3 from feast import errors +from feast.infra.key_encoding_utils import serialize_entity_key from feast.infra.online_stores.online_store import OnlineStore from feast.protos.feast.storage.Redis_pb2 import RedisKeyV2 as RedisKeyProto from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto @@ -53,3 +54,12 @@ def _mmh3(key: str): """ key_hash = mmh3.hash(key, signed=False) return bytes.fromhex(struct.pack(" str: + """ + Compute Entity id given Feast Entity Key for online stores. + Remember that Entity here refers to `EntityKeyProto` which is used in some online stores to encode the keys. + It has nothing to do with the Entity concept we have in Feast. + """ + return mmh3.hash_bytes(serialize_entity_key(entity_key)).hex() diff --git a/sdk/python/feast/infra/provider.py b/sdk/python/feast/infra/provider.py index 8b92374d23..29766c9d9a 100644 --- a/sdk/python/feast/infra/provider.py +++ b/sdk/python/feast/infra/provider.py @@ -145,6 +145,10 @@ def get_provider(config: RepoConfig, repo_path: Path) -> Provider: from feast.infra.gcp import GcpProvider return GcpProvider(config) + elif config.provider == "aws": + from feast.infra.aws import AwsProvider + + return AwsProvider(config) elif config.provider == "local": from feast.infra.local import LocalProvider diff --git a/sdk/python/feast/registry.py b/sdk/python/feast/registry.py index 9500194d04..53c3cae1e7 100644 --- a/sdk/python/feast/registry.py +++ b/sdk/python/feast/registry.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os import uuid from abc import ABC, abstractmethod from datetime import datetime, timedelta @@ -25,6 +26,8 @@ EntityNotFoundException, FeatureTableNotFoundException, FeatureViewNotFoundException, + S3RegistryBucketForbiddenAccess, + S3RegistryBucketNotExist, ) from feast.feature_table import FeatureTable from feast.feature_view import FeatureView @@ -56,6 +59,8 @@ def __init__(self, registry_path: str, repo_path: Path, cache_ttl: timedelta): uri = urlparse(registry_path) if uri.scheme == "gs": self._registry_store: RegistryStore = GCSRegistryStore(registry_path) + elif uri.scheme == "s3": + self._registry_store = S3RegistryStore(registry_path) elif uri.scheme == "file" or uri.scheme == "": self._registry_store = LocalRegistryStore( repo_path=repo_path, registry_path_string=registry_path @@ -537,3 +542,73 @@ def _write_registry(self, registry_proto: RegistryProto): file_obj.seek(0) blob.upload_from_file(file_obj) return + + +class S3RegistryStore(RegistryStore): + def __init__(self, uri: str): + try: + import boto3 + except ImportError as e: + from feast.errors import FeastExtrasDependencyImportError + + raise FeastExtrasDependencyImportError("aws", str(e)) + self._uri = urlparse(uri) + self._bucket = self._uri.hostname + self._key = self._uri.path.lstrip("/") + + self.s3_client = boto3.resource( + "s3", endpoint_url=os.environ.get("FEAST_S3_ENDPOINT_URL") + ) + + def get_registry_proto(self): + file_obj = TemporaryFile() + registry_proto = RegistryProto() + try: + from botocore.exceptions import ClientError + except ImportError as e: + from feast.errors import FeastExtrasDependencyImportError + + raise FeastExtrasDependencyImportError("aws", str(e)) + try: + bucket = self.s3_client.Bucket(self._bucket) + self.s3_client.meta.client.head_bucket(Bucket=bucket.name) + except ClientError as e: + # If a client error is thrown, then check that it was a 404 error. + # If it was a 404 error, then the bucket does not exist. + error_code = int(e.response["Error"]["Code"]) + if error_code == 404: + raise S3RegistryBucketNotExist(self._bucket) + else: + raise S3RegistryBucketForbiddenAccess(self._bucket) from e + + try: + obj = bucket.Object(self._key) + obj.download_fileobj(file_obj) + file_obj.seek(0) + registry_proto.ParseFromString(file_obj.read()) + return registry_proto + except ClientError as e: + raise FileNotFoundError( + f"Error while trying to locate Registry at path {self._uri.geturl()}" + ) from e + + def update_registry_proto( + self, updater: Optional[Callable[[RegistryProto], RegistryProto]] = None + ): + try: + registry_proto = self.get_registry_proto() + except FileNotFoundError: + registry_proto = RegistryProto() + registry_proto.registry_schema_version = REGISTRY_SCHEMA_VERSION + if updater: + registry_proto = updater(registry_proto) + self._write_registry(registry_proto) + + def _write_registry(self, registry_proto: RegistryProto): + registry_proto.version_id = str(uuid.uuid4()) + registry_proto.last_updated.FromDatetime(datetime.utcnow()) + # we have already checked the bucket exists so no need to do it again + file_obj = TemporaryFile() + file_obj.write(registry_proto.SerializeToString()) + file_obj.seek(0) + self.s3_client.Bucket(self._bucket).put_object(Body=file_obj, Key=self._key) diff --git a/sdk/python/feast/repo_config.py b/sdk/python/feast/repo_config.py index 8ef98736f9..5cf17bf729 100644 --- a/sdk/python/feast/repo_config.py +++ b/sdk/python/feast/repo_config.py @@ -16,6 +16,7 @@ "sqlite": "feast.infra.online_stores.sqlite.SqliteOnlineStore", "datastore": "feast.infra.online_stores.datastore.DatastoreOnlineStore", "redis": "feast.infra.online_stores.redis.RedisOnlineStore", + "dynamodb": "feast.infra.online_stores.dynamodb.DynamoDBOnlineStore", } OFFLINE_STORE_CLASS_FOR_TYPE = { @@ -67,7 +68,7 @@ class RepoConfig(FeastBaseModel): """ provider: StrictStr - """ str: local or gcp """ + """ str: local or gcp or aws """ online_store: Any """ OnlineStoreConfig: Online store configuration (optional depending on provider) """ @@ -127,6 +128,8 @@ def _validate_online_store_config(cls, values): values["online_store"]["type"] = "sqlite" elif values["provider"] == "gcp": values["online_store"]["type"] = "datastore" + elif values["provider"] == "aws": + values["online_store"]["type"] = "dynamodb" online_store_type = values["online_store"]["type"] @@ -161,7 +164,7 @@ def _validate_offline_store_config(cls, values): elif values["provider"] == "gcp": values["offline_store"]["type"] = "bigquery" elif values["provider"] == "aws": - values["offline_store"]["type"] = "redshift" + values["offline_store"]["type"] = "file" offline_store_type = values["offline_store"]["type"] diff --git a/sdk/python/feast/templates/aws/bootstrap.py b/sdk/python/feast/templates/aws/bootstrap.py new file mode 100644 index 0000000000..4013ca5a8d --- /dev/null +++ b/sdk/python/feast/templates/aws/bootstrap.py @@ -0,0 +1,35 @@ +def bootstrap(): + # Bootstrap() will automatically be called from the init_repo() during `feast init` + + import pathlib + from datetime import datetime, timedelta + + from feast.driver_test_data import create_driver_hourly_stats_df + + repo_path = pathlib.Path(__file__).parent.absolute() + data_path = repo_path / "data" + data_path.mkdir(exist_ok=True) + + end_date = datetime.now().replace(microsecond=0, second=0, minute=0) + start_date = end_date - timedelta(days=15) + + driver_entities = [1001, 1002, 1003, 1004, 1005] + driver_df = create_driver_hourly_stats_df(driver_entities, start_date, end_date) + + driver_stats_path = data_path / "driver_stats.parquet" + driver_df.to_parquet(path=str(driver_stats_path), allow_truncated_timestamps=True) + + example_py_file = repo_path / "example.py" + replace_str_in_file(example_py_file, "%PARQUET_PATH%", str(driver_stats_path)) + + +def replace_str_in_file(file_path, match_str, sub_str): + with open(file_path, "r") as f: + contents = f.read() + contents = contents.replace(match_str, sub_str) + with open(file_path, "wt") as f: + f.write(contents) + + +if __name__ == "__main__": + bootstrap() diff --git a/sdk/python/feast/templates/aws/example.py b/sdk/python/feast/templates/aws/example.py new file mode 100644 index 0000000000..a66dbba120 --- /dev/null +++ b/sdk/python/feast/templates/aws/example.py @@ -0,0 +1,36 @@ +# This is an example feature definition file + +from google.protobuf.duration_pb2 import Duration + +from feast import Entity, Feature, FeatureView, ValueType +from feast.data_source import FileSource + +# Read data from parquet files. Parquet is convenient for local development mode. For +# production, you can use your favorite DWH, such as BigQuery. See Feast documentation +# for more info. +driver_hourly_stats = FileSource( + path="%PARQUET_PATH%", + event_timestamp_column="datetime", + created_timestamp_column="created", +) + +# Define an entity for the driver. You can think of entity as a primary key used to +# fetch features. +driver = Entity(name="driver_id", value_type=ValueType.INT64, description="driver id",) + +# Our parquet files contain sample data that includes a driver_id column, timestamps and +# three feature column. Here we define a Feature View that will allow us to serve this +# data to our model online. +driver_hourly_stats_view = FeatureView( + name="driver_hourly_stats", + entities=["driver_id"], + ttl=Duration(seconds=86400 * 1), + features=[ + Feature(name="conv_rate", dtype=ValueType.FLOAT), + Feature(name="acc_rate", dtype=ValueType.FLOAT), + Feature(name="avg_daily_trips", dtype=ValueType.INT64), + ], + online=True, + input=driver_hourly_stats, + tags={}, +) diff --git a/sdk/python/feast/templates/aws/feature_store.yaml b/sdk/python/feast/templates/aws/feature_store.yaml new file mode 100644 index 0000000000..7f7be8527e --- /dev/null +++ b/sdk/python/feast/templates/aws/feature_store.yaml @@ -0,0 +1,3 @@ +project: my_project +registry: data/registry.db +provider: aws diff --git a/sdk/python/feast/templates/aws/test.py b/sdk/python/feast/templates/aws/test.py new file mode 100644 index 0000000000..cc2cf7e984 --- /dev/null +++ b/sdk/python/feast/templates/aws/test.py @@ -0,0 +1,38 @@ +from datetime import datetime + +import pandas as pd +from example import driver, driver_hourly_stats_view + +from feast import FeatureStore + + +def main(): + pd.set_option("display.max_columns", None) + pd.set_option("display.width", 1000) + + # Load the feature store from the current path + fs = FeatureStore(repo_path=".") + + # Deploy the feature store to AWS + print("Deploying feature store to AWS...") + fs.apply([driver, driver_hourly_stats_view]) + + # Select features + feature_refs = ["driver_hourly_stats:conv_rate", "driver_hourly_stats:acc_rate"] + + print("Loading features into the online store...") + fs.materialize_incremental(end_date=datetime.now()) + + print("Retrieving online features...") + + # Retrieve features from the online store (DynamoDB) + online_features = fs.get_online_features( + feature_refs=feature_refs, + entity_rows=[{"driver_id": 1001}, {"driver_id": 1002}], + ).to_dict() + + print(pd.DataFrame.from_dict(online_features)) + + +if __name__ == "__main__": + main() diff --git a/sdk/python/setup.py b/sdk/python/setup.py index 293e6804e7..bd51956160 100644 --- a/sdk/python/setup.py +++ b/sdk/python/setup.py @@ -71,6 +71,10 @@ "redis-py-cluster==2.1.2", ] +AWS_REQUIRED = [ + "boto3==1.17.*", +] + CI_REQUIRED = [ "cryptography==3.3.2", "flake8", @@ -104,8 +108,10 @@ "google-cloud-storage>=1.20.*", "google-cloud-core==1.4.*", "redis-py-cluster==2.1.2", + "boto3==1.17.*", ] + # README file from Feast repo root directory repo_root = ( subprocess.Popen(["git", "rev-parse", "--show-toplevel"], stdout=subprocess.PIPE) @@ -198,6 +204,7 @@ def run(self): "dev": ["mypy-protobuf==1.*", "grpcio-testing==1.*"], "ci": CI_REQUIRED, "gcp": GCP_REQUIRED, + "aws": AWS_REQUIRED, "redis": REDIS_REQUIRED, }, include_package_data=True, diff --git a/sdk/python/tests/test_cli_aws.py b/sdk/python/tests/test_cli_aws.py new file mode 100644 index 0000000000..2792858e8d --- /dev/null +++ b/sdk/python/tests/test_cli_aws.py @@ -0,0 +1,58 @@ +import random +import string +import tempfile +from pathlib import Path +from textwrap import dedent + +import pytest + +from feast.feature_store import FeatureStore +from tests.cli_utils import CliRunner +from tests.online_read_write_test import basic_rw_test + + +@pytest.mark.integration +def test_basic() -> None: + project_id = "".join( + random.choice(string.ascii_lowercase + string.digits) for _ in range(10) + ) + runner = CliRunner() + with tempfile.TemporaryDirectory() as repo_dir_name, tempfile.TemporaryDirectory() as data_dir_name: + + repo_path = Path(repo_dir_name) + data_path = Path(data_dir_name) + + repo_config = repo_path / "feature_store.yaml" + + repo_config.write_text( + dedent( + f""" + project: {project_id} + registry: {data_path / "registry.db"} + provider: aws + online_store: + type: dynamodb + region: us-west-2 + """ + ) + ) + + repo_example = repo_path / "example.py" + repo_example.write_text( + (Path(__file__).parent / "example_feature_repo_1.py").read_text() + ) + + result = runner.run(["apply"], cwd=repo_path) + assert result.returncode == 0 + + # Doing another apply should be a no op, and should not cause errors + result = runner.run(["apply"], cwd=repo_path) + assert result.returncode == 0 + + basic_rw_test( + FeatureStore(repo_path=str(repo_path), config=None), + view_name="driver_locations", + ) + + result = runner.run(["teardown"], cwd=repo_path) + assert result.returncode == 0 diff --git a/sdk/python/tests/test_feature_store.py b/sdk/python/tests/test_feature_store.py index 49a3a9a63b..f169c1336a 100644 --- a/sdk/python/tests/test_feature_store.py +++ b/sdk/python/tests/test_feature_store.py @@ -24,6 +24,8 @@ from feast.feature import Feature from feast.feature_store import FeatureStore from feast.feature_view import FeatureView +from feast.infra.offline_stores.file import FileOfflineStoreConfig +from feast.infra.online_stores.dynamodb import DynamoDBOnlineStoreConfig from feast.infra.online_stores.sqlite import SqliteOnlineStoreConfig from feast.protos.feast.types import Value_pb2 as ValueProto from feast.repo_config import RepoConfig @@ -72,6 +74,19 @@ def feature_store_with_gcs_registry(): ) +@pytest.fixture +def feature_store_with_s3_registry(): + return FeatureStore( + config=RepoConfig( + registry=f"s3://feast-integration-tests/registries/{int(time.time() * 1000)}/registry.db", + project="default", + provider="aws", + online_store=DynamoDBOnlineStoreConfig(region="us-west-2"), + offline_store=FileOfflineStoreConfig(), + ) + ) + + @pytest.mark.parametrize( "test_feature_store", [lazy_fixture("feature_store_with_local_registry")], ) @@ -101,7 +116,11 @@ def test_apply_entity_success(test_feature_store): @pytest.mark.integration @pytest.mark.parametrize( - "test_feature_store", [lazy_fixture("feature_store_with_gcs_registry")], + "test_feature_store", + [ + lazy_fixture("feature_store_with_gcs_registry"), + lazy_fixture("feature_store_with_s3_registry"), + ], ) def test_apply_entity_integration(test_feature_store): entity = Entity( @@ -250,7 +269,11 @@ def test_feature_view_inference_success(test_feature_store, dataframe_source): @pytest.mark.integration @pytest.mark.parametrize( - "test_feature_store", [lazy_fixture("feature_store_with_gcs_registry")], + "test_feature_store", + [ + lazy_fixture("feature_store_with_gcs_registry"), + lazy_fixture("feature_store_with_s3_registry"), + ], ) def test_apply_feature_view_integration(test_feature_store): # Create Feature Views diff --git a/sdk/python/tests/test_offline_online_store_consistency.py b/sdk/python/tests/test_offline_online_store_consistency.py index 6f2fc41841..0fcc368b22 100644 --- a/sdk/python/tests/test_offline_online_store_consistency.py +++ b/sdk/python/tests/test_offline_online_store_consistency.py @@ -18,7 +18,9 @@ from feast.feature import Feature from feast.feature_store import FeatureStore from feast.feature_view import FeatureView +from feast.infra.offline_stores.file import FileOfflineStoreConfig from feast.infra.online_stores.datastore import DatastoreOnlineStoreConfig +from feast.infra.online_stores.dynamodb import DynamoDBOnlineStoreConfig from feast.infra.online_stores.redis import RedisOnlineStoreConfig, RedisType from feast.infra.online_stores.sqlite import SqliteOnlineStoreConfig from feast.repo_config import RepoConfig @@ -167,7 +169,7 @@ def prep_redis_fs_and_fv() -> Iterator[Tuple[FeatureStore, FeatureView]]: join_key="driver_id", value_type=ValueType.INT32, ) - with tempfile.TemporaryDirectory() as repo_dir_name, tempfile.TemporaryDirectory(): + with tempfile.TemporaryDirectory() as repo_dir_name: config = RepoConfig( registry=str(Path(repo_dir_name) / "registry.db"), project=f"test_bq_correctness_{str(uuid.uuid4()).replace('-', '')}", @@ -184,6 +186,41 @@ def prep_redis_fs_and_fv() -> Iterator[Tuple[FeatureStore, FeatureView]]: yield fs, fv +@contextlib.contextmanager +def prep_dynamodb_fs_and_fv() -> Iterator[Tuple[FeatureStore, FeatureView]]: + with tempfile.NamedTemporaryFile(suffix=".parquet") as f: + df = create_dataset() + f.close() + df.to_parquet(f.name) + file_source = FileSource( + file_format=ParquetFormat(), + file_url=f"file://{f.name}", + event_timestamp_column="ts", + created_timestamp_column="created_ts", + date_partition_column="", + field_mapping={"ts_1": "ts", "id": "driver_id"}, + ) + fv = get_feature_view(file_source) + e = Entity( + name="driver", + description="id for driver", + join_key="driver_id", + value_type=ValueType.INT32, + ) + with tempfile.TemporaryDirectory() as repo_dir_name: + config = RepoConfig( + registry=str(Path(repo_dir_name) / "registry.db"), + project=f"test_bq_correctness_{str(uuid.uuid4()).replace('-', '')}", + provider="aws", + online_store=DynamoDBOnlineStoreConfig(region="us-west-2"), + offline_store=FileOfflineStoreConfig(), + ) + fs = FeatureStore(config=config) + fs.apply([fv, e]) + + yield fs, fv + + # Checks that both offline & online store values are as expected def check_offline_and_online_features( fs: FeatureStore, @@ -264,6 +301,12 @@ def test_redis_offline_online_store_consistency(): run_offline_online_store_consistency_test(fs, fv) +@pytest.mark.integration +def test_dynamodb_offline_online_store_consistency(): + with prep_dynamodb_fs_and_fv() as (fs, fv): + run_offline_online_store_consistency_test(fs, fv) + + def test_local_offline_online_store_consistency(): with prep_local_fs_and_fv() as (fs, fv): run_offline_online_store_consistency_test(fs, fv)