From ce85a68c87123856e11e6701a6581a53a19c8b2e Mon Sep 17 00:00:00 2001 From: John Wilkie Date: Mon, 2 Sep 2024 16:17:50 +0100 Subject: [PATCH] Test improvements --- darwin/cli_functions.py | 4 +- darwin/dataset/remote_dataset_v2.py | 2 +- darwin/dataset/upload_manager.py | 15 +- tests/darwin/data/push_test_dir.zip | Bin 14762 -> 151592 bytes tests/darwin/dataset/remote_dataset_test.py | 339 +++++++++++++------- 5 files changed, 239 insertions(+), 121 deletions(-) diff --git a/darwin/cli_functions.py b/darwin/cli_functions.py index 22fd99df7..682e502ca 100644 --- a/darwin/cli_functions.py +++ b/darwin/cli_functions.py @@ -869,8 +869,8 @@ def file_upload_callback( _error(f"No dataset with name '{e.name}'") except UnsupportedFileType as e: _error(f"Unsupported file type {e.path.suffix} ({e.path.name})") - except ValueError: - _error("No files found") + except ValueError as e: + _error(f"{e}") def dataset_import( diff --git a/darwin/dataset/remote_dataset_v2.py b/darwin/dataset/remote_dataset_v2.py index 9296dbcbe..4741c97d9 100644 --- a/darwin/dataset/remote_dataset_v2.py +++ b/darwin/dataset/remote_dataset_v2.py @@ -237,7 +237,7 @@ def push( ) if preserve_folders: raise TypeError( - "`item_merge_mode` does not support preserving local file structures with `preserve_folders` or `--folders`" + "`item_merge_mode` does not support preserving local file structures with `preserve_folders`" ) # Direct file paths diff --git a/darwin/dataset/upload_manager.py b/darwin/dataset/upload_manager.py index d0ee0c799..c49ad3a40 100644 --- a/darwin/dataset/upload_manager.py +++ b/darwin/dataset/upload_manager.py @@ -23,6 +23,7 @@ from darwin.doc_enum import DocEnum from darwin.path_utils import construct_full_path from darwin.utils import chunk +from darwin.utils.utils import is_image_extension_allowed_by_filename if TYPE_CHECKING: from darwin.client import Client @@ -254,6 +255,16 @@ def _create_layout(self): "type": "grid", } elif self.merge_mode == ItemMergeMode.CHANNELS: + # Currently, only image files are supported in multi-channel items. This is planned to change in the future + self.files = [ + file + for file in self.files + if is_image_extension_allowed_by_filename(str(file.local_path)) + ] + if not self.files: + raise ValueError( + "No supported filetypes found in 1st level of directory. Currently, multi-channel items only support images." + ) if len(self.files) > 16: raise ValueError( f"No multi-channel item can have more than 16 files. The following directory has {len(self.files)} files: {self.directory}" @@ -578,8 +589,8 @@ def upload_function( for item in self.pending_items: for slot in item.slots: upload_id = slot["upload_id"] - slot_path = item.path + (slot["file_name"]) - file = file_lookup.get(slot_path) + slot_path = Path(item.path) / Path((slot["file_name"])) + file = file_lookup.get(str(slot_path)) if not file: raise ValueError( f"Cannot match {slot_path} from payload with files to upload" diff --git a/tests/darwin/data/push_test_dir.zip b/tests/darwin/data/push_test_dir.zip index 6d76d817411da1608ab0374474f13321e222aaed..073acd0618a1ff73686df044d256eae6b4d3273e 100644 GIT binary patch literal 151592 zcmeFZWprFgx-2SYi&+*kGqc6aj21J4#THu3%*urr-4Gp7UnTntOh| zx7YfrR8?BBqcS7D9hoYXtOPI!GQh8+;TwV4Z$JF~1{MGhz{b(uM90C<-a*H})Q(n3 z@e=?zSKqAB@8$Rn8UPUF77zg7Hw@X|upj^+00P|pfd#?_0D$%vEE+)t9R&w#JHy{V z-G^t5ey=})o@$s|d{#l>S$|-zt{d#RG;d*B>k*4-N?AI7NU90S2ogX_T?*GRJw7{= z+``|+b~d(TWzLuL2V(M&m34tC3Y1TVAmGcUhA1MBl_gRXgv>*M0Ne%lk$*$3<;~n1 zTd7=JJ(-hMm_-?h>N6f|-9H&=y-sV~Z^lGIXfm3Cmjgx!>Z#T2c}pdMTHp{9yl;@S zTL5&j*xF}^;)k3vO*W-g;7f+Ea;M>BnLr-R?U9+FcN!Z zG8UGPw`^gn;a7gdVY6drILTDHe7!vkp0_+_GL!=E&9=B7jV*xHRpwCI7vJ6~2uc^N zcz&=OO4vV4O??)Z&tl+W;9$WYW{hXa#Mt!XDY^ASN@m8ds9uQu9GG0$ZEG1XwJkcs zWR$O0YV5&eA!wXfJ!gvU`=pi3oIG(RxnR5}1m zAt^ESm|b-=I z*aJoBEuD;=yOufFZszPT-29}1B3Sym?lhRmpwfHb>pXpfKy-jK-NQh9kH(j)VZl~$m zLKyWV-L_?3;hid;W0t%fOKBoKt|vdWm1Gia=MG0i!lr10_3vbyii`8LDas3}+Fd%r zBX1W<#}ETnk;bteLFJImIgi-X(}K!QO*WKGG2>5tzGUH@W_d74%Yn792b@120zN@; z3$JI99%4LUaLcskq8+H*a6U1<#%ULLO<~=T0N_$&W9KOigkC-9#D~#^ z5TOf&G+_%DI|$sSaYu%JK^L5B_xenHmiU8=q#J5g>piEgq*+JQlAlqMGScV6vaB+0hC- zWfVp1o!UQIHpLRvsWY~3q@^Tl07(iRPCl#RPZY0g2g`a~oJ=eS#=4ZNg(yMCo{-{c z8bLJJbn^i35T%@Q_Z4RV*9CXQ+) zdx^W{6GyKtgGwqUc)7N)Bq$;)>gL8&JB$mP5-3MPLFQDpC`XkkNf9&#X%f?XZlgye zEWB9Cn6E%;ozn{KwG!HpJd977-t60y^NpFq| zV0lP6EsmK2Tbte1F6TWX{@4g3Zo?8XO)|WwgY`<#3M+^KFQ}Mr*D+^|BUokTt<(<=p+lf8a5ts56jw9AFHtw~aJ@=N7 z&6^Y5GR@iK*i`vsx#w2m0TZWJ{{bw$W4fWE@Hpa<%hNaEg5fP-{t^`x6p^Tt6dAJj z3*Q3K4CHMILHVM>z9mFS#m^?^j0$Thh5c57@-{PR*=phhOu*juW*fnw%1}gMFCl_5zgy+M56Yl@N4cR)P+jCQuCwX{?FBG?T8dg%D}u zoLxMg(C!8dV76cldhcZfqeNypfAl?@=*_t`8}=}nErkZOjNa}CuD0*A91x!FN`B1H z1Th$6gj&1v)HZj~vl^{H=P;$DA;y^RYP;#T>J^KYW67&)PI2aUX6;}zVTGJa-$Mk{ zH8tIn)3^j1+~A@vr-ekg-p((*WVkMSkYqFmw~IU1fDcCBUfVzPc*RRKaEftqn0?|O z|5~Q?9lO4n2Q40`+8u)I!VBF@qH*APSAZ|#-R)(4`#tC#Kvn`Af`zTXo(tk5p?t)U ze;!c2eFPqYkAR}1BgHQuqo7LrUsDf_&Ogs8Q6HJ*PgzA@7e(J#S5NmsN>cd(PB~Ea zJ5VG#FeyA1U?mp3?*j_o2!O&SUdMM}wg5j6{v;GSt%F~rz#*UmJJ^^%X#Aq`H zqB3izmLRxFfix6}%esF0ehq&r#T^&?+Kz`+IxcI=J-u8~ z{e$$zd%4Gi``dYrBSZN8_leh2wpSxTI88iuC#Z(IQQK}Kj~i^Dd#QD`*1F1o z>}~z2ihjtL)5y4+xB2^a3sOC{XGD|DYl*D5OVBgcDcJ?v$Kkas=ObM;% z9o>?}f<pwHol^+k>#%N6|WZ zsjNjmB1F+_p>^u@0Tra$My zL)WH9GjKsvAG$9x+~DEo-haORxLtn%`qgU!wYUo+fj%1Iht&Vk5dUa`UtNgd!vz0L zL;Q;y{?s6U`QZ2Zlj;61H^`VjH50I`|wMf#Bov?7T577S&@(lk0{J$pupP>I6*MEX=jUZRu9 zP1SduHt8jQ#NF~o(g-d9K>qCs6dxo2o1pR>9!j`r@Ml*65|VAer40>_cKyaK@pd$R zco2U0cMQ~^an*ZBpNB5LYv17VERiUT`27ig)leVhN#+|ehHh7O2f)!uD0QLviJSp8 zQ+h6UI(LR{Bq)Umil+FYL6FNli1D*wU0>i&@IlwlIkUFdykrUeR0*_H)D;&fZGGdn z@dwA=7&q>tC+Yqwq+c55h^*DyQ;pVTSt9(=62s?D+CbTy6qFx5j=t3$m}l=VzNwXd zM{}8!75C-8&1@+jaPzN#ynzZ~9x+g{!(x!X5s3V;2YCT;#^Z*^0?4|NWeoph_w z0s}+W8Iys2#qk}LDU0$tp(=p#I%Mi=($eSi2}y5q>~wM-9=!AnZRN^p#-UJo_ID(Z%8s93v?|Q$+$iD8Yl|h(VgM4`5p*kgVN416 z;4>oHf2B3VQC-0uz(1$8KZ@#CTBG?8)qj)L{)@W)l+=FL)~~eoCw=|jPHKN}!3SRe z`u|rK{KnZ{=c|>0jK26}4?&3q{d*_Se&p!Nu z6d%(-j6gwz$yz%Ht(pVPEK2U(H-%3<;1l_C?i;d}8&Jf?S4l~WLsAxbWEI5J1WE0| z_vx{V=_FVH_%h=-00V$bcr|rjL9#k>`Gc^`s7r2(i%clu>T~Dlb7FW1)WD0?ybS?3 z$)8*26^AyAf!rzB{ddj1Ta;pXd2zEaqZ6N78Pr>>9n4qLD`s0lF6PPi!`U0llZ>5m zbmCYe8kWlzt-7(Cf0hYH+PPRYqn?|$?5n>CQSntXX~bRocD`BVbqwte{ppn)jG$p=v)%>|_&@pD>LN<*@EYD=$iAnRk5A zKbY3YzbX{q4}d(3Z1q}!PZbLB+p%i$}n7y{8Epb>u4>Mfb3Gw zBY{<At=j9s&efw1J1NCKyME1YIU8PZbwqbb`VaIZ0O=iaLv2U6}FL zOMOBi8=JlxF_7u9fuTkww<-##5O1FjMeQ2iamRVyp+S6z14Ns$%;}y75YK9g*b?Bwd!Xd`Z8Jw%`UmvDFUx)?;~xvGKWgTeWy$}0%lHP1zweL^ss=sbL{hgw{qu$3}j?Ui$tl!6__V@aezW-s(!T%xl-*ZUF2lnsv zPqBab@{cPv-;X8kUpKY>&YPab%*Ob)zdZh0r#XJqubrqr;XZq9zXO8-(z431f(Z+Q zfeGUoJb0yaCVBgYg{6hX`KG0%#l-lAi1;P^NJ$Zq7fsX=R2J8kRu&f)<`(Fa!YxBhxj|Gx}<%zrV4w1xB$z(b3T}Ff!89H8M2R1r%Eb zI>h)Bq6&%}lcNd}oTQ2pB^aGqT5}-lq~1S)C-` zXb&}Bd5osq>I^4cyAwa_>n1{!+#q?8=^;jvS}$>);Vw#?(kOMAT+txAsEMU~Q=R5hXPK8Ylc+9W`UMrrQg@fB!BD=|iBm-ND(JRrH4gVH%>(Pls2&^L>}|b$j0P{ngi0_Wh9&cE)RN z`|{%_{X!JMom|*;DcZJOHc?@p0s|hzKPxBU1x&7=U%#Sk+$15zDVO z%JlYy)RG|i)`kE`@O<@^{@t0V2jM|4S+56h-ZDZmI>AcJJ&ib&IGeN)Yx7h*HO$SOTU$J6UfPxyDG^?^e_!SA*}m!)$xu--=9R z<#bbhdb2yQa=-=Vc&O0*u6yTo&Gv?XxBrf~p<{;!00L%L_0GM*twO3&7F4+0q~K}v z+?O68Jc-{c43-vm2nW*e+U^&N2iVZEt(s&g8EWW_dNcX0p_)my?|!mP7RePucefnW zkXL?7e@o=&6QIA`!Aarg$t<;{go6?u4UWY8HOK42I;6aUps42lIKE)3J?9_zZwlAb zf+_f{k9_WkRHPg-r8fAD(=%_V6nJby?<5_(C2z=$N4NJo6fM9=!Ick>Se~?6{oTDD zLmi&t6LF`q$D=Qv;@6=wXdoe5al^NiwL(M4q<3sh5+OLZ7$@PS^tB*fY#o$OAXs?@ z(2ANfn-7H&yHR|j(uYlZ7kixJaK1&H2lWe*rL>W#T*GYhfe3rn+?pL)TYxZ}Dx*H& z?ADNl#)oqwR+(Qb(sJEQr*@0dO&jWfusG>&K3(2~*vm1via}_+SgUIWeTj=1t&EFd z7J8|L@3&2L!yl!>A`rkq7d~FSWcBWjEKB`jjdWLsh4IOcul;K~q#=&aTqf=hktP11 zek;WkatigJJ_1rfF!r9|sSCv&MF)Cr6LYU&dkGh3HwoUm_^l^7B$mD~s*|#BT`BQI zJc2j)!D-(@BENbtbO!EOpcSgzI4&Ei9#(m@s$8=Ieqm)8t;Ege24&aF?*yW);JvfN zz)7GHnVqt-y{ZAF%~PX-H$(ZwD!d?xY)G_*rKPEMP$GyHZVusR5gV|%Gkt8emlby zN!#9B!qK|1FoA7ozjsH!-9W7BrrlP+=00z)@Vc?*I%U_+)0t;*y$Pq1pa6zpZ;z7H z&gqT+n+(R=JH&{ zoIH?-QhJc!8o5;(tFI&O^HP-Mgh@XP(%78elw{W%9;#4i-&8JH$1-9#S5~_nZ)9D? zZ|WGf6GzSOtr>LqX zf8s7cF!YoJ%g1%aK4w)kpU^4hpk z*(E!$enorqyAmv+(;Eeqs^`VG%wD!;FW7KDhZm)v6 zC`OJ=DBRJ`WtWyC&egIgI*2SGi{>`hil6h`Xn)CC?VHDcXpq3uzdYGp5Ug9Q*H}JU zT}54_U)InN+5h2aq{p;ma#b!@0fIIln9;-w(d;4|8GZVFTOfN06M~yL?Ia~FpFOLd zl!Jmgk$r77BRpwyU5Mi=jk|80Amgh&dG2IM)|^azyFv9HUU!s*z3ug_-5_8?tV~lV z0<^M9Ia|yf%{w{r_2`ob8)Jq<*VmJ;JzpQc-fa%w7938hQ5APB%Pn6opDqXYZ?#?Z zkhQO9l!+|r8gC9qEXnm!fq%ym48#&Ibl{%Rm589YBe)8 zTB1DA%#cZGH@@J1n9wLH=aKjNs`xAo2TCg|O0D#L+QC7i7sM?k$dcyy5C7~3y!%q%;Qz6?PlR_cqZyb?W4CDBqMs; zeGAh*&*Of8@9bUXQR7hT)PH~M>kssHP+{bi%vAEvOsQVkgzWqoOrEsdYX-N6<<`6LW?G{=9>GJ6f12ZHv(qU^O{T;{(%x*zT^|ThAR5f$6oOQ|hB8&DhPn;Z zs-i@2v*JFcqpFZo$RqbYcXDxGxg4{dG&!XRGnm~x8BE@O8aOMsJctqOr13yF&OZ&h z8iR*_Oh>AFb;)D1<0v3vZru-I?Q>DGU(T9R1yOYpqa|;r%fX|5N|e$r zvgrU+mSboPX!B!_FPE|ish%jk<=0&B=f0OGTWOK~G_M}mkyXoq1>dIi>^)XcMS`$l8rs_RTf zeH)%R5o}JGm=LcSONp4=Tu^ z9gu8^q=d4?Ru^TvWqLMd6_th1>gC};fK0Xe3go-!3q>ADD;=+|TZnQb@+ciAT%^Ck zd1s!0{gS3!{YVg3K3Ig>T*j=X8XJoeLAFLd|79^L& zyLe~YF{Dd3skzUmu9@05NU1&HQhr>WP728%P(P!dNk1AwZXFh8g@M*e2t5^ZM6mWW zbP%O-&5SCBu4lMUfx^P|&CY09{DPxY=Ufn+{6Gz<5V9q>Rf74IyJxF!HFF^PhK<1} zQGF~GyZ5?1GHTGZlOz-W^o% zvG`jNAz|^S7_RD#(}xX}WhEn!1dR?X7P$V)UOBomoAbE}kZ*1XJ^Z-)OP?7D%ucp| zt$`g1SnNC-sPsuu)z+Z&%xQ;eDTdppC$Mu=TCTg~nJ!)0<&w0G0HjeOhZZ@%zMjh9 zokFlXd&jbh0aHgAWFv_?!+BNksL1wLi64B%2|vyOE!STh!s5uGucW7-JU+Q#6)Sdt zPO{tJ8B0jiKTqAdL=mGp4S_kCL4(pZuO0cUu5GOHIf6HlM^NvOmQSVW+3Wk9YdR!_ zF|;a@b*0fBIo$I^8^A0MQP+S@^b+bcV7tQsNjc?J&SN~9v`GsH^LN_LL{OAsEDmxS z=HcFKGIjZ#*ulx<$~`{9FAR7#BOP-_`xEQV=cJ)u%j0 znwu&;o^v28B7}gD&P}iP9J(WP@2*&@5nBp&#YhH_Fl}vJ1bNLMIrkjY;}qAWQOcBU z1B0D{(=RyI$gWEU)7_JnG=9ddW?-7(L{2q&vYwZCxW5ECkS2v01WqnqZDA#K{IIV4 zBm}$&RA5aNTUVs^D4kD;6-&rTA)O?GYAzulhtF11^ zDMTVT!SSg-ulmL?9h4B>So0})ELe}MYFAuXd~rxUnME-wTlZ79>fBBJ{#HeP8*XIn2M~;jZ9Ok&_#mtvudB-4M>YM&`tK`6Hq8|6iS&d zTb-n(d13=fdKgyG!NDVklY_R7hhxX`qA-vO?j=$xhF80L_Ooq?r*!$ld#(A%_gbic z($H_*VPIw;GF?LNvZQc#(a)yuEO5SAYKa6M7m_R2z4sP94VfEkOt3|fO*B#-C7<>_ z0aZi2u@1pZwNQ@9FUUcgp*fYDE_#l~EP`}pR9=*@%~ykq(uS-8?Q-LrtMqaMOs^AXk_y$a zQp4;}QQ7FAb9!^cSsTp|vVhna9mxoPd_{*&tGJzZe-*X-sS$?8%$SXQ_r{X%BIwYV_E49e$;TR3PJdH>n-}Yf zdC)8g-Y1$h(C=EhtO$uYDZWXaOmcnubCfQJ9)gi|V}oqHLR)8YRsG~dY0*Me>T2Yu z<*dPs7LREitxZra+UQy?!}+t8aE5@U+7o#U=zz$jfg0kG7G0A4xQ32|F`~!=25Jz5AiJ-R9X`a81 zsks~Vtjo@`I~nd3w0rSQughky;3OdXv#a+S7^4N!Ewgw;N}({d`U_R7Yj;|_b))C( zNExfr0FG0#@;VAmf$wB_h%_r9SkbRjmk165xJt$xh+jL43nh{T?KIH7fbw`m$~jKB z=M)8jHztlY$9>9ecODqjaljm{S6Gv*UADzbVm7#oU7`He*H zXS)`uZSCys$guKRk<1(H4m)+XTJD9zvEj2>oye%mwHo}7@SL^bBtkIJoF+))0C z9WocD9=d|-`r2Du5HJ*K>MM4668SyTB|n`-$Jnj8!m2KSKtlFGr5%Lm=eUUpa2v<2 z&N-i35vo?>eDQiNryENB`%@M3iWG|>zxRA3rP{VGr~8k(p!9(OLLC5x3N8lL*2pV@ zk(aH<+)vj;<=>w1#Cx$|^&A!-nt0|XOTL8>I59}41ha9)eOa3Dwun^%YU86cJnjZY zgzkk5HVTULLw&El#_KdUJ^?3Vd7XCjlXA`5e)@cbsF%LiB<>ru^qSxcZKTP{3Q_SI zOR`vB6H3Htbo$z;LkM1XCc7=dpkh&J`j?Vz;wtQ89T+Q`gZwM zUuwpkBJGTvCe$cPg#638h*lhv?T~FU9v)RTd23nab#vYkfTP1Ynx!j`s^FTgh!L@n zoxLm?8TgJMp@Z3lwCLP~Ct`aui;-n)k0CfBozp>_L(98;2X~PyH$S${HG%{`{|+sF zfpOXnYBnmq13D&Ca0EY|lCNp+esc_sSxD|JGSqe)@3b+u6^j?&osy4{Mk#fZ$o;HR z8I@*ut6iI)tfbmg95qz3^9V$TuZno34uu7Y=fbm1-bMIu^ydEZ`E!X(iFu59E6)LR zTgNb;fMrl5M&tCLXE0VfrL zz7wF$Q?AMB&Cqi$c(@L*?>)5NQ`FTyJWSCNat@G#OlPkbAb!a& zwRy*p;`Q%_oD7$I0I$?>R}2rtpzvrejsmV=Xoj8DfQvOEC!=SWS66vHtduW9 z6=AYnqhr}|jR`Yto7pF(3~_)B<-T6euoL5Xw^=S(bvx;_iv5{R80rys{-&@<=-GJ# z6&pcekp)L0(HWEyTRSvX-i?FChez(!m1XYy;O$syge92a6Bj~MD}V8I8q^Z!)NLfG zjekGs6=HcNW`_(!MQHIt&GWmMvlX*8yD z@fnA4gkI+DZ_Ha^v_0Ri$GGo~moou3KoY0eGs|K0WaUz7i%A+=iHU_Y`LUFDwi>A$ z<+*6vsN1U#Pqu03FB4L%FQ%{-`hENKsYp~%vD*x$Jp8^ zj~^N*pIK$Gf<))z!=rVJnvSrwlXi5PxhvJn683Wjlc6}d2REya8a*^G%x4PrdBy8k zt7^rD_JgQ~ygTODHKTlpO(bgV7b0o}h?-5)LtMSi$M82M_KNcbPDkC;lEZ4qh4LXe z6U?2s1~ca-<*_;T%Wyl)U|^fUCLZ!mM`G#g!q$`vPHKeXw>i$}dsoZ2b)xmzfVAZ< ziLm*(v&~gbt74F;gH=05yQ^?Ns5duhQ+m3lPGi+u@HfAM4Tjp}2wtFm zVY)7u73PIlr;FhGmO79?udlSRav5AxgA=2J5o<4B<718pyc8j?6ck+sGZ6A?b$xNvgKdxw67;!P z0?ML;1pbid?=cxy4^q_T3n*w2qeJc{6p+Tt4`tqZzV?F|qZEna7iQf%0C z8Ko+HegcET9wSXA*SY9_XzfbrUcw14bdUbf)Lq>2Qn1JZ_7wv|Gw@3Oz5z8^qbLl}V( z9Ma+bVD5ZPK)%uWY0@nf$`c~NU_Iyjaf#}lH8pei;;J6lIMi-E8rksoGsDxcF&WzV zlnAE=l_e#o`jsQ|n+)q6RxxLK!I(b6sTv23nI0+-k?}b+AKj9Sd7luNl~b+Ij;Usq z(7^Npc_V^&-hOE%XfGb@M9`_A<&l;aOxO-ithNVj++qP4>0VwP;O`eIi0KHQKt1bm zDeB5GenzgE^x~HB2iu#Z*m~k9>I~_$9_(^$wkN#OV;C_4?(O8|Xo?x<-l5YGXXvpk zfzTVX4szaCbKI_at#E<P`>BGDS-Sdf9PctQBx!`kU%)5Ff*-PqK1c7iU#~#w=INr0N(h4^8T`T(E>jy)NrIvD#sRO^%##h=i zwr=4%YMv0!uGd}UFzluoyWC#}Mq`U(j^?CcJEDTc>W?g?6K%|(N5H}b@v$8Lg z$TjkkEXH!GF)v`G;L*tGS#x#dv-3$`!H64sNI+_PIZcC|6Rxb<#ip>kZs<82wD-AX zu$mQ>D5+h0Q=eXWct)G_9Afn{B3N6)<4Tp7e-n!}MWxLOcjBH?gYHRdWuXT*b2K6> zxn_$a2a7mQ^0}nzBDO~LrGkgHY0lYHlyC+5^U9;cq%JMJkxBMVrrbQ6W(zTdy&)^j z=SV6UNv|(?7lXT@Z0)aPea_%GEBC`n{tS67gg@+PWLOF=jz2b}-G`Hz8qHJ8#3|aV zw{j{cdb3}5Zj0PiG_`Q+VrC)N&+cERwVpZN7Gb1vP%j&66MhV&=UkSbo`vLvYV;?P zu!_R+#55n=vZ5c#?=WzJC$2@9%2SKt(BMcTBj0!oP~wmX3l-I8(_CgbuUL-uN&o1p52M~k_zTLW|I4j9HJbAPVdRoJ=4t*_x%Yx=Gi@`FzY4BW|AHRtE>Hey%Ch{Ejt*v=S=!u2z4SyaxXXU*}9 zq~YQ$6l8M|uZv2Y&N_6b0GCk4H8-;WfrUH4*^cgB{LAE56Z9TUOSnOg0zzNM&Ebl} zuUsRS4}slTy%w&m&^=DF@1m>YlG(4#Zb_WS_Zh2jkJP(>653pb0A0-O=ku3lels8j zJwN(lq0coemKA>Fo0vysD1WPT)ZNQOSFhiMA%{7~Q))$Ka=N+5+FVarCaNOUG4Tzc zw|ktfr~Vpcjh8p*5c)mIpg=50hby!CqVb!+UGL=imjTAxH1f>39F&$oiL6^uvVj5H z#Dw6rd1~PX`0ZSNRY16Hk0C`R*3*E8-YTg{f^UN|D~@-wZG7mn#xbvQvBXp%n<62J z($@e#u8&n}ab54Y2=%a%+st~bvl%g9eI*>1jdAaY@S~35u7UP+!UbYM3RvWNBzov0 zc`3t4h*+ZpM%@xC#P6gtTQgpTqFTvzBoJgz;#3Io0WfmalPuJAD)ip;`baj7-Ar!j z(}KuIft1TC5{%T=c^<`hh$fr77RKH%Cg-A{0J-|Uug9x@nl>uJlWnzP%g!bvC% zZ7-f{auL$1uXVYr%}XB@x~Z+L>M7)Wf>;ye&XW{&9mfUa490;WcTM6Ofv!v?uc1qV zFQgpyT?_=5;&>|#505vL-ssip)!J>Cnrxz38JC5`k^(e&#- z#86oP%&tjMB$~|P9_i!RSnLfVRO*=4ku>#3!n41?39?>!r*pJm>CUpM=2Fxqp1W(4OxYA%wR6q4nqY4amj$WGDF}h#PJvljX zsb~L=iYQ3gHW;ZVUi&1aon)NP^k#}2c*1{Ko`v?Fmm(*aqX62vuM{hx7@dnCNBf`~z80msh!h1(mUE{09M~spG8>fjzG{;B6LNiTnoQB^!#P=o zBInNvuAFIEIpyID-8rR6IXUHz%`wxilR=$xR>oSIDfy8kGK!fws|~<4*`;x3jbOm* z{&pd49%#yMj#7k=9&Q^fsC87=YJrdiV7 z_>sl;ozEeP4*=7iqP;sP9jsp0Soo3T(s|k8rwBqlkVyMe_M_o7&FL-o2D6kM`bjyxu`4db>sLi>=qJv9;N`2aX#zRcF!>|oA^)6IpcdWRf(&p+wZ`H|&G>c|pfIg-s z$=yteoT&Ki_qXf%#uv@;RbkAglV9I*ryP!wAXvCe-b_htd?AJA6gPPi$rilTsFE2a z%fNRKbm&S|3vNxy544t7J(k~vQoLEL`F3*y8sBMl`3+Ptx_BI=djy#Id-fx0wMn)L zW8RRXEY$GDl!uPjQ7Ck_W4d${ZV^|+by5v%r@TAGA)s8H9rjGIK|BK#mm#oSZQ9~n zy+00l7?+%>3{Lyxr(pPq5X*1#Dc8tyGxY?U=9Q~S3T#Q2>xumo{J38%sV z2XGr!eLqF~v7D1j%}~xMZcplupeCmwotu?N?DasF)i2Q9JD%j*uoXn*s|;G9{%l%b zd6JH2Mw{F(I$x)ce}At_-Kx(eg$d^vf#0C zxfXQmRL2v4S6_~aZjmo3U`ty2t@`P)+n2k8%Ei!IaKBPSJ*Ex;^{j{ZSmxA3mEF9ZQ@y+=u)PV!0WmIb<;Rwsxid%xWxG;T@^d)e z4~Cn37bxB8jvvN5dVy zZk%-I-_14)+E>we$Ydk%KfXd?6(98ap#|O}Y=_D%izZZf8HhjVpcOMlr=VEC}<-<=+*Uu=| zCZ(D_`j0;{b%09PN}@4_-`XjFimrC^?joAG9hBBFv}nf~M)%dlkZxJqaM zIL)yN5%|XCmyPLstkh9+c%e;~u5Je0Jf-A{K)2$<;?19S-|#pI+v!;QzIHv1ia#w+ zxPqLPopF=_mJM9{bX0!=&%>pVQ^`bwJqvP;@P?{XlsaT6^Uxvgcp`vnn)($^uH0Jt zgsgYb?;P}eaeTD3otPN%OKPv}US9K#nutjS$oz%*dLftZh`is+(~Vhmt5c`U&72$U z>9ItJ0zZ)OyxengcEz31ev+Vu`nilitRUw}37gyC=PKi$MpvWDumx0+dnTi5du;n) z{_$ib&!fBa9WIbg?M(_4C4LD#P}7Sym}Vb?S|GJwYGDLS4OMLC&Lyd6Kvg^5-^SxA z!7=x86OcrY5YgN?$UqQMqGQC1O6l#N10ml-Pe@6PMC6ukX&%FyLkgZ9ruAy1N-vMD zgT{KHZx|D6U>ImiWsql3%*y)nJ76$>;oXtEx{D2-+{P$_o3dz_R#sFm19$Gxg2Q91 zF>Cao2Wd@B8T+D$3r+n%-zYsrw;r-}dgem@l!y~;9}kFyH5j>nQ@w&A45|d;#{O9c zXC;b2$1QJ8`>}Ij#=`6ZhoIfUfvZx~vgy60jMe8>dtu~*cR%8<)(@vCXBJuQ;o3yh zB=82-rn*jH93jK=`yn}HMx+{XlIxFyF&>*B=leH`&2KDlHk09aGJ(j*$C6&i`#siI zhliE?NPKLnnGcesj-9p zs34T7;%qI{KwbU(k+-=ehsUK0rvA2jb=4G(Zf@9`%3LwlJ0E06?v~yroOyRaypD0_r|HMi!iZge&=89wAhg&j&-RJbio0XZ(qx?uhU>xkU8m@W=A&qEv!7Vq6J&n4WFIAk3FHspM2V z)G$s%N;y?nXc`PQrt-PVQ>4aP=Gx&M7N&dSL;+mQqSylky)icq0zXeJZkZ8k7QnNkn}Qh%eY7juiplj;_o-{1&vEu7d%V+ujIzy;f#x!@_Q=k8X~a4MZMWp znQ~197c;=gP5cQ3G0L?|#%;IW5`vY+yp&3-Y`xjuM*Ux8MvpW|&CyE|p&1c-o>c|TS=zUPyC2J0u z7!*}``r5=PWT=toPw_qEbo@<^WRDJhqpeR_jBjK8&HgoK4;2LP6_>boX;UD6?Ypto zv-`lR+k{B6(fa*_&o@@J(CB})n#zJ{ebgR3o44uW+dOk zfvrL{H6FUOZ1C#S#*=7q7`Zu`HRejLu1bYjd&+Yu1Pl5ZSxRrr@_;C(N&dHUHd5D_ zBektD<3@D@W>7vY7MW+-sK>)`IaJo`4uR{4jADlnEyb>V;auwpG`xrf&PE_b&cw`) zTyT3{8zR>t|NGvEM!3Y=N0}Yt@RiXAK5@6DOeBTgcXh;B-MTf!7JF zWAE3z#8A%OqmfI8oLuYfW9n@<7p%+SXTa_>uGa0lL9FyCEnxiDaT{T7DlT8?OOntR zFqcjE*%koc;UPL84S;QIZGqX?&Z5D90B!8--uApZH`cG8ooJ_p-y?QFR`$ygIgT|b zQ6yrX8NqB`jb5lZ&*~mkh@4Bj0nWV00NzPwo>u|eRaPvE0qUnz0v63GDFOp4+x*#= z%-%g_y*6?Hn43s1o+XXmwXVuQsw=B}kGm}Z+NsGZhq_=SGw)eG z@-Qkkl9y=IP(@U1i5l#v2$p~qqoVTeU0io(7q~Ob{_)P?!?VXE=Y-$Ry}$Y9oA1ud z{cYuo?t90(b$e1b=vDf(#0^i)mz`KX%-zK=X>zB4ML!%_!rS*OJ$`}yJYGiq5tY2$ z$yeL?Ub{8sxq2M$nP=jMq3`2Y*XNe!$FD3bx@uJ1ukV1q#l^+v&lk^M@a4R1J>$D3 zq^)MoM!Iu2?rhoM`qIHqzdb+1aQCz6_qE@AmlpQlk45_q?9ePP4;J!_wJpx3cdw4W ztMcCS>9zWHn{6M~kI?=QZ>;*hePP`%DYk+`mz3XM{eAn#Hu-l{K6rL3xOi;Lx(%1U z@Qq)il|K3Q`KvdJUfec*_{G={`+uoloxcCD-h2P?w`M9I{XuEr-%d}vR`ghrYkV|y zR9?K}skdF8&Um`!)alwWAAPsiMLZc@Uusf5#6Iz<`G2Z&J-)uVXXJ?Z<=5&r3zw+I z?cOU)ANJ23^IK^lb!XdO8vgQ5zuJR;Dc7$RI@L$%ygIddkzIbq@WmF-{3|`O%|_Ss z*csE)A)_wL(bz(}AYXHtMUHUxwX)fd>ebfuG73$K`?s{pX;$689rI4-D^>Cgls`UO zG9tX@m~WkLpyr$3dZg5S;h>%LVfexk+6JYsJ%_%If2brhs!v?-uj@O{9-C92eBq#R zBmP}fmDRK1)wiFQ<<~#!Sn{#sP;KUaobUKgMtm9XaCH5-S-$qKYC_a=FW823o>kU8 zGuw5_lm&x7OwDrj>FT2tS5@)BNj0NQdFQ1`BaBA|=xC2k@?F)fsLeh7nN|9o?`sPq zHNRdiJfeEBK4QbMvoGsxG!t)p9Oh=MQP9S5cb5K!E~Dz>*PsR zHzW*WcC^?wy@jl?oeAdtjqgn$*-s|0uz1d!W4!(J8lh>qL8nftGlel0%CS~mcl4hr zOtR9juo&B?s_Jncecg)8#}9hzZ(6UvJ1evI#>@fgs-xChp5bYBR8xyf`2A*4g;A1U z+Z6kvyo?pSH~Fm7xV(M#hUzExyw`2*_TyTg^^d2l-|njwmE1KYB_Ke1^iO;<4>Q-& zfu~E|LNvbo)~0>%-c6C4EXv*#uTOtx6W+mm%*GQAdtOWmdECcV&0Qxq<4=bvou6jf zYVL~2>L0js^WC+RQc}JB0s{^ut?fS}Z|JVf%#1Qu|J>up{SMxo#?LjKKK$k-uRAtl zb9?{yNanQMp1XS6sh5sBTk10CD&KX`dHx_*wVbvS|9s`;8XY~bc8S^JEk*g=+qR$M z{L|7cmmDl|$KT1zD=52OdfDt;SxMQ&>w{GcGWJ~667517Vm9{=S7sLyd(R|ybD-2F z((UbdJTt8ACG+P5heQa>!y>Ik`%5gDeI@K?sQmtt>M0k#>g=c+mln25FL9avgi@1{ z$FHPhjaKddYNJNa>!CNY^!~B^*=9~}34AsFe)WiVV-|Kk`EBj^LAOsNsSjPT zyX|e=`)Sv{3r?FDpZ@E`F@Kb%MciIw^|G@1ALS9Z^%wYzEB|_lR*C+EyZfI{;Mvcu z+t?!}sSZ%kS;4>&(v$k6Zlm?rp=5OY0p!n16SmdR1(k>w|Ya`xm+E zSdA{V7W`FodHJ5sLE1q@TKj7%;T{$2aSJ1Z~$>sSy{dw*#OQR?~ z>!YPTR~El0{q3J3+cgK@hMW8u+e_Qc{P9y2okI!dd>3E)p)PY|`0vU#^S-L{d39#b zIQ9AnAEkDaEU#C;o*eU}aDGmTm*DmZWrH`Dlif8Jd4`xAUudD*=I^$LY9m!4L(_#Hcyh);{L5E{#50(%VdP_?b8(_i;uVv6jD--q>r(GrzZN`EdQ@^by;f zw~Z>{r)w64BnVUboeHtMbZ|>jms68s>)M~H>1#Xaty_{XK5xf|{G2a3IQ_Nqg74J= zjRL=*kyqW8`tIEjW&3v0eUI|A{Tth98h*WV->x@K3oQeB{ z3roBiomp`kSGtzjHDyN{pqj9oBtj`YpJ}Z2Jl9gYjnSx)-)Fd|tWg;;Z$(yu<^c1& zJmU@9w(r|CZG1}V-c6~~{3iNr+q!Puv~_!pw6!KLQc9X^XyBytxUE~~m7QL?TE3iU zxGH&uhf4pgy}sJ5wzFMrdo8n7+2=|px~F=qUcFz%%_{ZhQI^qOK?7E4DMxi0=(RH2 zP;_y+s2fh)%ru|91dU%D!v1ar{T=%I|9O8WRN4}h2BrTGm6qKbE`JzgW2v=;d2mqJ z=ko+EGRYWw>Li>e&^pf(z;i}`ICBA;=ZrOSh>0r`XU57G z3*fAqR&v%2o->x6*vRVFcq(c)3)&940;Y5j7^ZwaKG3AJ(LZcY2D8B zk$Y+;`+9=Ge0yYMVo!V&o%)ErybdzpR+0hZ7=zB4z!-=z=(r>wV{9PCK=%7N`^AYd zc3=z`Lu8CC7z4%-8H3jGAdi7OhAWRjt9W1x7{g@@TEhclz!)xL&I~HJz!_3!pe_T>kU9gE8E}Tw8K}*GGo;Qybq1V~ID=K4 zfoT&cL#PbQnm`#sWnfPVC_|_Wte6312$g}-3@AgW3{+-78A4^CFaydEDg$*HP)4E* z_ns76*pmW_W*USM9b$!J3{+(x#*oDrm@a`aWX8a335+2#1|~~j44E-7R{~=s#$Y_d zwu2o8Fj;Ca2KU5YJ3j0%fG8u0G8j7y>|lq14}=-1FoWj|RAnH}kj5EU`2fz4Is>&l zaE8*b1IiF81BDq-hEN%(%YZTxWw`ek2>4(O7$Y$TqmpLC%1D&qeqLY?r&vKHuR$2hQ>^UyP?dogLl$G8C6 z288YtpSB^>8^g~+On2BRI`#Vi> zoiT2TtFyD?XwT`6E@Ql%oeTZEPUT%JDsq}}>(8qNCC(ShiZp^XdiCqqucKYED*s@@ z+MTO*>YOgGS5kBApys0H66)MXt*@hVsAF;c7=sF7O>l^+ikfLaaA4@nkf5saihF7U z$_C`+6_u2h78R79Ia8p#et(-YR);il+V=1IWsatL-<*MZD-3ML`IYSW=a=UhvoD_P zIH1^Nw8u8TzJR4V?H3+3%+)Bg%g9oPjwu#2L~!gU+(q88*za@9M0b zaJv7VIgbptc%SU^pO6Q78z!F{v_Gu6d;0X$zMfmY412d~%g_3QX5_?Nd!aXQYS{JT zAy0m-9x}h?%a}JMmx4}tZgYrz^kT!lzYp9w^39XGyH0ujIBe0!dk^*{Zyy%__FZ*d z^7h;j9|MB}LKBp-KXy`mw{m%6;+Dkj6@2Hcs1#*K{VAhI*_xah@w#Z_%s#uEJLv3Q z-p;N^#U$Gf?~J!jidpOCezIc6<1s0J=MC$1KFA~ZtyYa7Avav`%)&~!SF(3X{*QO7 za?Mw_aUZ+6zTSg3-c4O&X11YWE>ksOla}#rDopW2gZYy>zo$2OoPl{Z#2L~!12b)K zM&bfhqr*&yAclc~X0xx_GA~?3Qf3yCrdq zffxfZMiOH%YQI+SBpaS&Hy9&YV1=a&JjsSABZ)E?PqLXsc~F)CXGooa{1==dbp|qE za7N+`RyqT#A3zyGWnlFKC_|_WtbPDx2$g}=51&KNZH1!KS%E@RNl7mR7$#$Y^=w1zc#koroczF5k@ znmm|aOXk-YYx0;E*uujCaE8*b1IiF81BDq- zhEN%(%YZTxWo&uIJaqj-@SLECuuto&ME@M^KQlboKO$y!NL0h(D`sJ5a8TIiW34Wn zyb}6~`M@KD*7*pbMG9FU3S9(+TJ=iL7AYh^6oM$!x*ssKNFggkA&5et5OFSKDW{NH zG2}vPC$zp?NLPu+b7azKe}50hQJ!AY_{1)m`^($3_N<&uK>>O}vjPGGo_V-WdDd}C z()cCHDF!P2w7V%i?`5p^!f>=flJXi86Gv_RX{UM^uNmQ0psLp;ARxu{{DmuSW~L=B zSI)YanfD(w&cnsU%w=*1jb(d>sucCm(t58Edvy8f!6WkTj5(Mk)Qz;5sv(?htLAH1 zrCmJPJkIxzv{^GwUw7ITE#UFSyQynL^megGWabiV%g4K^sOs`Gm_MoWdwMraFnFSk zKP`&P=%e5_On&$;Ofhs?W4BtW%;U94A+!Py3c=t`9FWn9JSYST5i5jN=s_V+h*%-C zVh;+nZiVER#mQIf8>!G&L;Bom(`PJYc)C{0&eBlxa>G`HFiL zCmIKzxDt(y)OZ9gi&e=oUrrrFFy?C1qU|UD0FkBMT+Z z!`c7PfyrRRKh=)xKcN+2(ZX5>W)X?_BNJN2yEXi7_F4O~MLoT3?RQ5zXz+Nd_B>t} zso$L`beH?x=1yMzUNI3-AtK0PW^IA^!vSfRlxJ_l{JMqArzZlbX3$M6c`)~HoY%;j z263a9U(Z{^G>ZLO{L@2~R*_jU^J&OpvxkaMA<<368pP=Q(k9R5Y}4X4+hrQeaDk zLp@o^iKC;TjeEpN6G&al0py~fjT@`Oc`YTEau7KUZCn;NO{y1AYuTm~SL5s@jiaUo zanh`*Xy~QP=cEa2sBow!#+|t?r4G|?ndcH9xr zT}!laTRCx3p0MZTQVl$BIBCogWun9dV^n2xhzcfA>U1u^sY0nCabc(Y;-5_6R6W^i zDV_77Ny{oGVG)0t7{7>OR0cLk;GHF4`G6{>A%O)*Nu-QhX%4CHCDR#Bvbc5y^ zE5gff{}Wwnb~$bnOHrhg(ls)8)Z0=Fe8a6}moL;XpGKB3h;i(dzRBRGjQgQo343dq zUoJg$$P^AVlx^*&G#kCEOPISV{xorA#VqN&+F$C*W(xhgHF;OXP?;MlMsnQHVwstQ zbQ7x`+Zi>{#Lwk%(cNP!;^I#eO=RUc-7ISs2kTRW6IVPMsgg4!^93roBi{`Ph}o*B~kNH3hm&z}<<5+N`Ti?kNC z-jZoG`x)AN>sfLab;F69nby`aA3KCRY6@ZNoA?q`%@3H0-F&N-G{5N!Z{>qcwj?FX z_A9g;pf(+cEWZYctwS{9dh^gP{c)i3t5~R@;WKfd@(;DCpgV(bpy;L=SEeJ4ZN4)# ze|fKl;6TxB7}U_VvvHv4Gd*hPu(>!;bSR>RM$N;4qT^BvpzNEx#6OwBtZ-SV>`Y=X z-pdV|8`@E1StK_SHMfbwWbu)0qq&jjV;gFuLo7EE>tsUwR)#O&Mq-_8hmXwoh8u|n z1nLX>=OS(-8V;zDs&U*%G#F4LZI^H((NNF=B)d*n{F5o1{XG|wDfrr(&yAcV%Fo0+ z_HTj_8)c>?U@)=Xl+m2CXlinP83t3J;ESug90N)n_0WrJvl4?z9rjS>?9~`d>bQq8 z(~>cm)PWCWo?nZ>q>g+jQ)xX0lREUFOzVvpOzPN&GG{S!IgaY4fKS~&(WGSSRxYNU z%t9b>{3D2cD6(`LHN>ki>SsRIHEEzZG#Quj|3 z`tBDTD0L4-q2|BgK&ksE3JpAp166P@-FOTGN-ZxM8BU+TU{bq_GT-N7Fcq9?@(VDS z)FUX`&cGrJCUqA@nH!2RnAANKWtNm+FsVBz%B(ws!KCh==)i4$j*Drl=u|V{0ymPn zccQIacZnNGEiH;H`imP$?JSCXeT5rItt^T(E#pQ~8;kZ5pX*#mWHiFaN!?-45V-CI22;Vw=E+wWOzJ+0w)4$v3?_9KMVSNNVlb(D zD9W5shry)opeS?g2Mi{4|3tHeqIymyeJK@f=4&M-uJWygqO*nmsu)b_YyoYjpBe^J z!J#i%1B0pH&{xnFgQ?)qSF44=RB-4s?TEotaOm^Z!C)#l^sVWP!BlYQ%h$tTDme7L z>cYjeSM)r)Z&z+4b?-z2{4^tOBz5ORkt=#|BdPl)iac)2jim0HD6*y}HnO!#= zYn5#1M>CZ9Uv~^9bq__E5659JsXHjj>^1>|N!>ruVSAhx7t@OVcB057lev-9R~AL) zOyx#WUs&{JR(NwEnf=%b&J?=%VK5b(DY(wSU<#=3CpwhH1>isxd`EKvF`x<_${qw` zFcmzM8O*|9DtIV!n}fkr@K6@_6$XXBx(?K(KvexHxdZfOaB8yeAO=&xsmblb7)%AHCOW@hFcqAdIQ@#jLc zU{d!`)Z4wkV=$>ZD9XHb9D_;SKiSt4i+?hO+RO=N=pz8>WBi&pQ_mWCjr;}XSe{p8 zNAsZH!7IN7-$DNLC93bRv9JZ-LH;l%s_$^^WDCB7{5z_szJn2S7$LC_^5;XTai4@G z{XTX_#7X}%N=+Tojv*)Jd+D1yas$;f`fLj>2|A>4oITmtNq^{f`1M>1z5^PDslUV9 z3oZB#XjrEH4i0~|;5(pUn)*A$Uv9y7K*M$mz5_Z`6kO&04$PD?I4JD%aW`z3<~Wd4 z_5?MQSzCs|ln=|7$%Y1$X?g>LDIbO#d9!14{T*fc-ojwYhuubA4T#TNa|eSdA7&eQ zw_I~f^z7jLyRuC5WiXg8C&LFmzt06kqeT;Mx55XSR5lO9d_59AaKb|_AUf?3pEfIZ X3`-j|HbpCE{;XxrEfhXhV*c%aZ59v^ literal 14762 zcmc(lO>7%Q6vx;3AS9)wr7bjteo)#Nj;nIZ}SQh ze-%8hZ(||T*6x(8$XI~czGM&w}md>lQQwx6@KJ@j=evMHbUOtzDOaFl-Ety*QX z)n2(?3);2CR%O}3`R_D!=aU5CS63>H^>(mWuLi5l+LfX2CvW$brlUu`X8I>dn58rb zKVepyv@*M&HamFZ1BZ-wyEERS(SG~x#sO{bouurR{B$&)DXuoJ^!Me5U)<@3BN>uY z67t`Y`_z$pw)pzOc|Pv*s9rJ8D;NjAn3v+0@&cdE%hjd4z;k)ITI2;jm6NMWc_}XC zrZ~5#X3i1#k;$AS;H1dxX_zY>b~6nM@MKB)0-Y37bys`y-ME^T>AyFsGIb*j>(mh> zy1+OAYGTf|D3&<^kdZ^wwc=ExD5mU^!0`#+G-a#mqc>|H17}X76zF`_!%)w)1F-MG%#e8)% zvbHidm~SnIscJ}VcAvW{7(o-fN%@db8*>FCj@N*44;Xh>P6~^|AXU`hdcp@yzzicOu z*|z2EQLV82X3BQlJ5ggz`I?$6p@6R_05X}30m=4^oK30~mj9pt)NH7Kua{k#%t>X8 zq-K>d@~uMh1L-@>YH}H~sM%%Ax;cV{Ofq8@HOq`y(_^HrRKDwb5{!=Ie2`!AxjMFVc+1*jA+xdv% zP|*+OCzSgD<$_J`Y_9CDtJUqSO%Q&i^rDkd6#P3326G__zC4L42D2av{uKs;`40sz zJc23)+b<~iCm0M~vQhA>Q>bF_#*Kn+!C-Jsh=S*%Z(!&(O+SUEztMqCMp5uh7|g8r z_%y1RIjX-#!NNI5=x6o58BnosP7-=j{2T>4ds^pCZ$kxRlhL@?NS45I7OGZQ?$F#+ z-G)y_QSx0YGBPR6-p8^zu2#2m@iEfZDb3E=SjYYhVx1F&9$DwM6KYjC`M)|D?U81C zS?AtDxnQ?6o2$EdZU?3p?3-p|b-CYwSiyX79C;j*i%w3n_3Cc^66As%V-&kuR|Pt)XQnyV0&>WwR=4x=lO}D>6@U)=)(CUXgF0;XDU&ki?5D$S7-7yi zPY$fq>UQo^F!#x6_ED+hTl)yd8t~4ir;h*A67PJV>iFiYoqFdpR>$A7!o!nO^s}$; uw^1*A=Pa$O4=wS`p||$5oqFcbyK9LDLvJ!_%zEA{(a*^Pp11mp{O?~z7hfX) diff --git a/tests/darwin/dataset/remote_dataset_test.py b/tests/darwin/dataset/remote_dataset_test.py index 4228f7e53..119b5d9ff 100644 --- a/tests/darwin/dataset/remote_dataset_test.py +++ b/tests/darwin/dataset/remote_dataset_test.py @@ -10,7 +10,6 @@ import orjson as json import pytest import responses -from natsort import natsorted from pydantic import ValidationError from darwin.client import Client @@ -24,13 +23,12 @@ from darwin.dataset.remote_dataset_v2 import ( RemoteDatasetV2, _find_files_to_upload_merging, - _find_files_to_upload_no_merging, ) from darwin.dataset.upload_manager import ( ItemMergeMode, LocalFile, - MultiFileItem, UploadHandlerV2, + LAYOUT_SHAPE_MAP, ) from darwin.datatypes import ManifestItem, ObjectStore, SegmentManifest from darwin.exceptions import UnsupportedExportFormat, UnsupportedFileType @@ -358,6 +356,15 @@ def files_content() -> Dict[str, Any]: # assert dataset.release == None +@pytest.fixture() +def setup_zip(): + zip_path = Path("tests/darwin/data/push_test_dir.zip") + with tempfile.TemporaryDirectory() as tmpdir: + with zipfile.ZipFile(zip_path, "r") as zip_ref: + zip_ref.extractall(tmpdir) + yield Path(tmpdir) + + @pytest.mark.usefixtures("file_read_write_test", "create_annotation_file") class TestSplitVideoAnnotations: def test_works_on_videos( @@ -611,14 +618,6 @@ def remote_dataset( @pytest.mark.usefixtures("file_read_write_test") class TestPush: - @pytest.fixture(scope="class") - def setup_zip(self): - zip_path = Path("tests/darwin/data/push_test_dir.zip") - with tempfile.TemporaryDirectory() as tmpdir: - with zipfile.ZipFile(zip_path, "r") as zip_ref: - zip_ref.extractall(tmpdir) - yield Path(tmpdir) - def test_raises_if_files_are_not_provided(self, remote_dataset: RemoteDataset): with pytest.raises(ValueError): remote_dataset.push(None) @@ -691,137 +690,245 @@ def test_raises_if_preserve_folders_with_item_merge_mode( preserve_folders=True, ) - def test_find_files_to_upload_merging_slots(self, setup_zip): - base_path = setup_zip / "push_test_dir" / "dir1" - search_files = [base_path / "jpegs", base_path / "dicoms"] + +@pytest.mark.usefixtures("setup_zip") +class TestPushMultiSlotItem: + def test_different_numbers_of_input_files(self, setup_zip): + base_path = setup_zip / "push_test_dir" / "num_files_tests" + directories = [d for d in base_path.iterdir() if d.is_dir()] + for directory in directories: + if directory.name == "0": + with pytest.raises( + ValueError, + match="No valid folders to upload after searching the passed directories for files", + ): + _find_files_to_upload_merging( + [directory], [], 0, item_merge_mode="slots" + ) + continue + + local_files, multi_file_items = _find_files_to_upload_merging( + [directory], [], 0, item_merge_mode="slots" + ) + num_local_files = len(local_files) + expected_num_files = int(directory.name) + assert len(multi_file_items) == 1 + assert num_local_files == expected_num_files + assert multi_file_items[0].merge_mode == ItemMergeMode.SLOTS + assert multi_file_items[0].files == local_files + assert multi_file_items[0].directory == directory + assert multi_file_items[0].name == directory.name + assert multi_file_items[0].layout == { + "version": 2, + "slots": [str(i) for i in range(num_local_files)], + "type": "grid" if num_local_files >= 4 else "horizontal", + "layout_shape": LAYOUT_SHAPE_MAP.get(num_local_files, [4, 4]), + } + + def test_does_not_recursively_search(self, setup_zip): + directory = setup_zip / "push_test_dir" / "topdir" local_files, multi_file_items = _find_files_to_upload_merging( - search_files, [], 0, "slots" + [directory], [], 0, item_merge_mode="slots" ) - assert len(multi_file_items) == 2 - assert all(isinstance(item, MultiFileItem) for item in multi_file_items) + assert len(multi_file_items) == 1 + assert len(local_files) == 2 + assert multi_file_items[0].merge_mode == ItemMergeMode.SLOTS + assert multi_file_items[0].files == local_files + assert multi_file_items[0].directory == directory + assert multi_file_items[0].name == directory.name + assert multi_file_items[0].layout == { + "version": 2, + "slots": [str(i) for i in range(len(local_files))], + "type": "horizontal", + "layout_shape": [2, 1], + } - def test_find_files_to_upload_merging_series(self, setup_zip): - base_path = setup_zip / "push_test_dir" / "dir1" - search_files = [base_path / "dicoms"] + def test_dicoms(self, setup_zip): + directory = setup_zip / "push_test_dir" / "dicom_tests" / "dicoms" local_files, multi_file_items = _find_files_to_upload_merging( - search_files, [], 0, "series" + [directory], [], 0, item_merge_mode="slots" ) assert len(multi_file_items) == 1 - assert all(isinstance(item, MultiFileItem) for item in multi_file_items) + assert len(local_files) == 5 + assert multi_file_items[0].merge_mode == ItemMergeMode.SLOTS + assert multi_file_items[0].files == local_files + assert multi_file_items[0].directory == directory + assert multi_file_items[0].name == directory.name + assert multi_file_items[0].layout == { + "version": 2, + "slots": [str(i) for i in range(len(local_files))], + "type": "grid", + "layout_shape": [3, 2], + } - def test_find_files_to_upload_merging_channels(self, setup_zip): - base_path = setup_zip / "push_test_dir" / "dir1" - search_files = [base_path / "jpegs", base_path / "dicoms"] + def test_dicoms_and_other_files(self, setup_zip): + directory = ( + setup_zip / "push_test_dir" / "dicom_tests" / "dicoms_and_other_files" + ) local_files, multi_file_items = _find_files_to_upload_merging( - search_files, [], 0, "channels" + [directory], [], 0, item_merge_mode="slots" ) - assert len(multi_file_items) == 2 + assert len(multi_file_items) == 1 + assert len(local_files) == 10 + assert multi_file_items[0].merge_mode == ItemMergeMode.SLOTS + assert multi_file_items[0].files == local_files + assert multi_file_items[0].directory == directory + assert multi_file_items[0].name == directory.name + assert multi_file_items[0].layout == { + "version": 2, + "slots": [str(i) for i in range(len(local_files))], + "type": "grid", + "layout_shape": [4, 3], + } - def test_find_files_to_upload_merging_does_not_search_recursively(self, setup_zip): - base_path = setup_zip / "push_test_dir" / "dir2" - search_files = [base_path / "recursive_search"] + def test_multiple_file_types(self, setup_zip): + directory = setup_zip / "push_test_dir" / "multiple_file_types" local_files, multi_file_items = _find_files_to_upload_merging( - search_files, [], 0, "slots" + [directory], [], 0, item_merge_mode="slots" ) assert len(multi_file_items) == 1 - assert len(multi_file_items[0].files) == 2 - - def test_find_files_to_upload_no_merging_searches_recursively(self, setup_zip): - base_path = setup_zip / "push_test_dir" / "dir2" - search_files = [base_path / "recursive_search"] - local_files = _find_files_to_upload_no_merging( - search_files, - [], - None, - 0, - False, - False, - False, - [], - ) - assert len(local_files) == 11 - assert all(isinstance(file, LocalFile) for file in local_files) - - def test_find_files_to_upload_no_merging_no_files(self, setup_zip): - base_path = setup_zip / "push_test_dir" / "dir2" - search_files = [base_path / "no_files_1", base_path / "no_files_2"] - with pytest.raises( - ValueError, - match="No files to upload, check your path, exclusion filters and resume flag", - ): - _find_files_to_upload_no_merging( - search_files, - [], - None, - 0, - False, - False, - False, - [], - ) + assert len(local_files) == 12 + assert multi_file_items[0].merge_mode == ItemMergeMode.SLOTS + assert multi_file_items[0].files == local_files + assert multi_file_items[0].directory == directory + assert multi_file_items[0].name == directory.name + assert multi_file_items[0].layout == { + "version": 2, + "slots": [str(i) for i in range(len(local_files))], + "type": "grid", + "layout_shape": [4, 3], + } + +@pytest.mark.usefixtures("setup_zip") +class TestPushDICOMSeries: + def test_dicoms(self, setup_zip): + directory = setup_zip / "push_test_dir" / "dicom_tests" / "dicoms" + local_files, multi_file_items = _find_files_to_upload_merging( + [directory], [], 0, item_merge_mode="series" + ) + assert len(multi_file_items) == 1 + assert len(local_files) == 5 + assert multi_file_items[0].merge_mode == ItemMergeMode.SERIES + assert multi_file_items[0].files == local_files + assert multi_file_items[0].directory == directory + assert multi_file_items[0].name == directory.name + assert multi_file_items[0].layout == { + "version": 2, + "slots": [directory.name], + "type": "grid", + } -class TestMultiFileItem: - @pytest.fixture(scope="class") - def setup_zip(self): - zip_path = Path("tests/darwin/data/push_test_dir.zip") - with tempfile.TemporaryDirectory() as tmpdir: - with zipfile.ZipFile(zip_path, "r") as zip_ref: - zip_ref.extractall(tmpdir) - yield Path(tmpdir) - - def test_create_multi_file_item_slots(self, setup_zip): - base_path = setup_zip / "push_test_dir" / "dir1" / "jpegs" - files = natsorted(list(base_path.glob("*"))) - item = MultiFileItem(base_path, files, merge_mode=ItemMergeMode.SLOTS, fps=0) - assert len(item.files) == 6 - assert item.name == "jpegs" - assert item.layout == { + def test_dicoms_and_other_files(self, setup_zip): + directory = ( + setup_zip / "push_test_dir" / "dicom_tests" / "dicoms_and_other_files" + ) + local_files, multi_file_items = _find_files_to_upload_merging( + [directory], [], 0, item_merge_mode="series" + ) + assert len(multi_file_items) == 1 + assert len(local_files) == 5 + assert multi_file_items[0].merge_mode == ItemMergeMode.SERIES + assert multi_file_items[0].files == local_files + assert multi_file_items[0].directory == directory + assert multi_file_items[0].name == directory.name + assert multi_file_items[0].layout == { "version": 2, - "slots": ["0", "1", "2", "3", "4", "5"], + "slots": [directory.name], "type": "grid", - "layout_shape": [3, 2], } - def test_create_multi_file_item_series(self, setup_zip): - base_path = setup_zip / "push_test_dir" / "dir1" / "dicoms" - files = natsorted(list(base_path.glob("*"))) - item = MultiFileItem(base_path, files, merge_mode=ItemMergeMode.SERIES, fps=0) - assert len(item.files) == 6 - assert item.name == "dicoms" - assert item.layout == { + def test_multiple_file_types(self, setup_zip): + directory = setup_zip / "push_test_dir" / "multiple_file_types" + local_files, multi_file_items = _find_files_to_upload_merging( + [directory], [], 0, item_merge_mode="series" + ) + assert len(multi_file_items) == 1 + assert len(local_files) == 3 + assert multi_file_items[0].merge_mode == ItemMergeMode.SERIES + assert multi_file_items[0].files == local_files + assert multi_file_items[0].directory == directory + assert multi_file_items[0].name == directory.name + assert multi_file_items[0].layout == { "version": 2, - "slots": ["dicoms"], + "slots": [directory.name], "type": "grid", } - def test_create_multi_file_item_channels(self, setup_zip): - base_path = setup_zip / "push_test_dir" / "dir1" / "jpegs" - files = natsorted(list(base_path.glob("*"))) - item = MultiFileItem(base_path, files, merge_mode=ItemMergeMode.CHANNELS, fps=0) - assert len(item.files) == 6 - assert item.name == "jpegs" - assert item.layout == { + +@pytest.mark.usefixtures("setup_zip") +class TestPushMultiChannelItem: + def test_different_numbers_of_input_files(self, setup_zip): + base_path = setup_zip / "push_test_dir" / "num_files_tests" + directories = [d for d in base_path.iterdir() if d.is_dir()] + for directory in directories: + if directory.name == "0": + with pytest.raises( + ValueError, + match="No valid folders to upload after searching the passed directories for files", + ): + _find_files_to_upload_merging( + [directory], [], 0, item_merge_mode="channels" + ) + continue + + if directory.name == "17": + with pytest.raises( + ValueError, + match="No multi-channel item can have more than 16 files. The following directory has 17 files: ", + ): + _find_files_to_upload_merging( + [directory], [], 0, item_merge_mode="channels" + ) + continue + + local_files, multi_file_items = _find_files_to_upload_merging( + [directory], [], 0, item_merge_mode="channels" + ) + num_local_files = len(local_files) + expected_num_files = int(directory.name) + assert len(multi_file_items) == 1 + assert num_local_files == expected_num_files + assert multi_file_items[0].merge_mode == ItemMergeMode.CHANNELS + assert multi_file_items[0].files == local_files + assert multi_file_items[0].directory == directory + assert multi_file_items[0].name == directory.name + assert multi_file_items[0].layout == { + "version": 3, + "slots_grid": [[[file.local_path.name for file in local_files]]], + } + + def test_does_not_recursively_search(self, setup_zip): + directory = setup_zip / "push_test_dir" / "topdir" + local_files, multi_file_items = _find_files_to_upload_merging( + [directory], [], 0, item_merge_mode="channels" + ) + assert len(multi_file_items) == 1 + assert len(local_files) == 2 + assert multi_file_items[0].merge_mode == ItemMergeMode.CHANNELS + assert multi_file_items[0].files == local_files + assert multi_file_items[0].directory == directory + assert multi_file_items[0].name == directory.name + assert multi_file_items[0].layout == { "version": 3, - "slots_grid": [[["1.JPG", "3.JPG", "4.jpg", "5.JPG", "6.jpg", "7.JPG"]]], + "slots_grid": [[[file.local_path.name for file in local_files]]], } - def test_create_series_no_valid_files(self, setup_zip): - base_path = setup_zip / "push_test_dir" / "dir1" / "jpegs" - files = natsorted(list(base_path.glob("*"))) - with pytest.raises( - ValueError, match="No `.dcm` files found in 1st level of directory" - ): - MultiFileItem(base_path, files, merge_mode=ItemMergeMode.SERIES, fps=0) - - def test_create_channels_too_many_files(self, setup_zip): - base_path = setup_zip / "push_test_dir" / "dir2" / "too_many_channels" - files = natsorted(list(base_path.glob("*"))) - with pytest.raises( - ValueError, - match=r"No multi-channel item can have more than 16 files. The following directory has 17 files: .*", - ): - MultiFileItem(base_path, files, merge_mode=ItemMergeMode.CHANNELS, fps=0) + def test_multiple_file_types(self, setup_zip): + directory = setup_zip / "push_test_dir" / "multiple_file_types" + local_files, multi_file_items = _find_files_to_upload_merging( + [directory], [], 0, item_merge_mode="channels" + ) + assert len(multi_file_items) == 1 + assert len(local_files) == 5 + assert multi_file_items[0].merge_mode == ItemMergeMode.CHANNELS + assert multi_file_items[0].files == local_files + assert multi_file_items[0].directory == directory + assert multi_file_items[0].name == directory.name + assert multi_file_items[0].layout == { + "version": 3, + "slots_grid": [[[file.local_path.name for file in local_files]]], + } @pytest.mark.usefixtures("file_read_write_test")