From 6ebd956d7fd163f83f1ee422ec633d3f8e34f616 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Wed, 4 Dec 2024 13:30:16 +0100 Subject: [PATCH] [Data Liberation] "Fetch from a different URL" button for failed media downloads, Interactivity API support (#2040) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description Ships user-driven import error handling and makes the import UI more useful by automatically refreshing the progress details. ### User-driven error handling When a remote asset cannot be downloaded, most importers either stop or ignore the error. This PR adds a user interaction to make an explicit decision about what should happen next – do we ignore the missing asset? Do we use another file instead? https://github.com/user-attachments/assets/cea48258-b644-434c-9fb2-1b890c4d86d7 ### Auto-Refreshing Import Status This PR also re-expresses the entire data liberation wp-admin page using the interactivity API, and auto-refreshes the progress: https://github.com/user-attachments/assets/e093268b-5deb-4bc2-a1d2-e2bb1148e153 A part of https://github.com/WordPress/wordpress-playground/issues/1894 ## Technical overview ### User-driven error handling During the frontloading stage, the `WP_Stream_Importer` exposes all the frontloaded entities to the API consumer. The consumer then creates a post of type `frontloading_placeholder` with an initial status `awaiting_download` for each asset, and updates it with progress information and status (success, failure, skipped) as the import progresses. The frontloading stage is not finished until all the frontloaded assets have been processed with a non-error outcome. There's a few ways to recover from errors: * Retry the download – `WP_Stream_Importer` now retries the failed assets URL (via `WP_Retry_Frontloading_Iterator`) before moving on to entities provided by the usual entity source such as a WXR file. * Changing the downloaded URL – done by the user on the wp-admin page * Choosing to skip the download – done by the user on the wp-admin page Sometimes we don't want to require user interactions, e.g. when running the `importWxr` Blueprint step. In those scenarios, we could choose a default error outcome, e.g. "skip failed downloads". ### Auto-refreshing admin page Two `fetch()` requests running in an infinite loop are: * Updating the JavaScript interactivity store with the latest import state from the server * Running the next import step ### Other changes * Adds `php_userstreamop_read` to the Asyncify list – it crashed the importer in `@wp-playground/cli` running in bun. ## Follow-up work * Pretty UI transitions. Right now it's all sudden and jerky. We need progress bars, smooth animations, clear visual causality. * Prevent running the same import in two concurrent requests. This is a serial importer not designed for parallelization. * Run each import step in a transaction – either it all worked and we can commit the changes and an updated cursor, or it didn't work and we roll back the last step. Ideally we'll never see a scenario where an entity was processed, but a crash happened before storing the updated cursor and the next run reprocesses the same entity. ## Testing instructions * Go to the data liberation admin page * Upload a WXR export file * Confirm the import processes automatically and doesn't error out --- CHANGELOG.md | 14 +- packages/docs/site/docs/main/changelog.md | 14 +- packages/php-wasm/compile/php/Dockerfile | 1 + .../php-wasm/node/asyncify/8_3_0/php_8_3.wasm | Bin 15083501 -> 15085661 bytes packages/php-wasm/node/asyncify/php_8_3.js | 2 +- .../playground/data-liberation/bootstrap.php | 76 +- .../data-liberation/data-liberation-page.php | 331 +++++ .../data-liberation/import-screen.js | 147 +- .../playground/data-liberation/plugin.php | 1220 +++++++++-------- .../playground/data-liberation/project.json | 4 +- .../data-liberation/src/functions.php | 9 + .../src/import/WP_Attachment_Downloader.php | 98 +- .../import/WP_Attachment_Downloader_Event.php | 9 +- .../src/import/WP_Entity_Iterator_Chain.php | 65 + .../src/import/WP_Import_Session.php | 212 ++- .../src/import/WP_Markdown_Importer.php | 2 +- .../import/WP_Retry_Frontloading_Iterator.php | 98 ++ .../src/import/WP_Stream_Importer.php | 115 +- .../data-liberation/src/wxr/WP_WXR_Reader.php | 4 + ...ing-attachments-post-contents-and-404s.xml | 116 ++ 20 files changed, 1772 insertions(+), 765 deletions(-) create mode 100644 packages/playground/data-liberation/data-liberation-page.php create mode 100644 packages/playground/data-liberation/src/import/WP_Entity_Iterator_Chain.php create mode 100644 packages/playground/data-liberation/src/import/WP_Retry_Frontloading_Iterator.php create mode 100644 packages/playground/data-liberation/tests/wxr/frontloading-attachments-post-contents-and-404s.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index 33b2046913..34c3ff0a6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,30 +4,29 @@ All notable changes to this project are documented in this file by a CI job that runs on every NPM release. The file follows the [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format. -## [v1.0.14] (2024-12-02) +## [v1.0.14] (2024-12-02) ### Blueprints -- Resolve the latest WordPress version from the API instead of assuming it's the same as the last minified build. ([#2027](https://github.com/WordPress/wordpress-playground/pull/2027)) +- Resolve the latest WordPress version from the API instead of assuming it's the same as the last minified build. ([#2027](https://github.com/WordPress/wordpress-playground/pull/2027)) ### Tools - #### Blueprints Builder -- Add installPlugin support for single plugin files. ([#2033](https://github.com/WordPress/wordpress-playground/pull/2033)) +- Add installPlugin support for single plugin files. ([#2033](https://github.com/WordPress/wordpress-playground/pull/2033)) ### PHP WebAssembly -- Networking: Preserve the content-type header when fetch()-ing. ([#2028](https://github.com/WordPress/wordpress-playground/pull/2028)) +- Networking: Preserve the content-type header when fetch()-ing. ([#2028](https://github.com/WordPress/wordpress-playground/pull/2028)) ### Website -- [Web] Re-enable wp-cron. ([#2039](https://github.com/WordPress/wordpress-playground/pull/2039)) +- [Web] Re-enable wp-cron. ([#2039](https://github.com/WordPress/wordpress-playground/pull/2039)) ### Various -- [Data Liberation] WP_Stream_Importer: User-driven incremental import. ([#2013](https://github.com/WordPress/wordpress-playground/pull/2013)) +- [Data Liberation] WP_Stream_Importer: User-driven incremental import. ([#2013](https://github.com/WordPress/wordpress-playground/pull/2013)) ### Contributors @@ -35,7 +34,6 @@ The following contributors merged PRs in this release: @adamziel @brandonpayton - ## [v1.0.13] (2024-11-25) ### Enhancements diff --git a/packages/docs/site/docs/main/changelog.md b/packages/docs/site/docs/main/changelog.md index aef43b7500..53f538b8aa 100644 --- a/packages/docs/site/docs/main/changelog.md +++ b/packages/docs/site/docs/main/changelog.md @@ -9,30 +9,29 @@ All notable changes to this project are documented in this file by a CI job that runs on every NPM release. The file follows the [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format. -## [v1.0.14] (2024-12-02) +## [v1.0.14] (2024-12-02) ### Blueprints -- Resolve the latest WordPress version from the API instead of assuming it's the same as the last minified build. ([#2027](https://github.com/WordPress/wordpress-playground/pull/2027)) +- Resolve the latest WordPress version from the API instead of assuming it's the same as the last minified build. ([#2027](https://github.com/WordPress/wordpress-playground/pull/2027)) ### Tools - #### Blueprints Builder -- Add installPlugin support for single plugin files. ([#2033](https://github.com/WordPress/wordpress-playground/pull/2033)) +- Add installPlugin support for single plugin files. ([#2033](https://github.com/WordPress/wordpress-playground/pull/2033)) ### PHP WebAssembly -- Networking: Preserve the content-type header when fetch()-ing. ([#2028](https://github.com/WordPress/wordpress-playground/pull/2028)) +- Networking: Preserve the content-type header when fetch()-ing. ([#2028](https://github.com/WordPress/wordpress-playground/pull/2028)) ### Website -- [Web] Re-enable wp-cron. ([#2039](https://github.com/WordPress/wordpress-playground/pull/2039)) +- [Web] Re-enable wp-cron. ([#2039](https://github.com/WordPress/wordpress-playground/pull/2039)) ### Various -- [Data Liberation] WP_Stream_Importer: User-driven incremental import. ([#2013](https://github.com/WordPress/wordpress-playground/pull/2013)) +- [Data Liberation] WP_Stream_Importer: User-driven incremental import. ([#2013](https://github.com/WordPress/wordpress-playground/pull/2013)) ### Contributors @@ -40,7 +39,6 @@ The following contributors merged PRs in this release: @adamziel @brandonpayton - ## [v1.0.13] (2024-11-25) ### Enhancements diff --git a/packages/php-wasm/compile/php/Dockerfile b/packages/php-wasm/compile/php/Dockerfile index 1c4c9f94dc..0c5bef7215 100644 --- a/packages/php-wasm/compile/php/Dockerfile +++ b/packages/php-wasm/compile/php/Dockerfile @@ -569,6 +569,7 @@ export ASYNCIFY_ONLY=$'"rc_dtor_func",\ "_php_stream_fill_read_buffer",\ "_php_stream_read",\ "php_stream_read_to_str",\ +"php_userstreamop_read",\ "zif_fread",\ "wasm_read",\ "php_stdiop_read",\ diff --git a/packages/php-wasm/node/asyncify/8_3_0/php_8_3.wasm b/packages/php-wasm/node/asyncify/8_3_0/php_8_3.wasm index 8b8b0c94d691b08e11672ca887923df3c82af5db..5c35cb2d32f414b8d463e7fdc47fac33a9119b9e 100755 GIT binary patch delta 1861302 zcmd3P2YggT*Z0ocvKuy=Y)DTa>@ER9?@bVL>4+#OiWR|%6cs_m$C7|3h={=jh7vIp zB@{6TcmV?jh!7z_P(VtAP=qL;0Z|Ys-~ZgZEfk;UeZTiB@cU)vPCawx%$ak}oS8kd zbB6WDvKiJqy)!FYvAXlo-i{+*Uy#0k<$K2D4DGyk;gpwo-oX_$J{&A1o-aGEq;X46 z2Xk3mY>3;!)9Jr<(VH`^j(Qxq+HvppX&}cH#akiH z9ZV~yvgKaa%oOj1>BFjDHXC*+D5lYeOF~Jlf=X3*GevTTi)M-L4plo%sTRtoL?aey zH{#Tj(FA3fO&EnwqlrXs>5MS%-y+QqXWbcl`P5@xHZxHTK=XR{KtC3v9~bo#X?Iz) zYpgehV9`d8&?X(hxZI3qXf$MW>X9{IM3Y_~<*|(DiXj~L9-H}6sxf}Im1h7+8OD&Q zEJIm7$|D;E%e}A9`r0}U4Qk_#cstF$ldm4TB5QVa9)5oB@6@zg)~25EuKB1g&;7}J z{GZ`Qp2-aT`_K*ox9=Q$aLv*(-PXA7c??}AUFSfGYv z)M&c!GBwpWw`G;Ax#h|bf>!e8%}W(c`aLDDn4bG4W98o1#ZTJDZ97Y2adGXkciiGM zR_y(3@w05VH-5Flpl5egSG1P7&6~aK9i-O$T<4jmyrrM_0p1$s zZ3afq=e?7ZcknNn2I-NB64b5^$)#$1l(zJsck1$Pgz(bk+i0yU`r?pG4ZN#QL?GUL zJT+cisUgLBwbhCI;$G4CB8&6BxcW9$A`UC@DJ!|7tqhEmC$W zZtteAdeXE<Tn2f`v&TJ%U@7+!1tA%sB`N8}{$RMm4tj}MbSTU+w-GI;HX4J)Q zs!xA_D~$~y)L4-13MX{g4H_|#ZFkWG_<`^+WQWcLHE7X%6sHwZgVEgbB7sOB#^h+( z`@BsH;&{c|-Xw4#uPH-(8|Xr_@@mtU=;P{e^b3#&icdHt3XfK zNU&${^Z=z{=5JVxs8`^%Z%&LY^FhwK%NZ_aj9)ABw%yz~iE`{N^c7A}`pUUMc4gkN zo15Pmgv0K#g8DpGP44ahh9>Q+v?}d>k_JWlR;5-@2&!5>A<_AhvHKT#*%tTHK0*gJ zD?tmQN0%m)Zi$8*ir`=ll0!j~K=3Xpjmubuh11G>Lc+>v-U(Z(X9U*NEie;6F|f(O zsbo8C?f$+z;2da?5Ju#JGLn%ME&(<-lhT1l^c)TWzydrQWj#tVaN|-ig_2e;e28My zuxt5f2(uZAVH68C>%FTeE`tggLXOK}v__@T5#%e2ORh9(Odw|4$M8UIK7I!r_;?+N z!{WAT#}69h2H+=^o>R~Wh%wn?OvZU+`GhixF>SQ>SW!ZH7Rs5j*kwvKr82~{Du9NV zQLw;-n-%r8`*ueL<+zB|+qF!JQY&_Y@t8cuLMzN*IcAQJYx)>g==AznT*ykR#0^RI z@~tFxCq`*DR2z%PSl+Y+nrq0%ca82`;lL-@rgz!a``CJ~ysa)E zS%^M!eC;xWY_wvNYI^|yikc%gb~xH8>Pp}johSmq0I)%ksv(4n~`f(Th2FuKD&_bH4@%sAEiQ z1{y-IK|bbZU_(44=4-1>=v=%fD$$1_{oAi&Y-V9dhNWil-WWGUeTDr)Fwfai_c{>M!wsUkGAHSv%R)s11K1LY$pY6j}OASnSH!5EA?(W-pgKU zZnqds*$VehU1UK0^w0O>@4BB~;48 z1r(sJ=k6svbJn@fDd_e4eSl!@?~@{^0fJ9~Hi16coqxQO(IzoMLz^|VWs7Emq8%P) zhq6KXFjX;@6n(h3ONFFDHAriQdUg1G`+M?_8^zFUp&6s~T7+n)3{nybD3+O%iGMPE zQ*nsV;X||+MrivM?^&#jqy|fnY_j&8I}Qi7qM8$5gPNXt~seOygWY zF@s|nYIp{vO(2EbherC&Lm2}XQaUI`8U?0q&==^;q&Ol3e9R!Ci!sOo#eE!ToV|0~ezw!a#+TZ+-tn}a$G3_7Bpb7s9!S3a< z1g#%;{kQ@6Mq4rllxy5j;*A2I}x8Airz9E~u+&^TW!AJ2K ze6p{7Tg|kLaiVbIOqf&JZ4FJXeA*aRXb(ZL=)+h%*w6Y)$>J@>9tMP>jFQU5MaD{e zZ5gYDQ(NZJFxwKxIa}{zWMC>{iNwIk#1@IQvKvS-6z9{rO=fR!m>~4=DyvW;$Nd`E zcd!`u`*f?1;{_{vCN9Bv!L*Kxn8s*ffD^na*Ys~YQdLscD+e}2natJr|)S*Lv)7g)&*d=P_!7-z(_EMEeI zeXw-eO*H;8CW`oJw!g5JW5(`wU^hk-Wlg0!D@*jl*4;Z7i5mT(`bL&L(zM-8MIy0J z1|FeJGe00~;(ws1w$H2>Wry&HuEYrsYZJM^O!OGM0sSHCb}>O-3uO)np0x1V`rU21c_=Bg62Wvt1z=@RjQZi82%e zkp4|L#Q+a$XZ7JhUy{nK9F{|UWo@>O84^T_$Vy}3mBk3LD2#|zxym+)Oo5NO%%XL{gUvN!ysK@SSg<@Jg_Edy#UvR0!A}WNfK5JRa zU@Zo#r(8pwp{exq3Zv+0@pygqwz)cN#QRT+bM;v-ABzy38!+M#;%WotXA*k#hHR6> zbLWYpP1s7dTo+AQbu#FQFPpJD**RTm&d#W;Se$Lk@>#LI>^3$V+2wk#_UvVb^o_T( z`y3^I7g-DnOBe=lQ#-K1*CoyDz#fM+P+aN2>PG~uzy$tY@!pdxQQX~;RlkWG%!$8L zGCo*Bgmw^BCF29XmGJ?BQb0X|4!{FC24rDw=(tosoa)FD9|-CM9?;)Hj})Q;d59ys zU{lv_w3DdFQ7eHeRnnHkBk3%`?o*3AHsC2E9p+5Gl3*(LAyRn&Mqf@PJxG+Gu?&A* z-zYdhgWfyCH&l|f$YOw%9n|5jH`ln7p{A6AE#If~&^~6UJf^}L(g}9I5CC1(IcYJK zdW2!$Wu(IRJx*hHMrIm9FoYHef!ern;;_crvI3FViT%a1PKn%im`Akg%}sUD!7D;%+D&7sa%$>`s6DWLH*$ zk2@o5-Pm0BI8(_0Y+!v_mieWzQ5t)ZmPOVXeQP%+aZ+h^_F((jbbV1zaCbnq`3`oC z73rIMvG1(-oOv&6$BM+Bds!w@@9)RfN=MS?#B zPEe#j_z+XANfjTKE8RU15DmUc)Hr^d(jDBSP=9q0`vDu@O;*)7V^zJ!3iSA=*kWsQ ztT?;Jt{q#S3}ZmStX&qTAWM1znwfe!MME~728=*b43f6|jwEq@80&@gojx4fN7fec z)^OH|eI+&zXDtx?J)9k5h5F&=*k;aiw&)8-utpp(?;Qz{fVO2Xvt<~`qpz@UTtU^I zm|%f4Bo#Dczs#6K2B-<;;_TSOSkZqp*dRV;j%EY-)D!yOqghL(WpTN&nRjzoBy4LZ z%$*&Es5Y7}2d7t-J7Fk?9d5T-7L@nYn;@haCq(v}EHSCvSIg>B?*JB2d6Zir&RH&Y zzRCIk_rjgQ-bkYc{jK^eEf)VbiNSXJAV9307n?KKCSEi}%zd^+v_2=39cAo*7&Dfw zVmn3aw^&cMOdsdg1C17{d&#$zrV$oXx`Q z73eLduxGJp>2p40EtJ}!=Tj;f_e`rprDS09qF`x}p@}eMh{iKm1GM|d3|1Up>^uHq zbfN$hTeWGqHCu7vJ|7q7KvaV+WBgw)9-qnPSLzpwI^1J&`Oq$Kcq8i1V#CnIZ=)k( zo%jqMv4IJRb+g#s|3X74b!g&k5HF!jhBPHr`hposzdD;mVuL>+BIm$bkyEZWo5OIN zTrAWaHk20Z$Q(8=HOr(mcM{Hpp%q}4-yM+z>drF{G`MT-KRqm5Vp$vhE&# zjp}9%=ZB6;5-YJ^V(g#(Hh z93m-VZYwEP?3vFVBv_j)U`0v(o@;^|CJX_P;8_H&R;*Y3gl%KAQU-n!7N1n+TPYAO zptiXn;kPkY3eG`339BX=EM#>EGxsfI9;)=}LM$JI+ZM9^5k7!j{BDOEfHlcw8|wOd zu8ln?VTea1Nz{ib9z<0tiYF~%&1i&oEn;!O5!OV`3yau&l(S_KyDNai`q*~RmVv~O zmZ;;_i`i2=@06Icm?g4ev3fD96>P8u#{BbQ_8gB50XN0PK!mU&7mHz^g_cf1%158E zrNP2b;9aaS<3`~D%h=k$uw~Nii$(3v*&o5;cRP}UZ8ynd?*t*Cwm0UnM+kqeXM0@0rF9%k*r))L^MqmTW5qtif&)9>crRj z=0oE`&5Jrq6Tf@`QwtF3Sz$J};RuTrFRoxcsD%F`K?Y+i7H3wl$yD}@mF!=vSX^Am zo(qiBu-_ExqgFAQMF+?fC!sj_c5_sI?@Lyb+RXcsB?U(Uaua*MWF08yY*ej8Xn;+0 zmVltAZgOEjTUTse%c_ebI*awe-8R@l0$R9zHA|tKM_02KD(CE9&FUrjcL+w%wD4eM z!W_^7yG)s=z6PC@iB@aa9Yn)-&kl>XkyQlp(g`tl4Xa zRncsXUol;ix18#XtrAB#43GZ?e82v&aD{0viEv6#4yrP3(!*0BUI zK(Tcls~tp69A@G2I#$DOR5A1sko|gN%Jp9B*+;CUe-=2gkYUCw&^gR7j5xR%D}+hp z2gtX2fq}{CIjKovXg-S)vkJfyvdhJe0`_q8N^Q3ASb{U_7YbaK>zURg8_c=R-GJqQ z0d3j9aLQXQ&TU}B>3h&dR@dXhmUBMTKNE4zeA&Q<%TA&q$}io>l7r-bw<889*t0Q| zNyj3k=O#AF6PRt;Ejd3;cF8zU3t>>vD0g@h8^DT1heBw*f#pL@W)`yNYxo*y2zkay zHka#bM|{Kj1R0FMkBE6e)es}UVVN|nKTxDjDbPaNIUfPObRF{4Ao9ep&Bn6u^LLwQ z^dnQn=bM>}R>`i-EGaN@AB+2IGi#Q9)8-A;zXhFB`B$B9ZXhPp+`ur<28sYUOqm$E z#he>`ODI>QCEl}$)nI$YvqfxW5I;n9b-%^5fho=S7E%&Odgr(7b{cQQR&aczzqFOT zOW#ow!{V{hA=T4@Z@LZgHHPx)Hj@azWUGml+gO8O;}K~5^fs0g7%BF!aHP#DW>bTu zB9U_2c5`VA+|EL`OO-|kj?>0GUBY&`-5lMI+f7#3bf+y=#P4A3s`;nF;84D`f#XJF zZHVV~7z}azju4*0cd|Z}ff%q8DtNKDw3Ah@oKdp`^bG{uRl<4(79k`9T7<@NHcq`m z7oo#trA3JRQSNY)ofVguUAqr~7kA&qMpAqEyV%`)#!2D$4&W7t+TXF-)eFl0f;8cL zi@|U-&RjPhW*)$eD;Ljx$2L&S%w-{Bh2)ZNz|(G9HSy~A&>b*Ci@rC-3S%!?vbJfM zIQ2cqZ~{8s&8ksz^>>HtfRYJ4h-#FH_Hr>`H)}`x-t65hkIMJn6H?ypvIR!#lm9dK znB~{)VG{x~V^Hxh4+j+oxRw~S*O-}q?KLatds!#y;P1WcN^lN{X5@YBCCYhi9~(yD z4aC?$JU1~cg~t2zesjE_$f-ont^@40U}LlmKYf7x989qx<>7;D3^jY`U={z(*OswlKbIrz877++fZ1g= zY>A{xqpR~}CTbFnu!Yn$Na9fnT}RE~&p*oQ(|5^H_A!Mcj)kBDo|@OwS?>C%YGVB{ zlg@rUW;Tw-V#UY%Y*x`RFUl!89A};&2QYW!=Z>={1Kd`=8%}dr<>7EPfUtfXMhX+O zSUv40wg{evC&c$B*t3+nd~R4*@zzOnh>sN~*|WiEByN*d&K?L5oR1ZRRU)|Ta$^ow zm78;LaYk4j(dcK^g4%oXXLguE;zF3rM}ILUa|*4c4}W1z2#zbuAg`DG!WvS!$Wx|} z_uwfukG`**V(lqxcN(fT!jDd~1{7{P%@Tuf(jY5Nv;F~2;)Bx`3a5|3JIu8Ot}q_lfjJrJW^aZ!xQSt12{pmm07S6}*)!_%( znMIVBm?jD1^tfDP{>2_*<@&+Du!WHg_3|Zl7dt0<{mrHYWK)A7w;-=u|1K$(mvh>x z27VTnDt;~JwP}ctU1p7GQnD@^YjV)fC{ivCy_T3N=KRbX_{94p(fA5`fXb!+!h^OB z)x`sXsxk0#k$=UUj}7@v>xy50;jOIlZI)zOYzDtgKgGku@T;j2BIhbw&R4uG?!K!< zyyzph2p2D3gJ6La_uVbrs}lJ(&UF1J&U?r=G1&e;aa_<>Soy;$&z+<@BX~S8KS|V$ zHgz{g+@=tkk zdFC%e9j)+l$9Q8A78h{B1> z02sc~_~mG=%-5a;BuUE0!|#Gq z=OnSbCT~QeJW`X_r!XQFvNpmdsk{?~qkP}fQ~7ipnKi7%9|nl;*5bEWVSg-az{ju( z(XAn`Ri$G7eq{(PvtrTvN#0B>Y{*+WE9RNT7TIEWSzH#*xm8m1DvkIBiNQSCj1NWi z=bG`mScSf>8UGxnF4IX`%f46>7RCzwwHEv-&T{l4t#|{QW9b-UNy+AtO6tfLCIyW! zE<+8ghP4U|!}UuX?8skd>%{}<9L`Gm*mRyQ!FIZ-GjE4ulIdOeF!#Rg#;Sw05;_dm zHqWU6G&+Yk--I`sb0p4L#rs`=kjzv1=Uw=CyWy9dr1$N^pJM>Lpf9fvz)$z(Fq-WX zwfpf7DZ9-UJ*snOirZ?p*W~00hhuZM3WpVaY(L&Z!NT75Adi)Sf+LUe7EvWDw<<## zGmEwxw&@{S?l@6<0B?`A@x%aLhw;2}G3ha04QC}{@nigbEZ#N)c`F3Z59BF)(FyUv zK;9gmn+Edd>ne|K1op{^~%LHV8~R{7Mv0f58_Kv%l0^bh~x9g$9Y%k zV#(vYS7g3V5Hq+@e!0+gJL5RQ>Q8VjZjnzTfOifK1sF{CIM^!}qn_ZO0x8K)@>&jq z7(i(TJ?Nl^YBCTsWH28Ko8g(kTvc#Db$&PpM-=Uz|q z<%r2I@oEWyLhv|@4^IddeB@=`CZu5f(1LN{@l~?%ACZXpypi5cj{rAC1~_LvrCOyl0g?f4EGW z&&MKuG)Qfq=t#k7@yuwR8nyV~^=$#HSW)Ly?yMGoL&HS?mtpgTdloE_uzZRiNAni! z2O5M~IzCW3P?F9&io{*7@{a5v4rF;I`%^^zizn2-ZqRV4#V}cL+*n95xdB!aV`omEHipYAk^rIp2r zwswl5xA0-Zv{=lY$m2!7aZovz{~9d1O5}~>d5+_!gGEp4!{6qUxa0h6 zPh*Rvol*Ty@zCeIn(mpvJ21y@=YxeWh~e+@2ONK#3^w_f`0ic)h~wCqVA4s^ZX$p1 zMu48V4wl&W_z1@@$AZnD6W;fDe@Ah7u&omP!h8H9$+2vy%QrfveW}m=fOle!+;jeN zOk1K?Wbu8>@#_*-05RwEJs)z7haf8q$f`MwKNHd`)6NMojTbn6I^l1PYd?!8r}KYZ zmvm}6_P4`gxZod!V9<)g@c0?LV@P2u>OV1q*Rp2)2@W29w_xw{4r2QZ{(__Aw-g_i zt=f0uj+wl=vgCsrZdv4gjO8(ACXa1#@(X;2ceiWjCsCxET^n0Sk*;>_!fJ|ifxqb0 z6ht~hip`>wPG%3bS^VSF-P9*mfZI+5PuR3+$J~Z-S#xwtM%fN_ZL_{@7MEGX+oyvg ze)qKg<7~bb2)y75&_RV*@DU#p0yNjY)05}$2U+BCe>ZCQ?RM>`cf1lD3vr+l|#tO-y^tqIdm@WKHyEZxF0#%q!8GPCxJo z#K_pMmIi5fy)Paumgn-P#7hf#kH||iJOLWNI>RFl&E|>Xr-l4p$C|(@;o1hhYc7vr z5t%cCrQez|1djK~YD9ImZACouB~;?1vloH+CjZzVoh%zHTg+>sh!Cs)Ps#h`-^ zL*SYw9$v!NIX=7^9P3QI)>7V=Rpuo6Tc7c**QbHIRAG*i5Ip^$kI3UMu;>66(r_38 z-uCBg4DU;@va5@^O^^P9=gW?bSKNU$R47Wn#ByFX!Oy&`TAt{m1G29}khDo8t>*7K zHqY?*hGf-th`iPOdB>~}gw4|%tbqs>Lhe|jdjGF@8|J8lFp-_lhsA$-%?}3Ga<92z zXSmH$H9SR(`H**yoD_nptfelRqIeU-&&^M>i{Iv_)znMY@d?baAOwP?0uUtXn2BaA z1MOpdB-*u|#?nc!YugN}#ci&2pg3&*KU{U&L5?dyoZ2cPQ~aqCo`h>8=ZQVYb~