From 0b5b694879defcb327bf62866b4cc14a6a088bbc Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Mon, 19 Feb 2024 15:07:27 +0000 Subject: [PATCH 001/120] Update config design diagram --- .../diagrams/pixl-multi-project-config.drawio | 12 ++++++------ .../diagrams/pixl-multi-project-config.png | Bin 126476 -> 128273 bytes 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/design/diagrams/pixl-multi-project-config.drawio b/docs/design/diagrams/pixl-multi-project-config.drawio index 1b8512b03..cde53b88c 100644 --- a/docs/design/diagrams/pixl-multi-project-config.drawio +++ b/docs/design/diagrams/pixl-multi-project-config.drawio @@ -1,6 +1,6 @@ - + - + @@ -72,7 +72,7 @@ - + @@ -115,7 +115,7 @@ - + @@ -126,10 +126,10 @@ - + - + diff --git a/docs/design/diagrams/pixl-multi-project-config.png b/docs/design/diagrams/pixl-multi-project-config.png index 9cffa92b4ae1b93a72bd071b1525f09d2e81254e..78532c38ea1f4ed5876cf96986dc73990ceb373d 100644 GIT binary patch literal 128273 zcmeEv2O!k{|9?Y6rR-5gE<428E9;ExnUT`roZ~p|&W=j5Q%Zv|t zkx(dE{a^2MS4Z{f^ZkDNd_H}@zrW8X?)`ed->>-`kLP&3F4DwEhi=vSRf`raqT8m6 zFk7^UCUViDC7LUjgObAxi1XmjVuG2D#-hRpoc)Uy`Me=&Srgs;oH1C`B7P~&xljCX z2^SoJ$S;N9hr^Mcp5jgzBpQ!&Cy0BXh@c4EcgHzloG~b;xo6-KaA`3KxR|7Zg`^C> zl)8j0_yv~~mxn9Z&OMJrqddqJ8sY*lSS*qst}P=j0jd%*L^@(Th&X}^zmx{J-sV9> zVZmQe41OD%gJ0I*pQMDnq>R0sF!-qM>4`;Ip&axvpf}nwQgY%_vY=Q-ckUeR>H`swj}751oR&$G)Fok@ffNqppl`7Nc7yOFj53{Gqj}z z3Z+HVl`xglhns*NLC^T0@B|FbgFJXCaY=FMxf?`(Pt;tg6UqnU2pWVQCpaRpF%e4M|!##;+#mc>*P0Att4Dtey&eW{&OWVQse?Op3)Av zS*XAq6F?aXspD`7v^T~HMIhHUR}~S5!xAx`^9vnu9v&!1G7*q#MB?!{-}#R@ zk)QDd1D~s%+#pn7{>_v^rB)axBA_B@4&-I1p4LTS&@PmgB_+sr+>w-r$pr)#q!Z4U zsx9hOnVUz-Y~pb^P#@KzQQxLKnMZOvfa3o3cfZUA{tGAlC)Md2>03HV zTI%UqYq^?hON*fpGJw1Q8<7Z>=uaV7loOb2@+A&Wbitu<9!RX#*HR5U&fCL@BnbfN zYJ7dh0EhDgb14Z5T~S1$Kbdln-b5TIav{27$#+qH7@{@RXB+TYLLA5u4d@O6B2=hm z(gj4Lqzexs-XD7D%LUb2(7ms(k*+B3x#N704y3U`{lcRNm;mZ)91%&Rx(2KQ#KG6+ zQBD}@ODxXO?aRQ)eTP}c&NK#~~~{pC4`HvSCpf@jFI|HplS6wU$` z`J?&pL1MkhUF3($V!_O4xPUInqM=WkI1gtG8g!6|a`yzI0kU`Q5zv{hPklq*faggf z+Xdx^1f&fr@)3V zmm|w5hzCJU3gk=QufkdqdL1Gz7b-z5PcHj?$Pn}p>g0FG&=U`l8;K4nWJmBsdVq%s zUqq>_mp7y{G@NlBL@{zPr1Cv*cy}a_g#i0d9--7_scE1qrVYtgh)cCiwpjys^M$JC zTA1G|cxql>p#FaC0vs$UiEF-+D-w%ALqNt6XcdU$HK2vZ0PU(yzTu8>a)JcrH^R~r z2T&VHM966H%OJoLkZ1=26=G3{^5;{y2hIbkg_<2GMPNd-k?t5QL?D_#Hex_W;1WhC zpo&SeMbSSH_bp(KzeiyILS~SWnIj@HGf+?pH8W7ksF{IM_WLk{3|aY5QPJZ8Vg=BgICE7qJgMu&M0YVZ`jerU8 z{x;Ni{;P2Wd5MTW!x1uMVDdKti9e%GC`tva9}+?-6yhgKo{wbyRlltMZ^VQ#9T|Zo!+}^*|e<@%!YtK#epII8xiz7=KZgj(l>H-BOr`TEI(1I%lC%?~btPpy0RenR=eqmbd*X4fz)}?_ z;((3vRo{Bzsg(Qz#ro9(App4x<8^bf-lA$ zizFebpAp%7y8~Dq@=ns+vXhhn!74I# zrr0}DWUWX=in3DTa%3PwO^g)F=r=^WDb|`2VCS%Fw`afhnTYynqpZ3^D#e7c?G%6>|}N z$R`L*4cS1K2bT*N`+o$2z@_Fpz9@bwDm_3!)YM9Vf}npNB_Q>!68Hv)%<~&lxr80D zNCE)^K?AA~-dAT1$w5pGPfLg^0M$7chbFm*#Ub4A2O(&z96JB z&xZ}s9aTh7Tbm>XzT*h`a%TZ){%`6WiYWfibq*D6Q;;<^ZBxqrbU6IaSgR1j06GFT z>q5^n1$mK?7=-~ToHeJk$uB`t^yg}DuB{)pNQtRZupe3LQ(BzU{{KOXf32sU;+!G( zo?_qstJ4CW|1WZMQ*2M_+=m4!^*<19^Tp46m!Y$~BkKEB({Cbv$W!xEaQi~2_i%3E=7V+uK%}12-%YPdn5G}*N-z6L5F8=};C~QvC*50!sQy_1M3z85wVEla>OV&?e-AWKkdP6V zm4whI`DYGLli5Z_Qbt^koGSXu82=l1^d*D$ORLi2aB`kCrPY61!u`|nYiV+h_20{r zBn~3+ABELyNX$sewf)_*W`E9Pa&uJr-@;_F-{^0$LH^%F-S$MHrUe1*q#elsLE~|C2?! zdxGWna3DZ`zM-=p`yBCe8$<35wQ?sn1F;@vSaca z_b7>Ts2=!|sk;CZ{l)%Dih=s;f}=7NzayDDq$$@_?i)!8bf@;;{C)u{ijVd0)$uO$=8Q6L9dgnwB}s`dSDGT|3o@*Ax$1)=>x z2!Y^+B054_ry*v=&E;8vOd9frZK!ZTY!WU7*9Pn(r2!5GaKZq`0wr!q+0->ZmLMUa zp`{IN$C;ngPJZ}H6as39l47_J-a6ojAR(XX0m=?FBDBv~;~S%qDxgb+wMlz|NY~_a zF{nu-!E4;z&nF6oFt5VtCPu%_Bncs*zfYe@KSW*-%^|s-; z+NPFZ_CX`+55GPO<86v?p8j{qQ zD>Xop4i16p1G9uhdJ-^S_s_e7jc6DT4KgE>-ug*4=zMVjQloy8(BoguXV9zy!juHA zc1g0+o8l|_4$_d276pC!D5-PhpQ8ngi^FCIbKSu}q4Qaef@8@DSXxF77!V8H&)+>%3)qu$ z5B@$7m~!~ccj$vkLw+>?#etDTI+;UG(;o>A=#bMC7YAj3Rg@#qy-~f`8+9;MaolgS0FKAeba5JLjYz8w-de zvNdH0x$#Wqj)?hMN+q(WsFqSnRRoMM==5CRW*}mqV^c|$gA>%g)Q$ZfaP3@o{%nO%;}X;p z=70pjMAEzg)}cOsk-8h?{?02}SP|fG5Z^_4kPg}XS|Mq`LO4$-AqI&RrVJ7&wRyFD zyQt(gq1}z&cv^q*D)p>U#kHYj`?u$b6woED{J&NOeBnGAa*&c^lwu2idw3@PBJ(%G`=jXCD7q8+3)o? znvj1cV`fg~NooG2n?dC$gQt)@C1K;|ChlDSsDFQFbFQ>3;3sO9`uFN5CG7o{yG_5F zxKAGQKgE31j0Z)KNnH4Y-C%z`dr=%0eDGAlJick{g2y{{yN=M^G8NPlaR{snp{C*a-ETr$C zX(od>>2K|pFZt5c?nTMp(7pJrj8k_cf^YpGjgLxAQqoAh-LZ2I{M#9?R0*!%&)5Yi zxD>mIfWtyxLbUK;L?k%y77p$Hou3^Ayxp?e65t1VWB!>$;3!4#1IiZ|(*N}t{n83# z`u;I3Qo=6Oq)17T_WhdGVA5vg}_w0qeI|>c0_%{~3WcXFh(4dK6b%b5&0+2D`JnIcm$?xgREqdv>+yOI%7VbtGo zGv{-pl>Ttz0T$YktMTPHTFQ0&*u2E<_^q4SB!~4zQ25 zqmze(I0(ytFR_9S$`uCz8c^y4zK#t1fyx0=)y6r3ZqI#`1YfxbO3)ah7&!MCD~3k_ zuR4J!2ENZv+|vVmgP{hgov*zGzAPt{Gb#K0pBCQVwGqCEtzY03r3z0{j&!4z*_0^e zUu?0FeY0YeLrchtMT`P^DAM4+aXY>&1q^;`(n0$aDO)(_<#2D)0V2>0d@m;WVyCZY zo$B!gN{YhOMAA33{Vd624#8NFk5(jYHJUph8XUKv0O6SV8jeWHGyZ=+0Px8lNHQ#}!uZ9|8QkQnXab+GdT6Mhl8-Ri4F%o)H$G0?~9iqp)SxMMV6 zw5v5&(ilLKq6k_UK`enW`gi8uO`@8i-Ah5?lSOILuZqD7@7pquN761X64(ffv)WQ= zu#2(+1lm2HCs}T4h7pH0W_(;k8==d`7EZtN)V>G=-W$x>%Sh<$D$C;vwB$j0nl52J z_#t-vAu0WnUGI+TPfUJd(g<67piS5eMyOD%>&QJGp)G#V2gZ2yp+lXCT`Z_d{0bj@ zBNzo%4fJ_~TD-uQj_yyPO|rSLCeQYYMg~0t!|W3@0wEu&=DFv#@3q@2t{vuP9KQZJ zTIBIL-kq6I>T*IlZOQutBM@h|2Hna#S;U~CEgia&(m1B%)kDDLFSzM+%u-+^1P6QfYu~#w6pt8Dz9AVv-e>FD<95 zI(k)~&aih@MK1B8dyJ^Ug_k!9m%7-dCscYEzfd}etO!T;KSi-Cxbx}7Z;ma$GL+_2 zXr?>61E1es%t*+(U){GjT{56Xh`)uuoOv)15!M;5)d5UNMiwoHF3Nn zBZD?EcjIaARMu+Jgwo;Lhu<-@hNvQH&Ck(q37Xt^_~g<-og2woY0ff^f*P# zuS)yio*nRadks_Xq+yLf7@JBs@^*`zVl!tmmel9do7Q)5dT{2B=OEAcXSLjmIfGYp zwZ%F#`4j`+i@L_=MryrbgK4&`Sss_|_rzghWO&8kvsarMxvmGSH0IuV^H#!Yuh&&J zPp|nfF&}ZLjXb#Z_SDN})uJO?+=CH^xLG<5<1}ltlFy?%xbN>$Sp8(wsWv~dL?YlN zJ3}NVK2ui^V(|U@Ao{inrwo*Lx78B!_p`Bk*V${|$EhYfYlt4N-?n!Um z{>y?d3t^8w@~E09`eOL$-zruxF>KPRn(`fHfH~=g(ru_Igqa>%F7wdQ(|2#Q*V{&! zHPZxoBW{(PB)zv~SF~<+E^GE;zSy_N_A)AGGO5-w&z!FLjpgkSj@88hcU-%Lk8E%| zS)_TlN%*0=np|^DiD1LSg66ZOp0AgPH0Os}MsNRww=(3BTY9?HT|40}>by_CE&tOo zu2-Adu(stK$$`Eag-foEbPlt*SQJPy3K;G4DyjXnKCoQ-?YrqG1kq`?{Q>p_MzQCm zEBmF+=@eYJP=7%-AZ)e*%=~Gk6+oi@Uv^~k%@8gGL##?<&kIY7vd-KFpm$2m#`Uhw^U^HeK zMyapO%k8-0bFEk17F>5bw>s}pxhC#nf6H>ij+v9M{ij}Y@n1IFB|-Zsy8ELbWAO_S zt-M_Sf)}6Jj?N}pa4{Xx9s4wjj=!^Gd;Rg^Pa^_+21iXQfS{56n12xlSc(3jJ`ivf zG(CI1Ow~rUC4#KtRk5;rVb4`xiDr$xJm)I}$H-r3dT-xb-C5I3lee-oPnS43ff;k@ zV4|~a(y4vUHB+x56jwJ)Yg@+Q_HNJT5B2teS66k{yg6B9(88BCqM4fGe&BMpT^@Y$ z&|vhXdo8)|WUx;baRqow=<}s5&2~vht2+*tYjo00x6?_C*3lo&H94QQEiU`*S$E54 z8<(&44`0i*zM2qk;!ywYbZt}H+P5k(Vmh*UL}jr_-?xVW83->H@*8t&y0jH}DtVPV9$1aMbd5)iYpIwEu=njs1J_4^Bg zre00gaYT%7y;g0)9lqz_#pKf|RWJ9mp4}|i7Pe0*3PC*gv?9prs@IZSe&%@Hsxpl{ z--26@b&O46=xv%xIgaNfX??1i7$P=oiDituYbkcl><&ATu{wf9^QwQ}K)=*Ax!jhV zsC&Iz((!R3yLX;7eXNtf&xpAcD8Ibyc+23f1H^ZY!fXuY0~gaevXT&^t@Y;Od|jtx zFqcJ`>zm|b51k#A7n=>9lv|bYnd?9e9paN~58bdEj3Ig3w%~2~H5xjM&#Y`|_!ukv zXO(nbrw~fPaMobeJQjX#PgeFB4c?JV{rmW zh&yI{=7pS5h%8seoJrMs#q?dH0U|Tlsj#=Z3(ndea_wx`zjuAjgPk{KZHu{s6hFK> z=^=}5#yZ_grc2bVde(aP_2zQ}Yu+ih(1{$)%-$v#SInNiGp_xCdwavu+O=v22Uymy z4Td6_&hNRoYEb!-rk51w$I55*F>dsqD()_2jJ)_Uuel%!HF_d1Q9D{z)WRV#0x>jG zuAvP9wO)GqxH>|MnKOy-A%N2ZL8*IUky=afsJLvasa@6;PKR>)HLt9_a`Et4p{ zpQXL7OGG^NkJez|QYydl_GTwt^q#jKHMbtUx;2%dA)w35Bc^y+t!$$IxKbGG{^Qyc z!ye9he2sPrgXfTWVQuGK54Z-_H1_9u60dB3c0RagIllx~d!TNleXtv20%K&@7-tAr zqnsseMW6i(23QjCP-zUuyYaZZa0X#KUvkRTx8MH(%&RaPss>Mp7JqT>4QqzmR=r#rZOd39WI zNez2r?yl<-E0~X#)+-)xcoequ4TF@A!GU9Sr?$SCHbJ}>V^q<+8M91G#E$FXkaPLv zK{KT|OwRU#^2^#K9}Ju6*A6Sih>MlwE_s;WtXXT8_dx8?HAjNxE7z9cSLhle*wCq( zG6Yu2YBnji@CZLF?zLy*Dr-08*T(kyB5w;i^yVZ-jF}@!w9=NAuT8-ma-J1;RZK3s zG|jZ56(=+3SCjlCxWU9E^PaoPE>p99)G8)MdRm@wUTPIq*I6BL~Fj@mtjbrK4=MljcsN!%Syt zZ4vF$qU@`#2Y{i?ErNL?o`IR$-yrCT9j?N)R+#)tx} zb?D_~(QCI~!XBdGfo+1%b}YLy+`X$yWhgxG;QRF2jpz~qZb@I?GogT`t_3VUJTsd- zwR>_$Oq-->if(Y|;oYBx4xHImbHN5Q!m&lc7S#K3BWNTr|5%I}rI8LM`4T6N?Uy_b z>hi*1(h#Ki;FdoI1owlCEkUan&uOg@_>vcu!PU=A)4iPg@ygfnwDwzr5ZMw)1|=;$r$Vb0 z{yQ#dj2z*uh>3b*WQBmW?%cqUFbWB~X_+Goz-ZVV2-zNIaxQ*lg;U#7cAnqOKGhYT zpmPry3r$ygr2gI^1kjhhwqhK4fPh{zg2l2k!zXFjsBbRD&D2zvY1BLnn(e|#N9P99M=UZT$@GWx7c&rlsIdh5VetT zPc)u<(Es>coUpk7r?MY?e`BU*hHl(>+>5QvS)ufv%a*h}68zGOK4{TBLVetCB!=&fLfj6c)2s*>Z)%HT6Zd;e2?uA`I-Ah9^oVi}};sQ!PNhNT*Lad_iP|Rkl!i{?ILVz5+k7s(h(OMJs zT6dhBtKO3t&^4!4JMGN$#Ji=y-=N)H7stS>K_eJ*!)s^o)CW4Z)}-|0D6($aOH&8O&sHJem?hd3M086OT`#{=_tS~@U8L(7R?R@H_|A)aR zcW>2iv5YZI*G|<8U)y%seM1z>=CCdH*R$If)kyk{?B>7taH!tz)BDT|`*;-ah$odx zqwgFOJg|30YHiE9m-b7!7lJ-SwIhpY1u6e6aj$-Jf6Mc-K{HCG51vK6)PlHoHGsCfqu_?|DVw zz!aS(VBhOT*Sv{%q>6)7H2*u z%D9=75iw7%5_BR3_f(+nvBnFT-pbIGc6qqbg!vI7jcv=WjR9kQ&XI^?0*1F9rfPak zj395zYaU$H^JaCbifM;F-`xZ$!MpLo>8_h4G9HAeSedr70JpasdU6mcO$(z2x=mi0f{+KOnuQA%NID ziVd!>;E2~nNJa=dN3)8(ms1LiLZs^)uBksQH#7ceX3E#{^gfUD)*}0K*XCSyB_C0k zc1$*nDC=cqVjumjP}-$s8A3#iM)Ppx=R%q-4|?9T7WKWTRL!-_%bI*Cd1!-N4IqEh z%HW_F9@Xb3k7b*mGrEiMe&4+<-@cN2+fhE#nTH$}sT#}1?Q34_{e0-w!PR+N(uB>k zq+|Aeo}I~cc|F?Oa6Da)DH05S!*gl46X?3#3D9-%a%tDyfKR6iwmG?mWg%c<&Dgpl zyn0T0iPFM=Etdte(F?}8*$X~bRCF6!hePp?LQ@w637oBND0YkqjG^Gk&T=2EyzK>| ztdTJgm`j$o?AWTOZxgWLB5z$)reh;cuLvV~xy`s3J-QC%k!-9{%b`$gP$KSNB18Z;9@ml-PdY0D0o#vlWBAoW-LTPGMTTTaS!7h{q zjOP|1OHMGo*<@GhsqS8&UmO|2n5<(vBGum2Pjpyp=oqIvAX@s$aFZsqb|{GKFX*Q) zcLs#7b(Ue1c8XE5;;cZe#RU#2=ZS#5&AFDjALu{tHHZ_kDzqCIwJO@hi@X(@Qj+cQ ztV}>IYqx66=g&UwiEu^ZO}l}N!mM%H3S^WYHzcDx4f`XIrY@BS^Mtbii>d`Z8feUn zo_v$^Vx=D6s%!y<&+m1RwKx+A&{hm%Wc>_-gON`Z+iT&G|y(_<;{0}k2YCud|U+htgekV9EB%B6kegcJrYr%>C*AKrT8*qWJvonQHi(+jWt?awly50p7mWoa}^~i=Cf3B z-9EKldxJj^S9>x~7cY|AsR1PX;})Ss#dNb)`sSmX86VMASx8sj-1#(7);-tffFoB1 z-3D3qwaC-c_xN|}GIvc!A!=kI4a5?gY$T@4PAnw>fD7db(e_K<(+_s8=DST0S$tJB zDau|EQuRiVrem@Wl%48-EXbcN;U1lXG+63rs$;4=!J)i1#_)+fs7XGXhsT&9zBtU*BPW(d3ItOtzsN!Gmx?`R<*_h)4? zx^Y7KCq!NIrUshx(gAPxJ-u4u+F%#g2c(F|&O_b|+Y9Y-qR?(P1B{4n?(G-vynrv? z9%DWr+BTYd@-i5}h{X~}i5+hqrN2KcK;(VgznPatId0#XsoPBcb&%tDWDP(KSNj#= zd(R4+GApnLO?|LTi(?fXyU!W)mLP^ct6^B|P(59rXvEpFChfL%hs|Kmy_2q1TY_2R zk403c2#=j=e-@L*TFq1cBp%?D9+AfD((jcE&MbQT&@ajBm1}I|_#t)!IVG>o&~~$v zookl~E?1SaEKM{@IinJoXxForOL6z>`w4fenSoaGj@kZEx5Kb{IMNJ#(-6nbaT#bg zamc%Ja_4#(36*s>MWyRicR!L8{q8W{DF%Ofuhra{nitWJ3O=x-K}zOOdGJVdV#xtv z^wZ=}@0jVD({i}@k~>YxOE$=Lq@TY%CZ&G><1ZRepQvXmaXbcg{PJi*ru*G@bMS39 z2cT2YuAaX1ab+4MeLe?6_1XQAO8NK$bhNy?w=4-5S{Ha|TRRJc_WJk%(AgD&zuv*n zR=HPIz$huZDh23|JolkXDKD8}h?OVlo1d=DUtcC|B zF|So+OXId#?Lw$E%)HV%<&+Cy4yY4Pu{WJcCuuoV0xU9{ylSO~umWVH_W%ONqiv@j zmz)XT%@BE?SG2ZNYO%`4aHWPFHISzl%>)#CNvoAJ1(e;$gd|Vny)Iec<*7Yeb7(_e zy`puA7l2XA1s^r%Sv>;69ALi{l`|hs>q((S5?RZAhnLG0$Oli$9~CgneIRC;8$am> z)U&JOn&2A6a3U|EYYi>08iYW-H-a_=AyCg`W%-&9T##_MA;jhFt229f+v1GUfELeD zJRr8?GNNc##hTMLe8y+&-e%uDBv^W<_9(x;#a#utg58EXS;?cPwino@ZaXI6H#sI; zx@f2%NmXx^K&1LXAQOI38t6GL8Gpy6?jWBY4V}AX*+nK4|BQ1DEeX_~zP+CX4cs>ieoX68ey`MU( zS0fH{b3xKxgTYrw%p&^;p7G#XF59aIKD_M+2SC1aX6j=EyJQ-k#w)yOc4o@4F=LzG z^n_%{igiPI>m1`Elx{DV-dZlcPkN}4X6z}CxQZkPWa;cO1Vml5D>&gAsb_#BdKpTK z(dz_eXO(Q&F|iQtgR!cWjJ zEBOqn#lf_0?5r0W^XzT)d)Fx(7qP*t`+kD?)dOi-(OHSR%d7zydU)A=hcvd0e)xIC zZGg57WL0Se3009S;fUz5mnfhJ;k1Tq%cV?0$@Z*pc z5a;o`tKwy8Azw5pT;~V%N!c5P+C}M;-KiTVf@hlgqCWO7?P?WQb}Hgb+!fac=y!Qs z-I+!GYaO^!0f0&+^ghUKlh%S|C7Q~MSQT=H(QOE;j%Lf7)Xdk47qx+NDiT*-CG^$+ zDQ0rch^uFhL!!;~J)hz|$KLnoJJlyzRv5?Ss2^CKTXzYVndI=F@^PTq&Xiq%Z=q0` zxZb`rdh`02XPQJ7$)1?BxX?HBo@{Y0J$H}i*3hT#z8MmBtC{(*b$a%mPw&H;uFx_G z2&L*m(0>ZQ!gO~UJ8|TtdiR}UwVs^5%IS58@c2J$O?P98BEsx!-7+?PbUlmCkcW0a zzHilf1b}w$Rva6TI(4FE=K}+l6lXmSRrE6$>*bb^;gTAL)sDb!c`n1YO}j^WNdUs) z9G$Hqz$9ubY19E-UPUnP7fwF0k~Wzp@$j@JdNsa`#KWVsG_Tp=JreKtzzS6~0RhCa zM%tauhpqifgJT&O zY@)K>Ij@2HLUX2;LUTVy#p!`$$P+J!)}wX1OTCYtNz#NQU=o9V5wy-AUKpXvEWPQl zYKVB68Qp9LvZhNAdydv4sbWK?zy&5d%y!W56~Wz+`nm&5S0CWt_^&jI7h;h0cp4#% z>0T#PSds>3+`;&eu(u7**?599niG%)=QotY>f0eM?~wLldJ>y?5t3JEhVvJnAIYo@ zWEE>oJOrew7vt3y+q*7UW6&q$)Fa(!676+B29qr3v&$@A3{QOes|=lXoM}u1Ri9D6 zu^xSd|G}n)lcQcj9NCe%UBs`_vk|ELnqdJlH^FY)&LW) z@8aK9+zGod6B2d0?t?+S`@8lTt2V!hwZYSiJX8ZvLl}J0X+~YfkLYQ}&*;!qYvb<= zk2l%*bBs5!HxddrOL?6u?)P8$fjO=Z$R$0_vfvp-AWof+^(X;zHOugH7?7k-GE^{C z3Xm{*z)?I#a*?fs0Ghhg`?gJ0T25zsyfD_3T!2g#xwxRxJ zn5rncE59z5D|GC&=aO}2uh$$v7tzE0Bsi4=*|bUowG-@MtO|R0L;4=FdTeyF(=DvI znOvI=M87#A4dey9e04iKf=fvjct0@5dY%?Lu5fM1k3465w*ALltAn~$i2~i3X`6KS zAG;tmF{$dM8_HYj<$LK&?3v2Esx+OwmD=I19Cq%0yOg+hG_zc=RN7TeuP9guDIJ*U zgCvExO*(5ucU@x#HhIkvUQLs{?P9kFN4xJFaIQ~8w&dHim7v$Hmv%|nRp~#v9H6sm zVENH2;GM?H9BX4&vWlKkyke57;rG5fGN8hL)S1tqZ?tx0&7*XkM}<3;I2G_Tz;{y( zB*+m3g7f&3kHasT8@g%_*IsgKZE9l`wcZD$b51L80#qGXv2LUTSDDjQ8^Zxf>D*Q# zyBD_hFpo|dvH$#@Pk7+y(dgB?+y7x>O7HCXTE7fKl7tZhYSd|Ult)`wX~bZ;3n2b2 z_Lq(J3vC}edqm6-HsvdzQo3w^7u!(hol!X$^VK@nCyk4pR_h#%-tAS6?DJpglc9?+ z6~r`V=$%USdtB~|lQ0zBGcgjSP7!W)#%aNmiveS91{VECq*lo zRd0>Ru4|==XNFC6_0DdsC?ZNJ?&}jBc$m6$peg5o|LAKZu-!`i-lZwxUfnvFy|Y|a zJC-vN-*flXjr~jYM(zMNlE?FFIl&%}v*>`OlNpQNVSp(Q7#A_iGRSe=XzHox)k$M^ z&d9w)TJB2uqMdtg00v<@o4CdpF!vV7kHjW^9Spm&4;r>3L&V^{*5nT1(_2ew;-X4mG+*36TyhAW-+j)U=GXSlJ(lXU_qj@z#)t;7m zEv?pya{)%p#At)+%`u6x5I_a#Not9@7FRrW8k~~2X{AKV$Y-)Pom(X!`(wX++D0e4 z&2<)ruqadJ_`dhF+{&ei8y+AI$#x_Lax`Ls~%zQFVY(U#d;MhONl-={!Zjy%ad)gU0#X&d-Xt2V)w(w=jvqD#wujc8+1_}6 zP_u1j2g>x#v6I88=}m(835WLb;3E|szVZ@seJ0SDU@dP_7gg;}=8l4Ag@hd1zM^ip zcaQ4E6-RwU4+HmBey(s;itg?9t-unoQYDB2kgYR2{V7PMp-X7uBJRbmiiR?^d?jhZ zYlGWfz?ITMzh03j#H|{P#TxkCJPPAW;7@iEKAZQ4SkBOf%_Z-M9A?Ax#3pm{#sjq%aV5`2%I<;YbK@$j3SRsd|z znhk=V>IUy@iyaQ)>3ix=vjI?KH6D6Vf}e5c^*uH$$tBM_cq+ezCA!%sUzpRT)n|Ffd!3f$IUHnuqT;s7T%B2)5p{JOV!cRkRt zw!3v&*5IA&V>^3uXCMi3Q8!-6*8_N@O;R(IB9xLaH;Vypsb{8g)IKZswTx;9FsiA8 zIlQ#QI@=_5O?XA%$F%^Vz6VwxkGRbR8pG4FG8s+$#%DMMjn7y(&_5pNb4}bDROTAq z3xK43^XA47t)hNi2a}9kyQ>%0t7>f$1D@(kAME2*xbgw-&H@s_ZI(*70@kL`7_3dJ z{if50Ou*V`VoWsw!L77PSy8gvJIjKWx8qqyY|gpMh`7F{p1=!_6ArMpaSUDT=&{YJ_w4b%U&0Ge+QZ7NJ^{7`R z@X4Bt$6Na;5&IrG(?#YuALdrk80&jPS6-6foyOVi2`TmPb=wt52>R4E&^amft5ZlY z)6Z7{rS3Cmb2!`Z(6!5MCbsWgmUOWV*M@#rrW_Dk2v>+g+?Adel5 z1%6N9hd>GPFJ%J$lsAw~y6oh>(g-`pxN5_Uy<7;s#)gNdZ2`zSy?w`PS72>(ha~g1 z$)_6!7GKdRH$J0mT79WM=6#hs@ZAEjSCF+m+l03&maC)iF<{;{@7;~15YSUw0Xj;& zK?pam*6LQw8MSkTz#) z@ys?;U|T6GWLZ6+$0j{G=T>0-GX2xUyG^^U@5%AmQDS-iwFU64n*jfFbRroxVa#vp zZCF%v!23-Lh#5YLs=G-7r#!$E_a8aFQ5@E~!_JGTe+LuDQ+PLexkQT(Kg06*T~~O! zWG-0YQh*RANt(kZ+yvs|9Fmhcwr{zED#UK? zp4UW`AC!8(VO1pdr6eh=c>n~;&?$K@Se8LTi2g}&Ug!I#B0y(?KlQLp19`arL0k|B z1!+cFuH#lVaYVUv&CJdyLG~W7vbumHH{14-NKGtPbOghu-0Mp_x%u^&H7kJ|tYw&i z;}9h8p%~*w+bz-BZ*?d(8 z=%W$cCbJA(kd)YaFSbPUDWata==heB(Hbc*;rDZ2lzM4q#(1)2gK(QpW{hRFiMj&K z#6xy#3I5P^=RVI)6)74ZV72)P{1pJ-C9)O`7ui?B?62=}Y0hQ2?C~rLxEs_gYI-{kc64FPVR+-I+aF0!9HZ94MG+J8?3 zJ#YL=Y2+;$5SQE)X^DB9rIm(=j2{^XXesZiPk*CUr1G>?jv2p#d%2RB9;02BB&AP5FQBTi}DbGyts)6!KjO0}s6BM>K^_Tcj& zX1xLVXq3mIJmaH5q*0Dt(V3oA{Pb|NQ*L)>ktfMFR+ z6rA}WJEzo>f?XB1SC3k^_JRPcZUQ`W8|P&sIk%?lIFYRdSCc2o9RLu}vtAWdE~^HH zdbOixE?o}_Q!0u|G&3K082Vq%l zE}wZ)MzeuK^VF2Da~y4CvN6rXNo1g< zAQ~Wp;^bBDO!|K-`QRN=gX<@mXmQ5Vi~- zdS0;s1fDEDvJ5XauGng&{Jetg)K_SyyT| zfJ@;0jlA2Y;%Tg0iMzX6#-ny8S$jRatfe#l;&HDLa?t7Q=VGZ7^*ax7+7Q~06JtXX z;;pM^dyRI6@tUJdt??md3N?2S?_!8D?(aM`${=>yt4A1sw!GQjCJp&@5A3nZ| z;gGU;#;153j(#xsh7ffh0P8V9wdNf270g8!+qFUb+fpf|n2S*AR(Z0{DDO5iv#*HC;a|D)+MdPtT1Bg$YLzqUU~C@(rg*52CXM}eS_8AI@T7^d z-U-o!R5$Z}=WAyR?XIS|3KY-M*G_`K+;Qicz=Tfv-g_6f6bLX(Jqo!soUG*A^a*%A zKJZ^NRa5jENnt!{^Rh#}k29n0e!MLAxk!I(_;|AXcGfEAGER<>AVO)$x(AVOA04V{ zQV(pt3$WBFf=Znv30I&Yrx!=l+qQG*c8JH$#x*ZZmo^!+%MR~bTc9|V$nv&hbE}xu zu+~bR#VmI%%$Atmi#4Ph+EmFNQ=QS1cQS=gw03K+PDI4V;hE||kMRw&{l#7bnDGtBkjMCq-Ypc~T-CH>`TCERjy_jr_ ztP!w#3I3xTAUOm#ZeyjDr;(*_woGuMH0 zsK*)3TD62JvW@ksr zr9fz@X_5pMgxgg7C@FCA0djj(&?=P%>6AvuXNp%SIXua&cJ&#}64%&&ZP|e&Ve`kQ z(rQvh%*z9I?wR>qxrd{bv9TS_8tJF(wjpSCnpWzyq14!INAt`HI`o6H>c?cqo2Nui zJ-cp+&d@)`%_g2aD#$2it+-BQ1<}0Lj6K)xy2uBvwgdNC#W~u&L2mBaYD4C3RM(qD zq)6CX9bimUZr+#UxM!o&!+r^1W83e|LC5Xb+FdNZnoIS-ayqkq=RF&{ALw3QIYg{l zVa^bRRhN1x3x&D#&P#GvNKLMZ;5NU10o_n&r|>j0`lj@SeXA}${-og*wk%!vwQ}$@ zJ8(W-$}ZfrtR2{0BH^*ZAJ;st>@!o`GjYBWX>R90#~<4qZ+>_*MfV=Fi4KkVgNa%d zD~Emk(b=$8rMY+X_XfChkHaoaUeK<66sywFG!@GnS@qqi69yr7 zY33PJ0mCZwr?IEYCA{?Aw`RlpLSNp#@ZpF-YQzrr$F#5HVtBTGDo^mp1j?bcTT|xB zqTbsVU!On!0$Wjz`eGvFY!3an=&{(nc;N#8+-_L=sv^z()6hrd7VcGS;z?zNmy#>6 z-sAOWRqe032{B5#=L5`t*|j~d#i-d=l#@ z3cFam0X@XAO8k28==y&ABsTA^^o333MxPiXvksp=Ws~JQGt(&=J_W2iOwN zt}~6lZvo-ik_jo_;;td9*j4B;jH`#ir3^J&08H(dW!YczLz-xpZ@V2uiK{e7B^=k{K7$Z;I-?iWF z$!K=N$H5l|%j)+GEVB_|JT7E*S0XGU@|eTNnge0)W-npHY<)cngMn8CSf-;mj=4zz zfOXhyU&vbX?I2c_-{>&C+%4T8S!yhI|`id-%?K>Cj{5ExeMOd2OL5;9>)p9vbwMM?m>;~hrDv|GH^zvE_ z)Ez|{e!;8+++P2T|qbB_HNRSaXBn*ciFH-J1y|YP39(GOqLB=kApy~ zk}$_9U{UOX{6p(XX*l1)8n4$`W!q@eDz8x_C|>FhYb$o>diY3_UEw(zy|$*kdczUP zAm^Aw3C^z1z2XjUQ(_Csp%^8*tTTOVvT|-L>Md`}#tSEPrFUENXr>O1Vc;pI&$8r$ z2eNa$GY$d=jTcjb?s!vPh=US%AMKydONOoM=ElcamCbmgYjj&Mg^Kj$r9 zy&3t_5c-$z_FWyQ66D2p0a#i4m9jZjOm%wfZ!dHOQG_emr>_T1rK#;-GH_Nbq>?FY z9nadbl{V!u<#|_KLFmoS+XyZWAz08Tw|X;{JQnKZOY+OyMZVRmhvK1`9N^+C@&6_#?9fMHyqmot4RmwqJ@u~#)P z#Q$+^`krQ6-4oMSQWpE)Vd#)8ycu^Oa81eEwxEqLL`*mC1B_AOb;$YS`)-Gn0YE6L z;Id=<6+7%qb2NyZp;yX63%1$FiYvO3fZoz#b3tpctyt5Y_w!i4xqW@{scDda5Vtib zVDMRa>x*9X@%lt_!$fKInaQo*Fn1QWcbI(C;NQBIY!5r zJ9YTT8F^2?%E_xPHyv~nC)j$34l(Q!>n%6Wz8c|Y0~1zPVaQkJ(*x6E(>;t?-SX-q zEHh-eQxC(657^oZ2c;yiPq84sJIMAxK|8JM`b1q~Pp3_ie88gc2daoM_SrYjL4=ce zTgRTEm5XZ`g4)8udN09Xas4vDsu2M;O$2bt<#F8xa6j#-{L^vcXl4KRDInT(O2&;9 zWSauLyFR-W_xzeVuxX1yA~7PC)28CJhvUtVW#;EtMJ!fA{<0!D-1C@F+Moehcr3R{ z6bOsIs*BrHmngFtxKfsY%>%n#1@;0*xpLvo@~l-Bgz!tGfYsCIvo{Z{csWzW4sylx zdddjBu>+1EdDfLPSSw>+q3e)Y-K_Y|vQQ9pfPoCqiJp`8YjPZ%sy-2h2F>3Lw-oFE ze&8ji%xd=@T+Oz8wcFthau2L-dH@r3_+#XKU9hO+BzNBJM!b;J9fSMCjebERkw=8o9YR5#&<(FjK+|%#7bZ&@w`@Dgd$+-q^8zD~M>oR{^H|t>Io4_de??Pe zPKf}ru5bUqjLosvsh*2_yDI}rPDj|oKCu~i)Ne5HI}PpU5>zhr-B8?B)yn^z#+x9P ztjbkUe(}b;%?8f;57Z z!_Xid(lJPvlr#d;DJ3l^F(4q_HFS3isB}v=DBVame0%Vm^SvBQ$tfpME_U)4G&#$*C%PD*r2*3sjI4j=(7xrjtuUu7^f&4}q%-w*PKHg7 z+a<9H*f`_vGM8Sw?O~40YJmWFV{eJwT%G>Yc(%|*V1pU!S zrFU1GbbEyK-nt*!&SdP(Rnj@YZy~n7DD^wPnAJEg(3h=rg-VsHLtq`B-yQZpc(^u1 zi&BlGeEYWj)alV719d3rr$dYAJXVvAMA9TzOr=XUHOAy% zrVaJ^=BDfUqh5>EaYL-p!dC$gR^)&6H&QBAs!{4K0C7#hQt7!!2$n6$fd7n0t?4~H zEKsk;{kfy_2Z;i8OdPrxbPIB@_s>Ev|0!q+1z|l!LA=0t#W4nhpfiKyqYXe(Q54OH z?)cxLVMwNEEX!-3VnH0JWGV@l=4)r(5Lz^%F?2T7h=3x}KzhJ-SNAenII$SX%Nz|| z`A-RR1rL-koemTkYyRN^lw?A}oRV`L%0UQ3iL4-VbSH2#SDe~zf?t#X-4=>2!`}qP z*%8f@fYV+9GDUGOvyKuZ&F~$PX1I$}9~jh;gq7U%4l^^K;DK#N(v*`}KDAX6FBNdA z%d7L9yH}pAz@-qv;ao~EDhEZQw8zcy93$r3ZJTuItfs>pD!CI_Gm~5`qrv%hC*LVQ zVr)=)k_c1~Rx%KLA2$n9D$`aiA9dZG&Rv}HD=<`azm;KK8qRXlh701@2mQhw(0JbyC`8UCv?dtHNjD z*|Ndlj`Z?w^0T7T@9wj{Hu5?Qoj%%`%@Sl#%3>oEstW}JXK4Q%9rDc^fOMmKGHNArgK2VL(s2&EmnB`U3{3GeF|=EkM}w zowgcNoboQ9M7a%&q~GzyM(3ZdEdU7d`WD!-KeQs&o(+DFlpHS9;s(^~dvCfm=seS z%CR?&crb}NoIL);EqBlL9pl5gD0G$k*{VYB6ROVAVY&WKE5I3hEQS>1Ev zN!38yJoTO(E|LGyk9l?II-dgq$M0q9gAZqNQ@{E>DV4JHUo4(C@bDfJBLbg{<*Gc! zH7OJxc!{qP(ZI{yUUrAw_UP!z#|I{7#oeAUFLB|T6+7NeI_J5e?HD^jQt9^$+QafC zV^0X1>G0sQO>RN08%Go}NBrv^yzc;%9h6R@umo9UH{v%RwMi4yDm5i=b+8PK3*x5G z6G>6I&4iLC1yjlrIm9s!3D$B_^Pg*U!Eb$=t%jo54f02~k?!T@Ttk1`gF)O6wk>#7 z)i%24Z=X%j%Q2QXjhiel4k%_Axh&-wOxM}nY2D8d85-4zvIsyK*vvJwJ!=z^mPO<^ zEY2HJ#dGE>leHIL1$Ts9%gA9!L)UrrQ3l)JS*7z0mG&<7?_A7@C)(wyvsjf zkb<>l(vF+@`n1D*i6AzqL>9nrH)x6K9Y2LW=#XluMJAxnGXS+^zSxX6jW&;nFxezt z`#*4FD+NM{W8rZxHk@{Fz*oGjh!~_~>PZowkTN@Du{uHbHYlME`Lf%OJK$D6R&DM{ zZ&)c&OLtBO#nc~(FhhWcqT>}Yt3On%acWcjxL zHY4Q2Aby_VSF|BLgxtU|46=YSi1#cnx+^$$e+;2NFz7eLL(tY~!F=HX*W+=+IdirN zngQ9W?gea}Crf8ZKIJbR*GJ;%1WE3WIh*~c((X2+M*;E(;>{u287_RX%bu;DXLoMQ$##=u! zR0&o614O_7WEJG};1F3FU&`jOJD|`U&R^|bGVJqBg~O&E>4_kqox^nT_9j0&avRN; z6ruDl5hQ*Jzf-bU%oQKWCa^rrE^30cNDAJOTicY1VDb5O{|$fgdl;g~idesQ@01?D z+eVQhJz;gA*^Tz|^IYkt3r%0YV?zcSb+o>^%6XD1b9Ui>&G*tZ7;3aPFML&OI`vH^ z@0oH8*P-23r@GiANh@VmpBZ+fB(y#u&falD=tc2+!#*@{-alXXJ#6^NYv&5cS?CXC z&l~bVrmN%=yj#bUrvMJaAX(h|!O9&x)FE$xelAb#v)y!qn|fw^DaPH_r|67reVNWX zvrj?&u@PN;{}sV|>&c4TJdc)CEr0nZ=(Ms)3-V^NLk7PuKovIMRa{f?M;awuBrFdw zvr^H-2HJRH2am?|1m}N(sDC)b0!ZN!?iMH$8xg@3t}D8W3IX zq7c<_k3r}4SZc*&g)3PvvJ!x-W~SDWRNOujr8QCP+=>31H>WL%G%HIbN4z9V0yF9P z!(JkG>$aDA1{mUqJkBS1!`w~ZQ2LwCotxvg+A&(=IiGg$$jS0C1C3bAdm&cCK{V}} zV=_>PWOd>6IQh*yNOLL$H(kVg6-$X@$7rK_{XrkiuV=zz-RdOx>*PS9y1?B}FJ{`T zhI8E?cN|!#WJtwG)h03G!_}CQf`q0xzke$~mLr$|;Xi8=CF#?jJcvi_Ki)Rzp18&M zeAcs*e3A=NT#5T!Z7QTA+0-}#_+q<0j__9z9pR~N{+JYXHWz-&#IBcTl{g8XKA63I zL@ubZQl{6m`>Xlh(h2+tw{&@F#w~hEMNI@1?T0eaF4z;FbyGUGJAEwND(HWQBR{j`7QvDj?GUYG5;gM_Sd7m*10YWTtQKo-U2$< zD<##S64GBh&NVZ0@#%F5n6Eqo5A$Ov6uc4og^;#70BF{#>O?YX-NA@=R@||ARjz_R zJc{nJ-(a5|n*J7^m}l#B$SXHoWIUam+*(!1A^}CuBh5OqBjn7Qzl>+&qz)!d_0(i z>e;0I*7oi`$q3Di7~su|fjrXvQb>4b>FaPY$Vc;dMKh(N&@0|m%{DH&Um_X5XKpbB zH!Jf9d8m5S@D`}L)xwhn-F#xMzbNjXaB84`eE7P)DSU!he%9c*?anuyj8i5iuA*^c zrTrof1z_s9a$BHwvl?R?g_{5P(lEJ}|BIXon@`^TA@P=Sg!k_9A%l?=w$5|U)r(RN zj<6i*;JbOLY#%&&w9AyNmaIvnp?M={4y1ADb$YARu6rlmXR+UhK5BGh@TC!@fL<#t zhTF4^X)g=qNd)wHd=FjyLK1unp6}%y(;73yapPZJ9Z2mAo*CZSb|5N0BPO$;>P_Op zWKI@R-q{iKyuFKap}yZk5*Dy>XSA=JCN#3f>EMU1*qn!-yl}5;??^wc1jbjr{Fswr z=vhjAXASj0k>MD&&}$sM1!MH5&M?DX%DbUsqJWBB?648y{PF!lfM7vF!%7MeuJ9-! zU|ratVo&Vv{q{u)W~-68ZeU-h#|qYr z={+iSvFLPtYJShN$GPFxeoPi9bFG~_<2q)^vzrjM|ka&H34(?>Re?mu8 zOGN5tS072}GeoM|#EQL)IAW|=H?K|*?B;ESPt@&x_6WcN9(VrGI6eyg_I=d*&I831 zA$t^$`brdWM2C(T2(gR4->}llIWN6pE`$p$x+M- zKRX3w-q>Swx*?*UVsLMCf7M|bU!QQvR9GwE!yt>35Jght(oq^yYc7CnF@3`RZZ zNXY+-c-ZE+OS=o zrpu5HBgWX^I2utGh79C6GL*b&r`E1@(%5G+fa#9WD5unX0+{B96*DPOzWoQI&E0CvVpwNbf&JdQ*>qd(>6f+@ zzU^0UJCgP3r@!z?aLdl)i`TU-TntF4CCZFCEeG!eSt%rNVSrV!H=+wRAy&t{P`5;_ zaX_6T01{r7dIbUjX2sO^XC^^N&MQDKLLz7seCpeQn6{swcQO+LNtC+_4Zdk0^mnT` z2VZK-HR`TNZYJF??%Nicc!4i4iGxmMmQ%;RZc}ITJ8sHSePzMP3`xIw@>CQChSWE< zD}_s=Dl}JT-#vDIS@#Im5RVZh8OameuU96U)PGD9njG@-nnBS*8cNLON|DvKcS$9= zQJ|DWy*g4!Zpo^kRnSlq(35SND|xVU?yS$Nb`V?Y_}uQ5?b+6bx_N@5g>p|%(h?x= zt30(hYQBONAf4!N(}y?WnLpc`F8ywT6iuKF@O}BH0z_o~yu7z!XFCA1Q~;3R&2|M= z0|b5Iv)wWO%alpK>#=;t0chB)R-e5!UV59;VzdR|-5wY&t3T3cHOew@(m@{xA0RmX z1r$^P1pv#fVR6`uC)r)PJ{341uuL}C=CVJEh~ zFd*2WnY6I#fxQtKcUJSMFSjwtCPHzuL-UVarniIddB9=rLa#4i(pYu2r~F6P#Xqeo zat7@5r<6uw^lsFt9iM3`iE;a_MoXwfeILU68r+?n=f|1J8vJz5omP{xnFw@%FDnCI zW@~!$d=M9qKaHAHNdY%!as-aeZY;Y*J^#S~Xdr$c&v_xC=AV5UctNyeOnQwZpi4uE zsW3o15Vu^`z9BK#lYI4J1faCh>VDw?TtIKAR5)$B7U-#gK?9Uj_kh;6xj>^F$v8wR zp&L8_>FDi7fo3H-AVsJZ0|A}s2_+0Xy5EbPxFdbby$N9~uSLZY0OL^=vw>0~gaR;? zV%grH1Aa?^U^Yv43eahU0!a5Mpdq~~(Z7Y{%j!0{pW-;IuzExwiH=^m+Lj*-n0MAd zPIbq1L?Mw!>Cky?km}G4*&^Zp2Oy_K-PZ$;vJ{ZSKRw~b1VT*i13E}p%++9+8B#C* zcWz|K{g+l9Ty_M?=7bIS5I9~JHVIp$5fZO^GQ!Lu3lg#~YQym#LaQ?Te&p^Iv)$;jEyt z9Uxv1huKaW*%TnnUVr_Qtm{&#>*wb>hs`>ipo2c;oppFn2b&KBN29HKDij3Bp6Y** z2kcDOrt`)Mc?p`26>a60BO5KK=Bstx`FgJpu<7&xQJMgQ7HHFI+>VQM&cS2Ubu9^i zWf3OlY5_-9=ltM$yd7L>*iQetorg29i24PPUMgisVuOw#1faB}=JNyaqC~=ckFk~O zY40l88vGVOm$IDWnfZiC!5aklq3?Me*Y#(=xQy31G9cC0eG8XuZee{5f2&X=W1c19 z6y2y$mK)x!57RmFPay&njeI?5o2`9?n9?a;Medx_l96R~RQD;FV>DXw|>woOH6v z)m0#3zfBbstyV0fN*DMbrZ0aC#9gD%cy7nATZ1(K4RuBIkIozV$qKiYMP0&S7n`R+ zO}k@Rm0cK)^F8;y=xn^y}db-7iYs)Hq7- zI+OUg`%;98QHgp5+PUa(MhdkEaj9j0kBIxA-j8g~0_zFjYX1vAJ1p;p+K_kwaKLuC zhsescink0Kr5N77FH$@gbnKPvRQ$WXrer*|7J6LTiw%V;0^=Q*w!5N0-db8 zvSA%6!^IO=0D)$w!%?a*o2jkkwK}Vrpj8{EIiz3-lg#(cIOKHu_VD1zq}F8zvDC%1 zO&yVV{+?H4Tf;p;FK^?;-Y9bCof+0Igt3xFo&1Ld$u$9t*)))_rJgUR4*{XA`s^ZK zi1gt@!3-pn4_Y^{I)H{RZ4)Rg$s8_!uQkopBMgswn#Cxl}jDWqRJK**<5X?wW?qOuF~o8c2dq) zt!8pu$|zVea=E%Uir4_L07xgnsaup85SMc)XYu_bf+YL>#7Ao|lK}ho1r9;jHQq^Np!8 z6p$F#5wCD$)6&E`lRaLsk=Y20&cGnL%<(_IUYm0|$rh3erRy8gzhaEPGjSUonF?#i zvAy{Y_K)Gi{z#V5mDh?;%_dKQy_No*e4@bTcpvntVcnzLY^1n zi#2O4zZxXl!*Ww4TX?10tVzivR>)^O3z#b0j;%SZ7k*C1s&x%lfZFGYMZ7$1pt)@3 zkdN8hX*xN3Vsi+wID5zdNJSu|IIGOh9TDs)y&$!XI1K8-WA+c8;?iGymlFTt z)6l(V5c9~KD;UJuO6c|aE61MA&V5xKgqXympP_eEAFhMUib{m51kOv0wPzhdKrWy#Vb(M3 z3R)xT@Bz_cTgmHY6`iM}q=OKOuz+o%(OHcf=@Khxw~ZCEGT_*{0;oxoSe!KLt$7GE zYuIk;UER^U{?f-B?V_*!=kY%rzNb|z{mkJ@>glgMSI+C8h%!=TH8s5QDaGrG2xM4F z#m?K)xs&N9OU@=7TkfMu27iNzI+(S5#5w>=G&m|yez6;n0^lPvQC#t5usNLOr!1l$ zrvW_)ejBqPQuKiqi;Q&rbqQT(V1NSG{PmXyhJ(3k{s$zbMJeBPymUp#B$QQ1VwpVaMG2!Qh-op{hLcC956l_ zM4=h9cJaM)V2?oL5{jL~h8`Fd$3UqPMPKI@u+(0ug+PW=i;L}j=xHGt&sug=k zC`A3!V^;0;z9GOB8ipCM+;D{`LC)v0x|BL5cd)uEHf*UfLQ!N|={@_!BOP7BqTb)^ zq$`@S7?#l?B(7#-22m>7bSW`9LO)C{b|~MqS1kD4|BUQSeiX~*E8{#{&TB}e#SUh( zG^fotdeWqWNXKVEuHRo;-{xqFX_^yT4c+!TzBAMZC;-_Zbg@k((xC|)hF{KVO;!X= zwTp=k`iA0>nb4@>{&SGMZcMl=!~x9MctnFFc8vik7|9io9ZJjhepDE*EV3gB@YPvX z*Y(ps)<-b-&EMI;AsSm^TXEH&e7uAI2cw&p_x2oWdY8hQ6oCZjzPW{kkOx*q4->GN z^hA?2?--FVkV;4Il~ypDkQ9T3-1aV0;f8SY90c2J{^1E^>0@#&W=((|ckOvX7zTl< ztZ8T}GS<1A-);1|_A`=-c+&DzD|KEk3@00&A5X8I2WfkhlF?>&d_@YCIb-;Gy9DXp4lQJ2jGF51h)iJpNYRc1-cbcUcy%h!4&Abn?JE5sV2 zpiO|hyXwDpe|tTWMVoP7H0vXUg~?+EFD^&(0+fkDtM3+lL;F~akgzdrYF$1hF!nLg z2sknz-veWRdrCDZB<$>fyOvUEQ3>=`Qw9EjKlp2vsStcrGDG2gu_`=Y6w0SQNDMlyGB9Pv*B7H?~AJuzJgAlYK~S4arUeT&bWAt_whV`ddwRtYIecK`^c6>eE{&Ug=o|_DUVYNZx@U*`+=LD&Z~3;0K}fFicB9Q`uZoON*nu!8rp}~`rh`|wpHenV%_3!QOvAAttz4^irmu`-s0q@8~iBD#o z+Rz583D0=8(Zk{~@f`@dcoqa`Kx#bBt2+A)+CJry2zuYqfo)AzHiJ5C6uT)9!qF;c zefotL@&-`BKqIllVq;=1%K%U@dJV+TN20V_t}I9+-!lx$MJ~u?q$lp*sc<*}>e0y9 z#)Ve#jL?+1^5Qu!U(S2msiNO1s;pjV?(?9~JezG+_~nzPB99IKL^u{Hx#+xGDca`> zqioU|ss8{hEs_Yk9spuf+1JZ}XO{X`%gNQH+k5LI&;x=3B+k@cgu9pTNhCcL-W{xB zgXGEwG!gg(nzA4jN4y^mHU19LHX;E*R39_}tZ~`TYQH3lyg!kGT-8_U5o9;f)F@KE z$ZKuw7F`1;sdfNS&Z7;b?mmk0jKqn1MgA4igaZTn$1G_cw;siXZDqX<*S&l4Q%IP`sp3J;Q{T3@Fc_1}Q5)jOTNH2J+IvL@7Yye5e8i zX(ra&mA+um*Y7=@g@d3@gDXDJK$frAW?DT$;_Cj48M0ykXVgRPZQmb=PQyYp96*y0 z|NW)<;*zV+HSkTbF?LM>mt8W5ABNvxZHxG#vv?ssoPVK<#ftziWl|(nN)5yTXmJ+^ zr$xbu%;K=B2;!Vp7VJO?5W}<3UCAc%69K6o2T8Y1f6aN^%~}RcJg~$2ff(2p$*Qhh z$Xg>RuYo(Da4dk2kGmz+{*+Oll&|f&)R{nES;dCj6AurUQMlx*8r}nyNR`qOE!L8f zeH(+nsshGmW@2dx(tMccsR{0)8F&XGTAXK0n)b_f%2@09z(4zZV@r3w@YydXo+oO= zC9Jq)l~&qB(E8<3qa=UTBKNR}@Ji0|86m3r2vsU^6A#ZhgJM%&ZB@^0g8qYLfFUS2 zE=ohn3G@RAfS<(Fa4=XkZVGuBhIa38hkAI6P-mou~N%PflghR~=8u z>~xS1k#ouxY_;GB4U|@>=PjZTiF8YX!r!vbqe&swBVpgv)Ft4;?@*!e?}oX_Fkw`L zm~c2PuZ7ZR7;SuxLk+&T3U5`e0A-*imxTelNpGp#XcQwo`KhVSntw{xsR1=G*k$M6dszg6BMrqejFYrl>jt->nubjOWApg7!A7 zTK1V#U4kLz^{K#6_z_Ta2^>!I;SOD|OUEhOW{eNsV@eRkH()k-mftwEv9Y{`!E2P) zdA%R-DH5v06CPkNu;f~=Q4%$x1i^X+%x&wE#|>_6iuD;9*v6}3i5EzXPS}75_`XDx zJ-eJ>BbWe$;)z)WwQBF*LzP%o!(k(Ia59%g>;$$qfutS>1K$XEzbdgC_t@I-J^{>u zO49+zS0Y}hJQ(YI#bkUwT8lq;qx&fDmMp#;-t1jPJf(Mx3*!+`B@*?93;Sp&dYg85 z4lmU}?Mku*fF-KycN+P|Mep&L1My_lDk5mI#%{r~azyQctsf>F7Yv!FuxgneW%nsC zeoAE@kFq$fZ~1*ZaXid+o+CDc7PJjeo5mm)nk_z-}q{;zFl@XTL4A3)qD%wX&z2930jxVq?{janA{_=eb2YtbbVFM-pA9N{BHjK z`PoLXRS;8E389|*z;cS0C*SX{x3^}T=PDHhPg~Sgr z)xSFJ=JlSiUmkB_zb?N+%)z);Nd?c7!NK18fChThe7^kM{AgQU6ZHE`+?;UV@3rcr zim3oJao%5IXXD_IF94gV(@K)_Bv>&{jB42kxLuvD zB+<@4hDF4q2@8iyo(rSGQVH%$;*8Aa5wOr8;KPBGw!>dNf;NOr+N1wcf^m@P55rO; zQGKKl+)ZRA4LgmaN0ws)eY3BbX?)4Z_x&*3JxeY*4gH?XBZ7%1q-LC*+B2$Bu49e0 zbUmFK$nD(|q8TRQrgqF2xUF=5aLk<_G4P#)qpaMhe`2U8YT15*xP4Hde|*?1`Okyd(4v)bBf&V65Q zvLCeHH3o%~zS@vA=m024S^ASYm54nOsF*zk7?RVP5$HkSShhOrwo}0h61zKnfJ_I*Mrfbkvyj^uKKJf*hXbpnZcWgvsYqnZ|lrqdn7;znqA_Fgb!9O3}9fcIz_z=WWg*ezg9ble{az4TxH zVy$z38g6=xIb#`P!CX<*U0vpe$@d(KiCn4z%xZJ50n;uFw&QUem2}?X;TQ(1ZaP8) zz0maR%(M!-^JJo?6F2EZ1eNq{yXrGuIBn*=IoFYAo=4bx&g~urhittYgT0t*HBnaf zt+n#Hj<4Rxxn1V>Pmd=HUXj$lKCwUA9CzMKqm(aKZ}V7eQA}7;t2loo)LKR6P;~ix z!NzUtK!zRt0od-LN~yg#^CEaD)WMd+IZ9Q)DD{U_!NDBmR#g*ieL~mnl=N$rW4iW- z0iR#sHRxK`E@?}XK+Lu($DCGdswRvbDZ!Q*9K_4}>m#nVv^I4d{=CYtY7{U?Ubycd zi?}fA9yk^w37?TNqChB6e&Hw%tb;?QdJ`2r3^p@m`SWFez`DrE52uE0$hm75^w^DA zFT8k@N~vyu-&2-ct9GqSepC_l9B$ZO=1cJEhf~uPgvNb#)jrP3Ys7`iaJlJdeJy74 z{-arg;*j(47m2Q69{c-?V3nJf6O5Yto~hcQPTKkrGNaR*vEhedR~Z z|Ao8YsGk?tD8`a+M8Eym%;vDh>VKx8Moc5zJ@;nvRwGHfnJC^+RBSAbl33N$>}uzCQA;91U2@qBx! zXWz?tmWO>yA~k;d5mOw}V^FEUaTOUEf{XMS94y5=jr*y2-Jg&~Iky^Yb*jHIkdY^? zH|?P0TI$c`+8lb6^jqWt%FPUq=hpqsmcLG z={kXOYkQ!1vr4ypD$QBfid0=|s%XO{gNm>y3+srJS%|&=6h=upX~!^i)aCh%4PL>M zfptk*--_w9RbsMlw%S=rF1dwOEl|#R&Uur%l5MCKTJxZshWQjLOdy+B3CrfOANZx zx=wm6M`*EWj&WF%*L2Bsso;z;*oRrWPtR?Ab+LE-tmF-W4WksEzTU=Dp1(kM11*#~ zA%psZ_!_if4&*tlt1AZ@`%FAbu;V$9FlvXSFJ^Goic1rym& z{s_yW;nBm5dy}_jVJ~{g=jsY;i*Pv%8_!*n9m@Ybjqy0oe$yElN_z4&) zd~#`alro>hcOzeh1e@X|G`cMQs6!dO_+)WdcPY)6Vd$*xX4>JuGoF;L`&PWz42GXn zwPpP*D!^x?ea5TYs%DVhsr?(x@ZnhfK9^GufdGhBw_OBK@1bTkg9zwYf-y!Xg1|eT z21L;NG6nZR1bs7JA7MqQNcfmnx$(qtlYHrNa14hzitoVH)v@0BQn66Mn+PiQMk~fK zN6(WjLM?+|;@TBTkL7?Z=+>VCd1$7fdDQ#uP)!|;UK!MRIS^^hZWK=h-Q2t-K)5uV z_E`=T4x-VrG0y&ZU@81|1rzXRSOkbmImX1(aln#9uF4%2sR8~S zc#iqwLOhiV85J$pnX3U?BY#*P0hiHF5Dk|MryZ9cP_hG4!aF8SJq;4J!U3zI*Kwo( zq#rjw;W=LgD8M~cevS^9v4ZRYul&xKgE$2>2t5qSe#8T&l5=%(?p#3Y|yZGeX5=c;n32x)Nbc$XxO6waL~I$rv1FpBxc+ZKz8F#DzY+2^fVD7!o3aP14AfII^o>TQmT>pN~`2p%$p*}B~ss zV_6Q8WYDBtP7obZ0AFrlvy2B-683#euZF%9?;rDGdo0qFQVOAca8Ud29SV_m7>OGB z0;6a{bvKVgmui29Yr1ZBAEtxj@?6EVLXovQx#vZA&f{=i^*_!DBK~;bX{!6{W}AQe z_UC)I5XOy-7Pe$2kyk&Y1JfMJ#R!-t%};%XCZ!c*)sai(?*%i6{TWJ6s&#mC(1W*! zL9u)T5ILP`lFuUVQr)bSoAvUB8a!v?pLHP0zHxP!?_XsB(fOb4UoAvEMi0t3!~U;( zf5^(2l^#PcH4lR;yc$Bh5)xq;Cr${?4M87C*lH^e5Y-2rzeWKD8{4Bc1iq5wKb8Tnz4wq4d1Rs^4o(#q%1oVO zImpnY@q9Z}bb!16d&8R-(6A%qd`7TiZ5mH4LCIK^+ZyB5Kac3|jRYLBz@fYH(@Vko zd&E*f9cbGG2F|Mg`)+6-CBTLJ(d$y+@jgVARg7KQF?OClnu;tO^FWVV(v(hRV$)si>$Y*ZSsijSfN#-!exsZwv!YFtGa> zrE$t(=iEUqu9Cp`4}boWhB`R1q+Hl2YPQaQV{mWer&ln8Ps#K9%Z6(~Ptm15?w^O( z5{%s6)U@}Ji`9D%Z0Pht_Bu+yEW#*fV}mH));Rv$B%T6zC~kL-6!LNWPqJi$0>c4d zg0|w{6QYO)pJ!0J8DD`MP-GPN01dkhga5It;$tw!?=@?Te{TL0yu+t_FzkP?VFeSz z7b|{%Jg9^%krOt~ zx->E}{jcr73v)j(jYFn4Tp}%&v zLEyveU}Wo>4ca~({VxzXb)%~3_}-exux5jy;1n(#jI%mgnBcfIsfx&XF{VEBC%}k} zL4Md~U@CZ*q;EmKi52Hd>>Nj=e>Z-QPeB`|^#8^%7}_3o{{u4aQN@3rH&%E^mcTbrggzN?EKG;jV%sH z3}75Ql<(AeSKzWPY4Cl|${vU^gR#~>@1h4Dkc0AL|BF$b`pn9NFN2X>5+dHbpWl$n z&OlC1DgxU(9Cvl5elbSU8O%OVe@q-W;&&*85+1eO;h)VOOBGB;NhhwQX0K2z(u|8! z1CVDbe)$rBhr^<#66@YNA`iGe$Dtf##vv|@pw-++8@Sm((LTxm2i8u0eMnG6vA2+y zD>@nudu_#ih)W2DWT#=)i!>Ao>Su9)8VzXmciqw&`?J&0J^`~SOT%66-qz0$~~Z0-%N;ZQ5Z@d=1iKYY>; zMp^2@8MqEmQWXJ<%rJ0l4wHEsNww^p7K6~t8gZr0{Q0Gqni~fPd+u3X?KmM>_|i`(r$seGaw zSIAlt3!6!YVp)xC znvzz_4JFDe$K-8KcH3L}3AB0&q#|sKH7sqSB4O>A#Ddx5yBqF*hKK5HAuruQ#rgM= zM@s$JkQ|cWgLh|ZEvDgA1LR(@0($`y zJ#p}U0?iJ~eu0%J&dm7s%j1O0Qxn87CTY*8j`McZ$@7#UX{xnZxBkr}XPmKdcl&ZK ztDf+&#y2wMq)xOWn-4as2g6rSE_v~}Y?J;ty`sd=^>e)_=%DA{4n+hUDm<-s!mf7K zDB7Ol+Dmu@j>rL+=`73n@8+}d1Wxv;H!-O)*r1!O6k=_tHz4|Kr>W&tH&Z|jwGuyU z|3^GG_ms;@?R0p$I{>LI>$kp}&;vwe=rjPN2VFsNm;}h(z5+N`1?!*HD~*iaBOS_* z1;EZq@4Wj^y(mv~R4EG9X(qqlYr?g19bpN+q$tHtJU;rGamE!ZiruMT!X$-CaBH~J za^ekc)Ac$7JM@m#LU6B|X6NRRLa*Q%?Jm1CiS4%Y92G)4z~JmyuN<4QN0NNuIEXQP zwb}j7 zatVEWjf**?2wzNr;fJ#v<5y=E^T$(`5kJMurx}W&o^LFJn3HRsnHe-*yT8kphP?Cq z?LDC&L{(J(6$S38@!(}Y;s>4-p0DbU$H-}?@_;qex;r%SXBAB#cRgeO$bHr0)4mdO zm=Ye5W3#-Io4eY;5cF1uhIXHG5SxC`W*Ge*vL`uXI~0nvyf+=Bi&wZov@Fz+BT@)8vwZL6!QG_Zw@ORu3inUH5xi zRMmASEb|L{bUir3cqO_I)!1frU$~M7KPP{eI`M!xHkdr+gecd}7FxI!qldGfp@2xM z2_baNNDqsSeq=KO?b_&8=7jHkS8cc#>bdl0qjHSP=_1!-Yy3J%c>sQKzVqjDtn8! zxD~&rxsY7Eso|_k^QB#9W=v@?p97}d=^_B#?zXSk3-keAQcK){+wX7i>hN8svSGXm*=qX3 zupD-n*!SHw&X-#`T9^tQjesH;5;b{Zw#KF`}&y-6y*U(z1Rhr=Cue zsr%W758jk;>+yXRyf!LGa@!pl&FBpGKEF7Op{p`WAlN>^SJRx(eSDeADcPkXuvMd5 zHO$t#`Kj?X%5mJ7cGO7p8_Qc8L%aD=5UED=E5}b z#N}`>hzz)1iymPtrR!WMWyGF3aCk;*R?|j6A#82goTv8TRM=+f8F~X*vaD_wyR{2l z2ancZg4jfnli)A&Z{|F3$6i>Us`VT`eqxvr)_63m44;d@sXiR7wNadfwm(@i?cgLQ zT^#B^abaV=>am|k1oah~M9Sb~E)+^4rY#~IK*?OUChzzs5&ISEVvj)(XI1MH?)z^b z{#(@_8rUpzg3o@sSxoXj_3+_fhPxgLemhHaKIP9YXtWfwsO(xl`ktfU;}EkSo7;44 zo~}n~OheKbs;Y^@I-V0uL!|HTs5zOJUrZ$xX|J9s1WpWLTVA=FJb(fV99qJ$fxNWl z$T*WN3}0L+9F`FqrWUx1- zEegKm_DE1tktb-gOlCLj))uE9bkZ0XH$?L^%OKp}$>1z$#<`|||M+%qj&9tWELlk@ z?Tnz-+<&80H=Ninc=e;NY4h&bQ0t|9>6Y{4PzY~n=(+vQrE?iMji=2qrsqUFW2s78 zzQFN8y3-{-q2^ej>-fId-6?WqFK29KH*(t=K7YwnnSQ?Bs##BUH;V&FH~Rz0sW<(X zR$n=tCwH1I4G5F%%NjTi=(OB-sjGHG8VZj~vo${WVPHJ3Xm=uAFRJgq<*(KMA++e6 zeXL=*Y=5)cE@ixKqqu~iOG_3zu{{=v2J<&8Wd4mz+zm}Et%OZ;+dhuDIq@zsvYCY6>!XXZ@*d5TM7 z!~U{G)%v={RHw(YlESK)&e_MD-~0IXfe1D(>u9M)!Kyy#sG^;`aY|hFDS}@JlDiiM zer8B=t1((&qA zTgzmm0sr%FezxGGvBRQ*2QFln|A}wQkeT!)CS7YnPYw}mV~SU4P`|~7eWsYwE9H%3 zZiyCj?i~S@6pS91)7ljFpb%*Fs8UVK6yT8WnL2#zfn4Vf%#e4yP6b|=HtUeVjt6*W zTZ65iG)1+$yE7faV4dLsx?!{(c*JFJgD%*#;*W{YI9I_nO=>Barp6u#7C(##J2$fb zQ2(=bHJtTixIFdAX=ytSr@ya-`;u5B+*>8Rgzdu>k>>A7Kenzmb`ng$M zsz#S&#+Vrwd;D(j;J%WXGT!H^GHbVXI!?dVqZ4ZWO!np6nD4maLJL&U_%$lyw$~TTXZadoL6DFTL1~Z% zDd`!Sp`>*LDd`+i5lI0-I+bn-X@s-K=ly-}_nve9IDb7c%-s9ldF{2IzUAMps`%AvSvPwb{Rbp< zaWH6Wm9sH}&6gR5z|0@7Is6;oZ-Vn)y-~T18ir)cji-Z6sA|1@Jl@9Tis4d6Bgcv_ zPwk%~#HR1E1)-VBDmXo&ReNYL9U6J}iLt!`gg;^JTD7z=N{%rN^)b3V+ZfRybB;vl zyv<@No#wgE!3dS}!``Ep!;YsvBd}vffAE`S3q@hjWM_D}R@U-`B=(KOIk!pH^!elA z_nO50(pgZIcBOdlF@6IDOYp0=f!SJd>SOMWld4Y`L&7Ma=nVx3cNNjmZQ zaPsnQf9%YpN zd_6=qrdHkjyJJNl-KOnmVJ0Uq{rsK(?nI|kjF#jX~1}<-#gLdyq(j70*^tq0D29~-(VufDin$;!4Kv5)ZR9Y&iY;x>2Io?jdf z8{eODw61LM+J@OR~?SC#Ehh9qv<)Pug>y&P#orsaveB8}gsQ-!KAdp?ogpC62)zua-)u ztw$9>V>4SrpIewNlcVf*2PI*Mu(YpK%iXmM_IcV5Bxlr~A~5ay)IAv1-{FCKaIxuaLV`PbDMM6})=sE?Ii zGLimWkZ7t$O`!s#&9h%hf^`+|pJqN;A*r7|lq|2~81U_!Idapzei3mrJt*ajEh!m! zUveP7@9n$sy<0%PgdEYH>5SbLZrnT<*_w_jbsclAL#Q=gc2gW4&3$*Q@<09@L}LmVTB8c(pe}|G6{=sJuqBAC2>VYLV5aV)9Va| z3gM=Zn8I&$5{Z*4^0_Udw&;Qwy&m_1H)G0sJ4j_KCjN z)-LBDDNBmp{|QATk(Kn^$^arJ|4PkHF|mtc7pFIJ!dzno%}^zQ1Rl?WXMeUPdj~+x zoUFR>S}n?~fbBfb)xpc+jo6nGS>iTJy5HiV4+GMzGU`kNvfBoL7-p^W_PpH@>-+fI zE&MapX*D+3;F7_oSB}^PjgS9;R5sI7+CB|T!Y}`YSZ${+Dl_i6ff8*b+**}4+Z4qIs+iMe zoy1N$2}f;1lGkH7T^)}6Urr>(&06Q*@{QL}VR~`Ea`>aU&hSzsmY5Gk#6os8ZRc~H zfGc58-SdpvAgmkf87O#q>5$mh*l@37_zogzL#yus3l7(F_u9YEqJk&5(f<4uUmkkj z5TJy2ixzmiug%{<5nZSGNtFPH>FM0=9;3FKlcQLAM=J-0ZznCKsM7?E+|aE;F4%a< zo%_9c+TSgTB3IFtd07^v>vOixy?wCPa!02Ks-(VEd(csOL0V{N9=MyvyP0YHMiNX^ ze_i~St!8TDe)|K?$~iBGW2I}#9H-w_`JiCylUj|xR@Sdkd{Ne9HO@&S^*RWtopK?Y zdgFg%qhaK5)NZMLgQ@lSoN8k*uOzD6=g<K?-{+R7tPCAag-WJe1~3<7KAUh?uz` z8-}Y#^A8k%Vv9^}WoX+K0kISIm?*D_D#yG&5Z25X;r)??pu}!$wI;@RuNir5dC6T) zFp3+sc|Y?jYp!+SBbD5PHWvKi*1CB;2FmkXCZOy=)f$G^sjyRw$&2(icPN1b%)~Ac z%CnuJj4qG}kb3-YXe$l+>4g{E&{>%HG5LwnXcBr5KAaetpW_2?KLKyH1vdI$vc5659}&otlti2K3bl7jWxotP2<uX@G`8fAtXZ>LaNN8UkkTOsr z#a?dWKYnlPc62C)@$!qLa}HDvZ$3Z&egM@;Evc;%9Wxut%V_>Hq3z+E?0Y=>cL0hu&)WiNa+!eP>Ch z$|4#GobNYo1;KiZB#p5ggY^S)wVbzFIyD`!GzP-Dw?_8B#-1cElEwD-geK1fC-jR) z6R~{Oj0UCl*C;eC^1ghs(yD8J9N9YyP?uN}x(}{kE7EUybt!)y2(&dvGXc%MOE2rT zWj{<*@^9QZ+!8mRjH9AjdsrC5m2|R_F{j#kiuPDCj}?>z`Q1cw^nus{lr!Rzzx#>y8+~^7ov0 z5OQMh+7*V+HFa^D2ahWF_KV+&VVd7~@llW-wavwK!LLao%k(hUYtJZFC7^@tr_M5@ z?%AZ2K@vE61X7jrYh3a33UjM@ex!-XzigN>;92!FiVLwgu0r8A0BmS#?^qD0yh)Hy zEMw9e2TfY$zTWI)G5oM!7plWWKZ1xR0D2JBsFFe)ZqEnaDBaXp^FFRk5Jk|Y@@EA} zCWMbEm#EUHlk_HLz3(8X-4X7Y1W*D!V@g|=e^KE!wikT})6Z-0=6^{NqyiE6rSm!% zce}bAg#>nJI#`uMXkYhhSU@iX&>nkpO;YT$_&ofm-w&vM{mI9PXhM~L)bpyoZqfd1 zQE2w>&n^Q(F=?)}i^FDb!*P4?1PGyPrFi(NBWYAi!NDst@kzqY$3j1J- z4@KNSDX`#Z!t1yTMnM4h?nrBRdJt|Q!_vU7ZjTTe^GSJWr^Rt;ynK}28)5oNdgi9a z9nck#6RuL?vEW8BU%#FXlA{z#fYza?hGxVM*9WY|;1ky}HAxuJyVu_xcc()Lcag4L zg7VX7Bv|_7ULg&U*FaYr@>SizaW)I!&+YtzL}mXrTVL9wrrq0iqAz&%_Y{sbrm z4qj?=#;SXtba|ROp>(&Cs$7ZgwsyT$)d0-Y;q2f?2u?zSA%16plL;u?0u9-Fm8Yzs z^bcaVH87A#&d(chYLE|}GYF(fNhdV>d1fUKIMh=e*`*Xq(|NE7HFgk*bt~!M&dfAIRL*!${0qFl&E0Bp_8XuK2+jZIMk0}MV|Xb z(c&I_Zv3t0FDu6X0kaJHk=zUS;JGWwIjvBgr7;C|@*r0#WHFRytav-*gS8E}5rQrX zjlchyhsO>8tX+?W{%`xrXS zMZ-L}e6TPe(Ek7i>|}07NPc0=Jg^2FU|qCERtY}vubGnH15VdCfxk!^uxTtmMIb2k zFB$Ew<3^knkt_Sa7PL0m?24&5evdY*6A*sjg^dV`@$;_l^GbmB6>RqcvpZ7Hb_z8^ zP)S|sqz!m*hqJhLv5CF-pOeU!jNCL!VLTxx8G!!zSI>Xl@=lsJW0dEe^o7KOD z0;@?X8rH}MQiK?;skY2L1ui*tDs-m9z$DJPxTMy&B|l&GqG6-7N<#qKj(g#T0XoAc zSLnVhsC@00{^bDp-ikzRxe1o<)wT~gt@|YwZUPpLY%Ah+zzZc?;PiU9-T3P{%UE$) ziye^D_j94hHJzJw!>Od_0?tZG0BUpCPNhw6S${D4c*eu*Fhj}iSX0A$q3U!H%D zi4ppSv-tsU-0Hvkh0ci-R4#X2h;aub_wPNM7r5ahM}pbUdS9v_6f7Zi0t3@11sGww z3Z;G&>(dM%>-x&4?vdi^c+{j~NC>6SwE+3!T5x8c#RSiV81V-48#n2HT$}-isTdQ=g?|ErP**$rWZM8SnD5KVfucs}NJa3<(!ZmiIhL z3}l^g3Z5QptYB>X*j2WEf7h<7n+ur2CAu`8t{0a8OApTHBUSm{h?x{Ocd8H0(~oS)m-@j{#< z5SI6k8wenV>s>4xH!FKh@M#_EW<1?YezlxA9i83LOs5~~A0~%`0;66!Y6O@f0LAE6 z+-Jl37*a&J3m4AJCvqR-j}fjpv5Nvg*W3A>Z8&JRj_c zyY1YS4HW+qDP}KinLuIL1c5LKV^Bu0YPMkdAwepWQ~jq|kbS|acmeW$8oF*T4zS0` z%!g9%AzMy7fQO1eD5`r$&P}|!mU#av$M^rxTvJJT4aSrU?nv%iMQpBb1^_E#x%50%B5^!e#8)z zK4hrVxcAw*)5iGtEkkHpTt#WJ6V6+MIs_6{ePM2qMmw?_rs*Vw# z9XI^E2rij(ml?~_p5JyP$1;aSTdUmv3YTz7ML-{O^8meU!4U78jVOf^!gfVU{fD6> z(yRh#EVe3&)Vj9Rup;eE+3Ci|4q- zVW0Ti@JNH=@idsx`>=)FQRp6C)Ng^IJe%mF`h{3G%yQFyzeq00o;}D_%CSDrQx~_q zu+H*$G;>g}@Ez#sGSLK|!uXV(*qY$iG-Acn>;5sLbjeJG?fe)x+p5dqubXyt%3=L( z)1^2X>TwLf$E}pdPA-}7r3HFS&!~}gRs%REm!=O7NaLYBajCI?iS=k)&q)4{o*8a{ z7NtnZcJtomM6@?uZQkVz0cK)WNGk3^N3hS;@Rrs?Hb}zAmB@9(Beo z;oIjoZh3!1utTq6p2f)?cKIZ_YRjH1Cf0t}lEzx1v?iSzw=(Zx*icAS2n#RPvGvbk zEyy`x7!W1|1Z^|1iogN6zg_YrPpVhY!L$S?z=z?$VRa9CM zpGq6ndFVck)YuzvZoAevtUXt|?O$DtO=3jA)>$a5y0@B-X5Cau5I3ykkHMs{zqTGI zfJnM6$nqDoV*nAEt(#st*r@Uh6vpU)R<7U~LeJ6mdBDNK9TZ+(Rx;NNUj`In8dWX0 zj;BM0yvM(OeaUY6VP?qY8@6nxhPC$%j7rEHk1^A+gS2UKDUdtY7-fT@APjj8`NEIQ zQ^$bgp;Kkm?{qC+;FPyT*p1sNh%*xF%#>HsPhJ@(=!+ zVO@a{z8W?b#J)y~8yco)0C53FL_pKOQ`id7;kkV}|NpiBCWRv*-^g(g2B1tknx{b+ zq+u5k93r5hV>yI|&tS&g;yYwjey)SQ8K#L`9@%KArC0&TIM{D8iba375epgude+3B zdA9A#K;q*f=se9~Zkl6NpO`0@fQ@d55+yCR7{6U>J185bz#dk_l%U#@VAg4kANrZj4eGqZ%OUmuf3xw@-%>XaaT zQVrm9fn&YNU&++g;+B*B7puGq8ft!R`PyvO>&}inQSg=Mqt!G*S@t0FOT$? zFT~=cajPFxOw=F<>apapriJ^gq-f+p$x>a0{ySEs&;!Srt`T#hVRbGP^3Ht86SSkZ^?ubB+sM{<+4p1rC?C2M!em`YO9el7E4U~#|Sdn2m@+A}Y zRP`tdohG0ZISF@EENsjywUwz|w(n#(%wK$E`tD6Kn#NoGA1y$WGm_Kp^4N3E{|I}k zF}(OTR%h?=3|@(LkD^IjHP&y=N!5zX*<#;WocN_3-cAV`Svl7M%Zi4bJ|6%&I~Cs*93hUQ?!hI48GprUwK=t>-JB-wYGku zW;~UOc!_g5)rD{lZS;=#=AGy15Gy~11m7sd<*1nOYk358Y{>B;B_@@*?S>5jFR7C% z>dQ9a#9{_jUQREG-LNloSH$J6Ee2l{E6}^?--g5kZDYcrS{_&u`Y*4!`%`eq2i3zN zU5}!;dN|Pq#@8q)U_LdJa186%3ic-uad?$IIobmAz1Q3Bb2y2qiw))xm*3Z4fKAY5 zUy3#;*>^~t^kdru){2d9f`-Ev(DrzrsO(K=1eKZug#Z~}ketW5-U3bVgjreYuVjK7 zu+bgk=DRq8Y;N)EQ2j~TRkrX@s=aj;vn2zY&o=uiVfRl!*kdZdsWR2Iar0erVk1AX2rUUMoF)_~yMxPFuV z2?!8NidU+RW_=rVcBd=bDK)Il@mO`k^h5v#!*3yKrmx*VZv_1va}&Z*q!%+kLp>yr z`{|?FT^iSb1~=Cj&YF5J!?BX}fO!b2UXieMLDWOfg_C+SR(ktIN1Q4n?OyQMwbNDV zw$6f8t=XjnwOea@bdPF55Z1U)z`iINbgzw2uxY2Q0k{aTF(NnOBUNuV4UsFW zr~xE4^wTBSL>^YKd%NNcqC11g9#x7 zg^`PJ_tO0ojO_2>L*3EJv;iAHt8exisJt-a9#1i$`_@~4Veff4XK!3+iH9s7ZnA?{6l*Z0ut4V zL^2ljpL&2dGpVZu#os(mvf!+-MvgTjnfPp%@fdU1D65}$GF$NVt5dkxSo&D@XDIRw za8C&(qYPc3;2_|H5eO3+sXRMP2AVKJdZ$NbB@xsopf1jwP?WC{xV+SF}Ny7mt=N$(Usc|Vkorwy}90}%Y1*$aH5@7ozm+y+i9Dd-9q&?Sv zj?mM8A!!{pAQpb1V}0KUoV>v z!fl{qP-udl2$9!`yGT(3`{&nJJ)+1BGE;efQ6fUmF>~M|ID@(;m;p2+9W(h&yKa}K z3bCBbLv-g576ELw4i-!I*=QwF0f#guqf1XomB0<2fP;k{uSSKnAB{|o#&8jVX(X}j z?xh-!#cz05DWiN{${e5_%!I$UO%se(4m_)XsbXW~RvIIPA`I`D9UKCs!oI$Tu>toe zVmhqSX=*C0yNrHOVh=*W+%0cz86#@)oA}yme~n}sYLa)gWMAGO=JsM%*?F=P`&I5G zYB#EjZrL4kMOCA^#sgS_1JMTExN&9yW~;Xxo#U_(saFcQ%{DJThbm~yD< zTMr|LY&bRTb~JeGS~h{uK9g&2Ds=5kTV9=Yd{oKHS6(dcy*Y(*5C8-H1pL|O*xBCfokaSW|>rzB)PFL)*}2O4s?NG zfE*eLzXbqjVrAH|-98w{ilodNs2_&ln!5#@Cu%{xE9|`rV5chCT?L4C>m(R;#j+HV zI#=E99Vr>!8ko+#uiYjqEY+k}--K*}oib;|Zyidp1f$le4CM)ZSP4~+9YUnQe)lpp z(}QVx^OUD^I2~5FpiD`T99FI2lsmX_&Iy;~G1Cq3y7p5lP4cQZP@K z-h8mP0tid)A2z&Q9MWRA&qbK}2F+!+pw;55L<4h=hOSRfIE?5ocqQ6(KpqeT)gvHg z%H!|U$T+=6RcMPB>tMoiDTV)5yj7C^TP0Coi%@Y7C-9q;9j;K^B&SeL zXYhDc(EPp1THAncY7D9_prom@0eos2_kI3Y>=hFb7b22u1M!n#X4PzcuQlQy6+nZh zK%Vn6@fr6-jg63N+<_)zbQjB~OUt*c`lVFyd2o;;<*-Me6?l_LG6=am_~*e`6Hxw_ zmV0nKmIF5~Ou;HRCVps8>bq*JPmaJ@wa`d$`@sTi$no2Ll0wUFbeCEww2Z^~_PbJY zkC!%Un2>m}TzinCnl1b+($`V6Z}@`$$$1*c=jjkJn@Z(* z9?wo^@+pXnMYop}l$ zklBy+ST?_cHpXIaq*cXI1gYb)DDXW@MF9Sna2WDeMmgAIZ?;x|@#CNnDfs=G;p)Qn zR71j<=x>0jO6htSF5u5YQC#$z>2zgRpd~z#!klBmASu5W!3rANU}UF!xbyOGzhBxp zdmvt8c<=395oc}sOqW0b727T9Q-q{Kw`bzY(u3 zT#K@5K8&V;c;jzA=K~24-TmI=;Wh=y!Cx_God;pDk~{s$P@-t{8!NNpj3XEMP({N&(SMPW5V%zsf;VD16gic!ZUJp3kFJ%v2L zLAIY>r-~%=-bUyl6!p+xGfz*;V?S95pcK{oQ{=GT7#e1;w{*XQn&y& z$35#uc8XLmN}+Yv_a9xFBMBh_;G%V@I>NAv122CajBNt&)($7>2Q^tm2b9nS=?2=C zbU|%6UZ5i;c=S(en#DPzCovDIhdZ*zIFO44j9htL+G+8BCyV^IU4^U~NYE;aQ|0_O zCxm%K7e=$hj#B53f63p02un1m+ zEAV-tbtr%2UNS6?QY9?h#M;KLqbNY1lkYLTy0VB-l7yxphLsR#P}?z^hQG& zzUb}#J9`RZ??;fbY$DlX%gvf!hE@7le|>Ec!R(5}O=;t58w&Lta6~t=xP28mIJKLq zE!hI_;%IB&W%Zc%QnJA&Kng2E$mGWTTaM?WrT}`leQFELibq}i0VV^`u8oOMLaGnX zdQ-N?aUL@OL$i*!^|D~2PCG~L5S%QHe;qx}v#d*MMk|34Mb&ot1;kIzXV2>Ya}|&` z1Mr++5@Us9Z#|G*kB_hZh>>@mUQV|?Z zt|&}-DODQY2pT3Sz6cC1tbl9HYyQeEh-4gz>&M?~-yH{4=xRVbGhtU;Lc5rh*=gc6 zQ{vZ$;zP)dVyLIFR5F#a8uY%T5uZ0AM!+q7T)h z4PcEq9XJ){ z<2N4=+pbtZ?(y)xG9{ZK@<;cu6mTbh?A%mW6x z$bm~GP^w=y#>eqP1=YkU)u^&G_g7T)g|f3B*bQ!L)@T5~l#ycLd&n z;hQG_iE~`bORWXoI}HSs?StSb%JG2wPt>#^WPv>}UOrA{51B~P#eTXxX7M>3yP6J~ zKauCp;xVkF_1naKP-Xiik_z(z{t1HllApoWK+X;8B9{!=Ve@ww%0-Me`M8~dA#nhJ zsh6!TsB7Z+=WZD|lR8y4&cQWLKz{?Y#cRQ7C>KSg`Q_gK37IMv38^lp7J^$|^a8M# za-&cNlkZ(g!+Md2DI4mCigCI#sH^D`oyalxtMzg_mK_ZQ{ARq@wSB;8+Y`4BY}htY zoY^ljl1w@swt59y-NnTnXFSF{%t;Glm=ZPB>NCZg71dzljOL60GPd~kQ2iydPpTjG zTMNi-0Vy5N9KOC5!oi^89RqS_Uje{Occh}{fa61E=o0EyrjHlHXaUob@f^oFI97Jw z*^YWG=r;;da#Bmiwm_AJ89J!>EBXtQttsbL5A*WJjy1{Bae^37uxT`_aVh1ol*qt9 z_RIA`lbJO)3OLAugEPIuxf<~_<2AGpB<0{1O4`G34VU5~(dJRMX4GG_$gl8+K<6bn zX0A#Pcw_AjSy6z&Sj%SdPFeW1a~{1)1N{V;hBe){1((WJB*JSTxa`A*PCrCui44em zmsMu_3+XQS1cgKAje=J`E~*+2i_iI4%CHZ5-)RK6zzV7ZQG!^!M!RNe7I1eK!2bQH zbPyp8#EW$a4i)G=E*$E6eF@N4eJ?D3?ReW{owNDT%6&8eMF`)*1o2Om9wXOkorY^& z9H{uGAG%BGX4hd%ds{`trLObc8W2pC-vR>-Dja$`cX_d5N6!}FeJcRqq3ekEnM@en z8(_Ck%k~3qC2inZGuriX53HhRadM{xvWMfAp5GL2q;8<5$C3pralG*<%vK{5x70>2jZs#{)(R>0??I73yODcPK`g@+boZx*S{myCL{##|$QYhw9=f!@dlir_|@aKQ- zZr@Y7%3$ES$|d_i@M)PVjxla*-EG=Ed zZG3cBjaE{k!D!-6wHIC1`>=u5l zo-cUShW{5X%^1l@dI8jPYy`T;?bJQ?lyvi| z$v;=F!0T|nYS&0%f3Z;*&=kyx{B)VFUgNtXE-;!(v|flsO}B?0obYTFvj4%E2?D^) zO_-gC1OYQEEj#aj4FqL-%7s#wfaj95jG>>8iZn4+Vgjqq;mGa2aPShqzpKCnP#*>= zO^rXjpn2u8FMmDs=XEBBA02LhENDdDZ6VN--nwU9F0_Y*Qe?)%^g2_ak!@O~{X*c# zU1=B@D&Y7p>i_~E3;{yX@>n8eY8sK0q5=><2Xm;41Em#w46+r~pfG$@2|`XEU+kjo zLRuLT?u|)_DPs#gW>XH6OJq`tNV=1gtYAg{G?Kaw*PF9GD=c(x_kmwq!)pF;hsZbx zFkV;4#izp7@NT*GiGE+P7=aSK^dDOGrd>(PRJbybCH3%pA;50oB(sY(TXVk<(Z0`y z7tg3d!X$Q6s;P&Wjt-GjQcu5FQNYK{)Z}}#EkKskMZ=iD0f^7~DG355$RnVcV)F)b zAgZ?;$`~GOG3C_g*!<-y6l7_!QHF(Qad?0_(KTZY8zQv$bE5GA*!CCkei|%^3OPsWt$~tD@W; z-hvrU#F4+>1|n~2TEqc6-Ux7YEI7B<=hjKu(6n9ROz5QH1y{xyK&32alO5mq^@f3L z`XP1cos6IrQD|71BP=HkN-^MNWwNu(pM4u}k=jQc0s+}LYNf1v<#x><5ssBTzVJ*X z#*_I0nT)+mcmvGndI;lHrt)BCj2 z`)X1g@JyHuN{Y~OB{_1J#7rkuQtmdw!QnTGS2>L&NPRw$^!3GlnznftbJOjsW|oOx z6m}ZVCu8{fUd*I+=n_SCYUZ+v4VTK$ z-8Dm#`|r#s6YlG6a9$&|*Ln?6Wtd$|rR;_+KCs(IBbg2N5>KA){tO*|+ONE+ zlY=x?`(VGL@H+WB7DS^%as@bJCH1mbrGj`%xNBCxvPnWxg_Gs`)UA1ae&icH zB`|48xZyZ*;>`M5HrD(4LYgo(TdOu~AsZ9Gdnuvy?(21DiT#)rrS@wBncXCVK?S=E zEHdVlPZWwCyJ{rE{5sr-28xq}-m(cxq^{g!biThJIWFG*(3O#P0F(3&Tf?1$w32wu zo&$JB@6pzCt7i{dpr-2vr7rP=N_Pn;qgB6!mDOxSh3&g#Q~0Q^82N(G*dvW~tulb% z?hpFx-SeqFiQ97ukJRln3r!Yy&sIhNgRgHAEhQ!Mk;> zwG)WbiYBEPK1DShT1tU+8={9;bhAD|?_Yb1RZQbtQ2L-(#MW>4u;oL%N@0wkyLnfp zAYF&1_w#QLlt34kIBc01pbI=lD~+uo2)z>Ce3vMNpSX2nP1liz#5}dXxy0DHLqaPw z^tGq~@Zr~f2CcqfR;h%((SyJ5aJ%~DZTCHJ9D7XvhqPGf9j5#BEn>j)2llS1DKnI8 zASN@zI5$0K-{F2qj)~yoc`0Udc*vDGUW;0jgj0POwoT_Ld>qcp|p?u2c~x^$PS+HHGgxrwc4j-(Oi|)V6c^b+^p4#$O-EkgvPe68)aO8~W9PtRhwJ2u;oY z719=#k!3zn4?AW>l^2|URbx1Tvs_^AU$LAcP-HKoLd8#3s!;CQ$wOWajeLBYGYrQ7 zFGJ}ngfZ>gfHcPEbU~k(>d95Re};S|n+RR(G3cV^MIqaQMb-@O*+~msZ@Ytt0{1TK zegpbbN%nMd4GsPpy~*F@en*sSA#jD!c=f|Ve(W8Jlq~-Q?G|uaxA0;X9^!Bm?f=33 z;(Su}-3@L&rq*{1TEE|`^Be(t@Cn?EeqHRorlf9RW3S9pqR3M&qN$1ADnfE$@kCj0-z}@zpI$i`1Tdao1<4HIU(L> z^eS20dn#HSDKu*!Z3RyAB~MFc7bbl${Lr+T9F3HLIXwYs*4}J4&<#?BXk_rCVF8F> zGV_4d30jjWNV-yA?UxxyNl)lH%p7(Njc0C!1+_4JbOa-{|f_(KeLxGnWk)ou;Kc{Kz~ zV#ZlB<5f-Hf!m&Sw!29CO;AQs&nzoeVn;`;*u{XunW$OGBvkILf=Q4D{eDGseCsMVbDebT2Pk@z&ieDQi z=Zz)|QW0__@)Zi4<9e$3n|6=GVBaI}NmwpW3BF~ZlJVa!3@b!;<#SYeaXF? zJKzQ|vmOQr$KDe;KZxV9!sgl>x*%<0gqk!1J=TZ2 zRmy>H9~*Ald;8uEIFlg$btV}B=rvdRh8|o`N0Yjszna;K#-aYLQE$qi$=w)re2glq zKmtr>p>#JX<)1-Ag35B><1B4_Ab|G;po%=%Z(nlZEO7qjXy~znS6(eOexRf0b`UYd zkAmvBix;WV;>^efP3XX{0UUD=XkC-Y_*otkf&ZKLrbteObrLiR(=3OD>GZu%%@c@% z>QsZT(aGYz_LwRL{`YG*Rjr^9%WX06jB-?wJOvV$;jvn#^Il4&f&#{u0M+>ozD3WE z`_^C4C3qMSPFPDInni{4X}W7$BuG(!qZ>vlw4y5kJRg1+GlB!-G{CVS%cRd9a~=Gd z&4&=xze4aYIM66OyPpIHM$%J{L=foFfzQ{C%go4{Y?^DQ+6t7ZivbLd^N3Ogjx*GF zFpt0Aq`?7sKd*gT(nP~E6ac@efHuD1%KwiR09fK=)?ES{`nrbeEFCr+sJ1r+a`P}S z4qC+}Se&Gp2Rz5vbYeX%6?o!&NLJ;+vj7u}D9UQ>Op*LxV{G6LowN=x%{wJ|&pS*D z{&><@yH6D;Z?i#H&SSTA(rchPNsp8*DgMi7Yvpd~JO_n;A1IO+El`{f7UsGUZ_GVL zT+ja%iURPA8W%Xyzm%#Y$AiBG72rnyM7aZv$3Qi>P16oF_X8WU|9A*JPvJw-yKG`m z04%k9XO}bx`k6Yb<{KObRKLM-?}d=MfpJ#VftrkOyJ-@i5?wSJsfw$xSkU$i|%0rN+MHy7Yt^l>D5pAiOhp08N z8UwxMRwQp}fd=9GY?BYO?stG@sIIW%@qdeQz$-@Lf0!@HV z_&t|-82gf2f(mW7eLdhwiutR*Df#52j;Rc>@FDc>DS7+__i1o#-sM34`N!`bSa_Rt z!TJ8FrPysV^Xaz9W z&YjF|_nTWLYaKUqr%(YJV*uKvIa@C-+XRrQ=WRg%BG;a6H#P=<@^-0iCVd&=Ug(I? z!iSu|0VZoJluE0BO0FwCFV$>PY{vbeFThwgPR~|y0ynF_YgGaC`1{520@=}D#qA+L zFOJT@c=eaIoZ5jJ2AOC30Jd{ZH4nh@mw&_)1Lfe4_Q>WWS^>Cs2;_@Ws7^mMa3%Jd z{VypmPL(!+gFFLTeugtOENID{SaG6jr(HG11otB*Ku0?BavN#Z=a_aX{Ko@)O9?W_fO&;SkRwFhQ1kD#-uqZuBIN@qo> ztZ8f;j4o0b5I+bE*RKKHI1-9VPz@lAHci<7)crAl2($2Uk_9COxu;9O<-3s6GM)!E z>`f8U4yr;#C9+oo@Jve5t%JdO9NcVetT$4mkW+|B=y`k!uT5E&kxM+U0cnNuXewp39Q}_ABPK&aHn& z3wUwbVF{la0F~5lg4u<}KS^oIOi7rWEfvom{Ezfp8RxI=T4+B_B zoYT)c7K&M|>=kg+F~5-dummjqw?YTN0^Oz`95?tNxe)<{5G-S&1YC3eYE}UnI$;ky zD(29Cokgdiq1L-O#{PKIp`dtySB6L_KV1mGrl!JpcqsWxrE^{PQ=mo6zNSXF-5&Pl zCD^(VlwlQ*eUNJJ$t8nfitDVgNa3G8gcHi)SGd)2!A+q&@*S>D#>Ub_&c9mZJeJ+f zUBF6>s9dlH9;?~^D@0BXEV$EyFQhm*rlZEPgwbo_Q_v&7rkwi~!I`DUsZ4H)OyWp2 z8R{kqC0nxuUEzvej1p{lp)@~k8RwxC67YIV?rd`b;{pF{?A&}{Qhjql^)+jHq%9m$ znwm)&VWQDLOaMW_HQNHcYCd!%pQIQxB*jl8u)&2HT|g(t&s;1tv2*(*1Jbvih*a(d~S05X~Bvc#6Hxw*y4 zW;NT91$4&!^CumBpJUed2`d^i)!+;k7JNjPFC*VB9-#S_L9cC{zCqGlf?ed%ApmM8 zL7{*pry_@#X)T8(V!@^#PRrXfjnced4{R0%_JD0!8gA()UgV2q$e|vZ$XZO zdAT@VN!Ii;@NmM~G0S7LyGxetYDar2jX8vxrW=G?&wDeg46rBwE4LJjyr?ka{UsZ33L3G9nlCT3&nDU(sWsX$bIm6u_75k zKo!s-q6~_BnoN=xK7a+28-?7T_hC1%4BVS;+g}B~!PEQ9uE7z3jObMWNxm3((;b6M z04Aj!d7Ojfa$s-&B{L{dD2i(OIG^@S-d@;jD2ZHD#*)=B-W<%7OPSAt`V9>GDbqw=r;artObOW@1 z(+&h&7)7s`g)-?plQ9@Fj3XAP|0pZYK8W1LsFK^-$)t*qWiw4kOI#l%m;54uym%|m zI8eKJb@UdZy^2kx61xoOu$88NAbk3iY}6AX$JZd=@#=t@la?vL6@b1|INB%o#^{I! zY&dg37JkEB;lw$R=SMXYoD(_%uPI35F;Z#`goO$o@|ScHk`&bln^Q#B^U*= z6h>)^)SC+*(h*mDs)@fp9W5u?zw)pW6)*DoX@^qIYj}$uxe}bIBk0TWTd(IN2- z4l#ldC2paat`9(7KV{^;Cty3lCvEP4WTk&F9c9lJ()3zM>{A$1<^=^3DcJH=#AV>i zE1`-~KNs@SGza^W9MwBjIrf@jlQ!%}DjyjwBC($mi8FVXx-a46Pq7!)w8t8XHy&|- zX;Wk(Mq>Q$KK1qQp>dwFpqR1JG#t+N+8J&dx(SX?*1@h zKbVN#K%^;~V3a)?Wx@+UFDY{3K^!pD;Mb+|r(V6bQSRY=orp{}7c1Q9$8oxh_9 zd9sRTdQZjq6DdBMkPqV4txD(o)@I6vb-SO+E zj17mvM_t7Dq;FA3PUuZ>>apinEj<#<^-Pb@TufJ;@SJG-*;;8P!iRreU+Cz4KJhao zX;&91C<-a{GklW@LzcPgAB(s|XqLKHxr?GI)BXToN_?h;1>#Uac1V&glWydnPmDg6 zEQ?VOI)`z0_Q}T&`~F@zVXS`ODgul$H;aIWfHmaemi8jDOhb7QQQC2_$ef*H(?zTs zf9^&ykW6s%^u7}T3m)5Xr!8ZGqRi#VeeysDaCwppCdk57(fAYMW4Dk-S&>kPV=Z7? ztc7hhfV1ea<^#I}dSK}!oeyfR6>1$g%8p%5lApbin%g5Ow_?O}sSpFjjJ%*b@7d=c z*xY2ujw6hJ5}2T=G|5Ub_DMv3V5@uWxy*tSC_SO&up6DhgBqvXyt1fkR$>f6jlXJ9 z&}$&v+tG5@e&0dz>8Y+wWcb?!RiKX?zXxYn)NuWLi*qGok0JPmV({i$8)_wjiBB#- zoESsAVEO$not35?dn923teyeyhN4Oj17&9~ix^2;2(=ia`5ico*o_3W?!z>nN+G0x z61=>^?VS7HS1GQqo*lY{JBw{k&>zbIJ4lD; z@>A*GI4O*}#9Yk@-JnYe1@}ZL;J7p=*}pe$u%}}IU9Z+CqjFl}5t^w@|mJNx9 zXvTb`&aV0+5@7e!4wLRZyC{)eVI&@WlgldRj5Q-h-* zVG6IHgZm~}Cjub4PJ$_jL70qW$< zm0KPD4GgU-x2tVshS>?aYT>2%iCd?e+M!n`@a|=AoXflKTSF;=D3a#kPBPZJUw?kj zg!{0QzY4U{b^=(!z_FUQUB_PB&^;Ih;H@;+8^Tt|=e)(1B>Tj(NeD?nK9N%DSOB=n2|k7B3UZ5A%yH( zGuAPPBxR7LqNIhCCE1c9g+jJMDWqhr=Unyu-OqhLujhGQul`6~X0Fe6KIgp8`@E0* z#V;$m#~lat zZ)ypBWWFwanlX_}mX<6y-20hY@%rhBO-foOGUQoIw2_loRSfr8yUS?q!g9ThG+m?9 zpxoF-D3(e7xV?UDOk)#KnMNY|&{OuxoIKVuLb)?OX&6twZW)Jt=JUPySsmTUeV_Xl zdX-VL-`VfyXOy8dZBMF#1)w`dn`0@&PgZTjNw=n&<=bVQe0~>SRGmG0R^RnVj%ybI z*WUNN9g4U8F6`CeYOniy!8ku4*9lb%?BG(~ZR>VwC zckuGIAwAk@PG;FGCfk`VS}o>J|A`ELQvdR6SmmSHnqPTmXTK~22alY8xtu&X88-Xt zeEzTX2pQM&F`DPY#Mu@$cF~wd6oTlNmRz!7{l{lBJeg$GNB5jXc^GUwj3_!vk*-L$ zsLKlr;%tlxefZF?@5MfOdIaAut%vnZdIG0c=FbRK@LwnF;}V!RbdmdYeWRPiF1zGb zgJUCyH%!)wl(qDwZ;p4`^H5;%%?|!lf#T`aI-9PaL8L+ue2OBMfKGBoyj#*6k@;espTJB4L|s#bsfVC31L=! zlyg8g5fk4Z5G&d<9WzWy z-}7zixx@yk<%uWqXev27WY_AGMO1vZ@8hX2xW26suS8D>7&aRosq(#7e?w`}wS<(o zuJ?(T8ik@np_&v7sAQ_pZ-9~^rj z_{-zrAH!=KR$3l&R_!Lf30T>uxE>pwdW835nDyk+<+S?qXYYO*EsYp6!A)B>?z?OA zgGcjQqXq@luPbDwp>M>mbYRB~jAg-(FQ=p#QViLGFhP$i4E4r$lSeL1jk_hOx?0Cy z^V@XT%yW~4Vq(yAlk4x6*lWi1=qxqyQ)^Mbjt>y3Z1kkh@_tH>9QFNmPulg^hxZ}D zubb4lZ=~Aq(!kA`MuAKk*WV4r#?2m-Sap!^SA6Nu#NXWyw%ZW^l6r^`i?n=yXdows;l)cn+F!dstQsN=Yj>8m?#vBYdNPqwQ=CQ%q;+E^p zgHb%q+U({mt$H?MU0BYEiO0m7*k{!?3+;NX>X}N=;<72=xpaqaFh5U!Jeyp)OHj-L zo*f5&Ztp^VEsK1Ce_*+4Vvy1hfqM7gfv1&bV94?LdYL-M7tBU&MP$IuIC#?Won=Kl zvZ@rk9h|2z8=T2!clUaDo$G?vNIRvJc9Ym~v;!QO zVGsZ>ZIZ2N27{rw!Go%GHO}Jyj;Qy)Bl;+(9}5R|8h)}c>)?lNG?v9CXIuA8x6ktL z$T6GP&57o$BNZajdpSrL7xE-9e z>3<*0L3RMMtvZP=1^n>xJ^RvicIe;QG-9riSR~$0p`&-;AG(pR za~YOQfd>?TOr7XY$`(|J*22TLa9{3}L&k6+ zN>HvFun5KNkAE;Hx~{ypr(`KRFrj&t6X2jCHubI%%f(2)ww=KLJy`9Pt_$53*{>Xb zX4sl4l>uDBcA(XMgdA9vXFK!SbydI$wL^cD27DXTE?wZzdkD>_s*b;uUIv!n20gio zROrKbvD&NK==bl{GgSB{=UZ2j!pk?Gw;ZUrIbrdAqTRBrTKBD_Ss_xYXiy*dhZNTt{KJi0*;qP0oexH1r()$2wNnR zyxApG@*=DN8~ns>Fh)RD3WH`>pt3;f;5pFx36v{F6Ffz>k3)Y>0C>AtV^3bQxU6AW z;ao#JJK5!+`5u}oXINa?S++2@0 zyGlS!55Y9ZO6?A^{}cJGW^w{*0M0&6tqft3v$wlm{Pg1T^BB&JLWh#I{U<0&j<>Jj zi}}+XR)5VN2xmoBUyWFy7T*F@1}bz=t_(h07i^oN=h?=%);u)Q7<&{HO{)Aqnv1-K z+|kCxN3Qh|JtfvM>c62tXA4j@v2AsbtLcD>xynyb^Vp-Y35sRVA-E0bhq93AWpc}e z*+eXja#JsEz2saU;gY~FRsJ?;<%hTTyXvw!fGhmppL(Fgp76LObl-IgC8)N(Tz%^O z{6v4T<3T={HMdgM?tp+!3X*dDCl#5XNnF>?1`v* zR|4`6`&R(w+U_bll%rXVKhz(cdat1_(m4)9 z2fLFDBbvHLGAQtu?SW>m49FR@(Xg^b5J>8aU3&?A*35wv=n1S*yK4sQ>c4u5_i}&V z-{e8TfBI`rpRJj6aWbP~;Ro&D1+73ZljpI%Op@}!s-l3YSB7}Z;fUp<4to?Ci{P?T zAuBZj7^|%vLgLHPpg2&g34w-8gC2*QjK*rInR&Z3(9|%?B#X{6XwK~Oxc>&Aphoj%%Hl01fA;)dDm3RK zM>6yw3vgfjrN{=FYFOHv3YSAJ2QXeE;wOFF4J2O1QW~QF-YJs71O6RA4nRrD(x0p^ z9{Ne&u0Nf8AweSLhs1-ufyq95n9TW*3|ZrGtacCH@FLio}3+( zLBbM!kv6rQXw5}hQPkGCzUoZF=1r_Kct`jpBABwh{^cdPWLirUCW_L;w`(Fkm7`nX zf-+tA1$WsIj7abvldocfGahTP+ycrWahHW|N@1-ToEXBc<6ZXgca5{ip;D`hU-!AA zt2jJFIq44qcHNX3GucZ(%kJ1%eIG;g#_z$OPu{bunq;(%L>8L9UjG}cRIMj3S(tSl z?+r-vu4i1#(%|_q^U!IogKoY$7(2o=bS z{2s;uhIUz74p@OLDU7?M$>oF_UF9HJ-AxcR`E!zlnf*KIW6+NLivs&@LEmIj)ApB3 zy%lhC1%2}F7@T{qkl1!{+oAp8wQ1Ite!H%}hmBHU^wrPfx9Wi)%h3tF_#y&9z1{I;+o4e(3TM$* z{I-sLmN_=0^asoMQ|K|Hm%_HX!PX|xCyUlc51+y4LACdkJKP{$IAQ2!ai@u$^6Wh1iyWGn zcie^{U1Kj}XRtY%;IlV+sFag$QQ(xv2C!Q`ZM zLlW-obW-$1a;<9v6*4-(&`Kkvb# zWUl6f`aHbzl<&f-!kpITboQlZSD0-H63>;M>9Utli6_xLC5+PvnW}BL-r4y#0VHe@ zisisy#$BF=V5K~GEki@q?M_uSL_oL*fs%lC6pV;2^`4V`}~_^6t+vx|LQwu48;^qRCV z%ePK_+o{|9Gh#iZ_;mdJxlulGR{CubObV?x`gV+B?1T|-A0)d%jBCXv4uVz6*p%dC zL-RK#GtWQ$$#eY)@rJLYS(pj zkLx<_f-B``N%7KzP&~nQoV|XnAV>Z)*R^HkI(lEYaO01A^G|s5$8-k+2v^wa@na%x zYfZ@#LcPc5_Rk1v3NP7d5X4~b`@lRN>$VM$NjQv;~Fe7a3b&uE*MD>q?V)6 zR%D`~*2rTC?;{vfQ)&_C2BJ1$GTpT3C|^rUMhkw+s|U8PMq=5D0-`st5(1or-=;3$ zFETdAhY(lVKV%GJ-f%QV*XosOPYl;liA7TcWz#>p(RB?G1{V#*-G*p&Mm``b9U<@5 zh1BWi$~s;%(Cpga>dd`VYc=##I!5Ac{MDgv>U3QyaocxB3vB^I=j5lI?&%Tmj-@G^ zz9S}DdYq)lt;zIkoQ&c|5%hfxpMDsb120UjG4=!YHg}FfghnAv+f!n3bLBFV{6y4? zT9lMlI15d@Ygq3(Ltnb>0bj67HtJVEm?K7D0NeFmsw)RYVM9sE(QUSK)Mt*qwhnto zvsRr~55pykvn%$~t+(3dfj?UK`L&O0ye6`!=R%g0gA>mZs(tT9xnleT<6W)hHKkR* z-s)F~AUrE+K;LauDApO2wjOH?X6Rw4OGRSdY%SIWKg0AuJ~gRe#iOwgf*l->78UpS zN6+4hlKllXJ9q_TIa*v}Goy>lVsl#gG)}+!xvvN>imyrGn>BnGav`qqg=RAZxjBsi^gtvf&Dli1I*ZE$FR$kEVk#5`_n+}O*d zns`+P*L4X`dPGyMCYMgU0JC|Yi_$jw;69FS8N1oJvm2B;y)gPT{us$DhMJwbA8O_`Z zf(lml=h3QV5-9mN0aha>{4#@aE#h}5?(~ultM#^smrm}+CaXS~Ca1t!u?TsOZOFRD zT{fU?RrByEeDHXpkJ_Munp;mOHKVJQM2F>{Q3aaVqD>U{-=*O zMwC*`K0F-ovld%$n6gWYb*0VI%gz%V*K5HASW&lTDh|Rq{d1~8m>H#woYNnP={}rF zp)$aY!yZ1Xv+ouZ+s`>8hGxz z=?*#)c-8v34ziEN`mL{THTpoyGZ9L&z0bK@9wlOTZ_X_#9qTE5_~}o#o7%;ztryfs zk`@LfJVm=2UysF{%Gt=9Jg)llTYNq?i}Ox`KumJwsN{O!cn)nWx@hNh&f1sUn$4mD z^=ZM%p z_KsgtqBm_z5N}UP3(qjPiS=cXVmupH9j;lY_S%{5dDHP(Z_{yo@I=b?Tb+JLj~U@L zp=gH;MR!Tb5DD?vdNcA3F}ZYwn(LqZrfkLn{68hXt=8h_S(3YUm4og@(e-4Lc|xob zNDo{gunp@(dp>)~MLTy_oV#H|jol^s8@mw+ep{QiTQr$!&qCfXNAf9gknUKhOwjPM1}BGp9gg zkEs#{!{Sbmr8CN-WD-{dKY|fRcyvbhDr$@l(Yx*K)mX1hMac!fQ>2`<7Q{fCQtf2F z&AP6eEc-4Vd?;GxFyRfhetYoAv!C$RY0Nf@xtT2|IWVG2uMA3Csg;H|dfU*EwZ$P(q03|sCH84OwkB>oeP@cjOBh?SK>mDV^@HU&KBZ26 zMS2se?KRjiq7`x(e&_b)ryPnMX)Gy0?{3|`v^2^=jT_5UbiXB+UfFc{N#c8ikSlq1Gb*vy*%1(ju?=EHg-7SkxU}I+w$48_EV&HV{PYd< z^?J)WhdKMb#4sP$3B~E(Yr?C|e#1n%4V&;)3vmfz+1zyE=~9nqnf}seaSGidcX|sp z8~<#Ls)Ui+;O4~vokORyy?$YYYJE*KF)+JYSgKpZpExWau14Xz&_>iP#xT+GpTR}* z`1un^ZCmE=M*fMA3(+mU49CVzl_CZK)*3L%9e8-!2p3*W?oHr6v`O7=;Yd9Ra|0y` z?AG1S0}%ktAPl1jVRYh%>f7GIJ02Gj-(Da!&2~aY+mmUuTzVlv0kgs+FB#l*yyN~k zOvtY_LwJ6N2)SN15&f(R%ZW0KPY^f{QEwg@=XM%nPdWISZ;};+Bj!}cHDyGBvOiTw zrf=&3fX0sKv$s--3)r}~9yf(_`A+@RO#&Q95WM2UA_(qdz+ojHWq*74@%fFsj)jLY z`J%LUduTQnNIl+elwZpBr3~{wM(y0aJTWBLX8AbnmIMk5hxD$a-k{`ERO^4lJ4hf!Clz zjD$7UxprSJasM9R@GEnaT3*&rF5cLLwiapWdsS^?BqiI+ZFB`cF1U+8_n&AhAg<2$ zjsnhT4l%cbC2>vPrOZz5jX>gl*%+Q@cl`8MOio!|InCu3KSf02J2yVNYrCIA;Fh5p zcBLhRW**b|UE}I)woAS{<Y>W36OY zG^Z2TCX>l%c-YRA%YgW}yhOC{%n&|lhx|$p-#r>8C3DwttG>Rv7YfWlj_J!xMU1&_ z0xYOGeE`2B7V(Ue{_o-1A$)zWREMk|+obHt?o1O}z z?A;OGKksNx-2IJr5C&# zFvp3}3t#oDyHWxCWI=3BOw7K5*i!>|UleU726<&N6Iac}16rwukr3Qx0wvNoba6&Wc*}nK7^Oq^PwoT+v-Nc`$$xFL zKRXGt02AUKkyK*_1Fw*WgPqjHbb@Jsqk_9mv)p;IT{6{dZTBi`zo*Mi0wSy z7{h6I(@|AesXAog7=nIU6kHjS!Nt^$L!eEdx9v^=5$_7Su%E*`%czTf|2ha3Q7 zSv!w*v;oF=Gu2QAV+(+{eL#2dpsKon}00mrQV$? zVnr+)}RFJ5Vs^5@FPW@n{0$G=Q*yo*RR7GG5W46I!bQ53rQ5p!S9G_2& zlrgoZgb_Sv#tj!oNZeRMgr+L zxtN?wdIH6ys>vt5`{5w{!TRQreEM$}tPrAi^n&1scF^`p*LszOPcJe65b6Me+Nb`& zBV&%wYXiUZ3M`ptDVJGC1ndr)$w5*pb~pBK>+73S>Opchf9^seWkNj?&uC3$C3vCt zwFEcbKb{|A>ht!d65QVci@l80U(NWUR1Pg~$iaNh&`?_=#?K0RAgPsdo*h6hWD6Z9 zG{WKLy(pTNrp7)PHp*jk5=)F~efOw@IeLPk`ux%^qu8oLIA4DDwXSo^-;cKEY2S5z z9O|HQxK0u1+#7@0wYaS{jj>-Q+B3Mr@~;RKk*XHH`JRTs;zzWoahm(%ZV>ASl4Uk8 zHO8-f0vXG_{v+A7iuWK7gUAGw^Ue!zb5b)eG(DCBDD`s$D28(1G-a~aTIO-k#8&2B z2Tg^FMuH_E?ebsxDi74}*m|&vG(sOeHVGSmskBdi8DPtaMsAr+OU{x@pt_)RCtz;G z5E4G8Y$ii@2e15?_Wv7o|%NyblNv2x>Z0@%f>`vddDo~1wk4aMXSq$U%ALA z_7s{Wd3KO`O4Df(%z>u@yAlBBX9|gryWhX8^tR>}O(>@8zK+JDMVfd#w%L4>e3P@Z=p4hM7L7s*JxxwBq z!7H(;`IkUvK}gZ2A@IeEE4vTWo|~=s6!%+I-st3d%Jdi3SE%9Poprh69*^5cYeHmw zx8OFOH4qp1G!NULw1UwQe{bdTOYpCH#b-f6G;p~8j)#d^Dc+s?JcD4&^``#(f;>IU zuP;1KneHo{D6J1ty%p1Vm(x?P3U)#gOgns~+Q8@uq@R@eeBa+c6(iINKyDidwhI-4 zF56z%`oh*i9=&NE@pHyVxk_&!?Nds0rRp6s+7rfe> zn`zIS-Jg!1d_uxXn~JQMs@!NM+ZkF6YIEnPjt_crhrgnz zzwSkpy%ceao!c$(>_b+6-Av+0FrX4&6NR^!w`0Z{JE=nV;^I%n05=32HA%XzG zATz1h0J%ZWt6F|W<`=grElj>Vz(Ue4pSk|DrOQGdI4Jr^v_&q6nd)$v{Cb6#@RcNU z`^Y{9O$+lKBNqfnO(1tu-i%p^t&7R)(VSrLQfibT|Hcfs3zbPXyd-tI*_8q-A1~(S z?TI|hFq$}H;VL5KvxHyw8hq%d{EW|igL-s8&K9zeT`|6RhXzR`j{EKG34MkbLlN#B z2XE&oiB=Glb+Y8H+~)eknXchcdnx~h0(BzoycUyUrdLz46#c9)O}~5*r*wg47wZ{E zd0Oim-U}HSlaB_{qwiuSjD^HeACatSh1^;SlBdd!T4-tL%YjU?gROz$H?$kQRDt}+ zkiv4~|JaeWZ*{|H{1?>p>9${eFS1E7{l;oU4p_3pEqnyj3*~7VUvwiqT%L>TYWPlt~>7Vgw6+|(~Tq#zq+^#tB#MU#@$DHzz zgs=%)K>~)j8;Jml*h$!xczf-T??ogM>7m`*IW;cbEOsW^7OxZ2_Rzpuoz4j5)Q&Ed zA*&Y{Xf~%HKN@NkijTxhIe;7t;RY@1Nbw#Uom zt>aYO@_5CnfE*LeS&dEEpo%Ull|V_y^){&O!yI>Fh zFn{fTyegA4*x1B2@v>vzZrKp9y^qdrKPJ`a4eFG>#DCM;M?c@J4K8H!_UKY7QP-Bo zdjI#{Imb>=;N`B~2xe{CMqtm{?OM8q1`R)`TS#c)RDY1hF zp&6B`;f=8*qX{(Y$}2_%YPz5p+uR8k{fzj5UmwrtYjvH3o%tyLkRt3&96{Woq_v

|9x!)C=zTQbG@g>y{kWa}(=j^^xu#^yCF1{qKyi+Avw1V)g|_oCh2=P>RI&zIw8u zXr+j6-*+)6#3CN?eNr1%xF!tskqDjRjs*KK#>C4k8Rp6OnrZr-oWQv24X3FM?Wmif zXR3Uh6=sDe!3ba?5v;M(=!*({ZYr>McZjFQ`Qq zK~2>G69oci=ROjK^bBrNPU}UZUFO80Uo~eZQ9mQ_7Xwhth!+{8m`tN`$)3k^6B@mB zhCg^&&Wt9Zww(O80@S#bU8U)9Z=py}m|FO{5KqUb#w%?5UDs@@~@bRRUO#+6nfE+Xr#T4b+bImB2eyk=C1KXiqsTJ9|AyIT_P4rl%T za)H?vc6~-wm!porn|8JDZeY0>zd@}S>jFOEm9B0x!RYRh2c!{4j{T1{52{yqXeJ6G z!R20#)NFEaoL*0-%`PtMQ%xzmS-QTlo4n&nG2owCqFLxarbVf$F)Tm}lt2GXI(cg? zv2#qvc?q_02t97;!|=o#_0zu1$(_WX!?4FqNev0~d_^SI`b<7iao*Z<2&3S~?p|Xf za}1fS$W{xEtuc*dWG>pgnPrWMj{jtdTfQvN#b<^yd?&1IPx-o$7{Yk+G41?AX4~u6 zFB&+!V~@mr;hx!>rCcoP4OxZoLduW2kOd~$#qbHCmizps=y4TcJu1=F1AvXv@VHQ0 z%J$fsMqjrG-3iJjW5P;Z;!bQn(G$pO5cr?~3>f=o?E{@a>hV;uUWER0!vocdt8Zfn6=!WBJpLl{U;I{Zf0-YOFYTlbH{ffE!WuVp^~XRnvC_tusj1^ z$dZ4Kw8#*-$2j@0c+5_UnNe;|M3YD&$2suDZ6dk&j5c897-HYjw=NaLmde9EQrom* zTrTDWG0>ZR<;ix>)_2APHwcz9zQ>hZEf@{!E$mj$41){3icG>L3d>6I^982jHLA7d zw0jKHrzMmzlsO;l~$#-GkMvpju#Iqp)m_cOM0G17-yddDpLuKQbS#)>w* z?s{eOl(#JJ+w`&0M?X5YRH8q77zTZg`QmtDxGv-<+^;vyCca#jZ|+m1|HaKrqcq7_ zqomfMUG9oTz5}OpF}FWIZZG#6ceAcBh3KK5fH=L1qU2-O9Pd>O6+YO&WLHuP{+v-F zbjRjCpS87dqCgz4PY>!H-g1Ww`J$JY@KgN+!XFhI}M;4CuWs?f-R=?<|pq?gB4R{ z*^0p4cf8jLRs1+1tk20$2#@Zn()mq@32qv1y^El!#tr4aRMJKx#A$ z!3h4n$Hxk={EVm!h{3hJA7~u_sNfL1cGb_t=8qG>#zNWeyG$~ozVbN!9QJ(k@csvk z6%gd2P@5o74%P;b8$k_#+sDy<+CsKLg815|N)|ps@W1EfSsn-A{YV%;ctfmNCp-M= z7_b&?>t?FYzHOHvPR1{Lu*}DbE_>-R1Lb!te$Cae%N`YQ((u#g6c$ z(@qX=_5YgNHgszoLbevo;CmbJg&VR?rEpH|tc}EN6=qXzT0e=wl5?*pF zS93Jho<~&nbQ)V`XX5#kEB)?q!9N0{yUt#x zJPsw+|5o;&vSF!(PGHk-95A`4B3u)8u*gw0{p))Z6&IZMD+AIgr}Xtz6DZyyO}L2^ z{0|o(&?h}f#6&vM+4IZ(^gVwp*8YV5q8C{xeSd9DkCb(qetXEI>#9hZ%Wl8ZTMi`V zX%U#aO4?!txSsgwGEwe%|4qmE`}U8BbrA*NN%Xyq3c{h&fswquGuf8U(>%ckfgZ8U zjy@P!-&XBS{52?B6?(5t(3%Fd!IZ`rBH#!+1ASPVa#dw*r46@^I#TinYfSE++8ZXd z6XKxsp+_c(n}yr2?!kVlI!dq1+P;0KAFD0HTK;-rF(tjaH!b`!A{H$|Nemjv|pOC%n zYpdJhc<8K89m46;uT9Dw1P|8htBRvrjb~=wXDdP@?%tc1+J+y~iO0M0$XJwW92dJo ziPcz*s9?C^TBcHlVz^H)MTn~PX=%P$v9LB!@F;*t^vMI|nW6_#=>fBk{X+QsyCLNL zH=WOK{DlMakn{!h(X$$$XZGj94!!k2K}A92tR`7eWqsf{y<2x_gWm?XSV}?XCBedA zt1IRfCcv?om>VTeXQ*6h%T=c)ahA$swX*GQj*t2zKM3}pIPAaLVi3i7nB~!#! z*ME@<;#N%5Pb6+VoTpf9^IXxxG`%TaaP`!{PFq~T8T1y(%g_YkaICW$;=&+F20Z(5 zK&|}WM)=)_W2XUpSI_Ym^w(w2@<>LO1+Ed)>rpItq zaWQ#p&1y$aOmTm`8i`wdG%Y`SVB<>5@`|JNxWr4-QzVL<2AEjVf0pO;9wge`*3MQcpH9#L0rWQpA!g*!SM($xw`~8M2$gex|NsAe&RZo|4+n!(a#1I(|LvfA z>j}Owh|xI$6x!zJ{|6fEAr80jr3B_*!Y}3D^XgcsM-WutqiG04rRC0#*sJY`y*OHO z<*wB*M*DcB);qT52{O$Njqyuhi_#QwMOr z4ndUru1wa#`LY0lyQhv_kf=P&Kn=U6kZsNZA!`Npv;Te9CmWCL$anoOBlmwBzpAGf z+a!n=PKlQ6?k$rA8r($$BDTuc zRf66m8*63K6bzcK4U8(e(L{~L_dV|%IJ9WQftCL|mO=Em=dZu(qYMMz=U250)+ZR@ zn7@2L9Cj_p=TjDZ^r>`?(+FblwXD!h6zU`%$moYa5*fnFk(COE>^86bi&fOxAUmL+c~ z4m?h#xyhIJ4=QJ;B|g5|_JEeDh#gQQN=bR%lv4{07?Yg8cR_|!X-g#S7 zryFb<3Z4WDLvS0XBD3c!3$u4vcog$6Y`Ln)Jj?uTrE4XNrhAWzPaN(a&a5dUTYhr5 z?jSIycmIqe^NoH0NC$zs+5x6RIM4{@e+l~e=|~g3L30%0kN*r@8v;RG^?$Wu@`b-wk|w-!okkM@GL1q?c5O_#z4m+r599f1Km*Hb$pEO>MY`7bo7e$U`htMW zMa|)0V;l}w1f=ZG)h7q4+JWZn3Enuc-aQe}v{#iuF13xTB?n9Kx&8sT{&Xf80}3ol zu1wp_(-fJ!qieNyWD0*olRDC6{Wj@`ckVeMZOEZ&)S!IO;gBh_+^^$AX+S52**J zt3`0rBE1-RQeobpS=mN=oS_lH&iiG)p7K~Ju-2nfXt3$8PhlrrF^3iSQz73k| zTjpu0?g(?*teUI1H+U z;m?4o8(5|I|7-1i+u)NX5&vX3Upug+uG{Z(m)|f<9sbCh^Eog~;CNY~ zs!{K>!^=-NnW|)kt7Y37|dU*Z1w3+H8xyRW}%i3q}yUt&vlLB_-ef1lT( zZDt>yA}6V}5@7$Kz{_|Fa3OBffC3N7yg@O3?##C6?*}TzJ`;_Hi`?4|d?;CzGnZ|X zU6Y8EeMjq2d?j_~k6$Kv@q=s^gg21wg1l&zt?CZT;i={Zd4C=frWHINfxA9~TEjjn zLq;4JBiiQLh;KJ@<1ZRDq6-X4Dvx4QKimzV>wHa>Puta@XcMKeJN19%^0c4|DgErP zF7I8FDXFi6k{95Z?10-EnX{Tz62_9lR2!u##G@qC7K%@rGcyxY-L?(mv#6E8Y?EGX zI0yr6W<&h%x8UHtAz>}6Dt#j0I#)_^5{2#e1)uD9CrL9v5+vSQLq zh^3P<9cop!1G6I&fZ2G<>Db$5gRwc`p*(KKbC zK!l^Nd$bt*i4jlmxGn&Ol<_O^>;ui{d+V#ExyC_>lDu&_Rwf>vw)kpRq($dnDn|$O z)ASr`rb{a!B{E3eQv2j0rQVluNcEW3!p9mIK{|wkLI{couj zf9M~j=!9HP&@7=H>TC*s{~0{{vK=Ybd6p>LQ{l3EpgyvGB{W|*r1!&~^&c6KNHkf0 zWLInsz^yQn9N+4AyTRlSqyU(ZS*PTxd3%D#(okkn7X$N_4xo1ft z*@(ov=#ep*h^2yLv0bLns|=5*eur0(y5dk21BmajWN>E#SSGpSu>Z(0;Ly5(qs5aj zvnzsU>PMBG^}X9S74HrFP)h2Q7e5?nn?*))sYuxjQof*#kneY6h!jo4&UwDfcY(aB zJd%ay5tMIRTVCJ{OnQaS);Ke~70DNKOhUud3}}1b9=}lpahASAKU&4~xcctzlP|Y} ziqU~T!}m{J-UB7Yt~g1-QaHZnOk`KkWByk$CO(b941GZk6;) zY7!mb-6srK+5jrA#gG_wT3`7ocV@Wmuxliw=^Z+&L0IC)z7>6SZebS6r3qH^Xj)Wks0Ma>1 z+6$n{QhRvg(a!s)-j~w~VEW1duduz&8zhI7YSBx#LY=V4R(z2f`yU*@z^|d@O|OO*_OUGA@!QUtY}yf96K@?j5@l%h1jnW|&|d z5ie6d+EX)vXU?>BJ5d^r{1K1;Y)Z<5n0;r_(Xhu`z8V<;oc0$fZ0rYAsiWS0!xjt>34x6Ln=GiqF-l%3&|jt z9zZF@m&#BGy|yv8jno{pJdCOglMTrQZZDuzE4{p|2yzvJh(^CHju6Oy)=0?q+hEPp zAzVJN$Bq!e6&7@>+Hw^YT=;Y&9K*lq2e^0srXNo0v7=ezC-zfsUe8%3TG)EN_$lH* zjU#p=l2XcCDXbSXX16*WardD`Nj6Tt^j@syJh<#UcKsNKUFW_zJ4ER~Eur!Xx2>+z z2%8kIlk85OX-9^_H8HwQtiBA|W6_llX7zI6b>Gu5;BucO!B9o8jgw+I~hI zi!xJdmVphtEM+>QigP+vA+2DP=NN3*Aibd(sBdNPOX-yMs;CuZ6-Z<9*WTQAGe&5i zMV6QMo5&D2xMNH-g^Ch-qaQXd)3l7-yozo^qnnk4TTw+Z=%z3EJuhq?igR91A5P%% z+g-{5Wt(Y} zd@n>c#Fp%Un~uj9ZzT{6@fL#;(%49F^@R1{1+J5}U)2q#1~&PauAU=9k zThZsS-*-|?Vf*-Xp?muCt+O$E6f_m|ZHM~j0zF>4E04Q8b9Qa%bGLOoNThc@H0b~9 zw)LmL4LcM>3YvT>aixFgP(?CoEc!pAE^xj>J$El&dZPok`3`&AqT-ib)BE1oEJY@S zEt8Cg!# zE$c=p`=A`-kL_cbf}`ob-%mF|9e3W1=t(5Ph>B(duk*tHm)F^%!d`UA(9`P@_R$u; z$fFV^7T*;Ut0lolJ4r5L)L$|_QZRZ(fmha?7`eL>Db1kZ4Zu9G$BenT34$Zrj~+k> zbMgP&Jm-zJXT2_9kK`Vllelhz_1Pd+n1CPXOy0~Jag2U^D(1Zv(w6Sx}w(xo60Gi*rwbsNOgT=d*(pj560Zf45XByf)VQ5civl1rRvSRz;1;UKJW z{{Cx6v7Va~UEkLPVdZS+`tUFRB6(>rk-=`{q$puc$8INyzn*P4M|Xt4dFZp@>`!^l zf?K>*->c%4ElSj6qVy|oNR^K8*G{^i-Tglej8*ysJ(HYX^!y17b}SM0m-DFy#YriP zd>4z8K8}FdS2e@u&bpZz78%+JJsXr07ToAAWNUHKqakU>^@4$I3-vqN>-l&b7D1hU^oz2qpUl9LI39i=^))nqp$-Aa(KRW_C~mG3g0eH`qLs zr+492&r8@4=cN2v;86_(l7F2-kM=gi{ha;ax*ol?N7IpEj2JLAuUAp(8tzA~)}U}S z5sq#K`3VYZVq53z-E~l*kd}A%KzdHu5GxTVk9VHWAd#O6#1+pz-Zwkd{jhB3eQ=@L z!3&&#d*D0Vh^D8cC20t(*d&MXrV7#U&<66HNXV0eFj=0PsmFiCHoa$B+?>TbhYH8)4Rvz%v$=!zfYXrp)UOaIKq=WUBp##Ry78jJN-TE?(5E`WjbZ!^^n&i&R+cH}LjN!AtQL4BW6 zG=)M$6Umr(<*q=d?fIdmwn8N$*G--uE5Bqia!U8+8y37&iwJiKWG_ax!b^3%+pf^{ zRxekv_cB4D;>uZw4rx*x$M&*(In(9$+6wLN%h|WTNs(AGWB+5)`>5dwbJo|PwA zz|n7-xm3~$GyyVf%&mtlodw~D$icqc+9-W4wsz0-)^BXSjj*n%3P&!Ib{@jm3TglD zsd~myz1E$fnn)iP0S^j+}yeerPHai7jLE;;#@ z@1k+`OK-qgwDO|FX5E}HCglk>MXTqQHv6w|ahNK_MH-_F*}MrwM==HI2Bj#Q!l|zP zr{oG8=WQOLKiB0j$v_m!0IJPifZAeW8e;!O%$s1~$83^k^;PovZ?cWdF0@8IVsD8T zeBaymb}h)hW9W*>`tHD%7^3FT&EHq#J>1egm0Tu^tsd^3y8!YIQK%U*-kBz!eG zqJ7ot!@ep7*jMgtF1)tr_?C?0%ksWH9}W*_g)n>b^>-Ed_(_4R(=Rbk{o*ExGqCf;`6kv$}|_ZmE^p;4`@o1hLaql z6qXkc@$s=l;@xu}2G+f`1J(x*X33i>P0XwZ^vt9O1$}DfQF-qvzSmQ5)jU^Sb5!KI$~ zeRdC1Zg!ncY>j36-sgI)(wO8O9q_voTjT3RRTwE81sGd#+nF9tS! z7O=>yq0@=Rba&x}n~rykfaar!^ttQ=W*g%BgImLd@TdHa{@R=|LS;s(0T9Vp+G!z# z6@PmVY|_=)bm3jS?i4fLqwQy1n~QDmUv%%Esma^(*cH#fCyTlXq1B}!UvY#h8Y-0? zVrZ+~ACA!&$Bpcr( zC*T$k@WKBLUmUhDsY$M50ZvdD7QG*5h#`Ev0U#!V20tHh=g-;rDXQl7GZl6zy-XEC zw6#pq(5aw_4pw^5W_4(54$9aq*)dFnH_G=Q{!po0enPjz-!O*0H&nBYogWO*d45` zV?Qk*r=i5rNFi>cF~p8!FM=|qjnKf+UBn$}Qm>z6f;)ZZlMSlBYVTR}nwlr0+)}8_ ziv>KWB5#1@pUVoPuOd7sIXZ^=1JsDirtm|$R2Go1+JM932}uLZAFh>$^|FkRPIWi> zu3KNNuBMA*y7Z2O47ii+RFcn zt@i+GYX72tuL6on7eqiQAt*>wdX*L+3DO0mt8`F$?_GKcO%Oo>NG~EtQNRMyQ4m28 zq)IQ+JG`CWd;d4{X5QR8!@Z+uNY445z1Lprvx|kzciY(lqJJbR(kh;y*MU+25)ARJ z$YsumEpM(U`C%`gY%?CLak1B9uFE1cmFz)i<;#kyG+{(T(l}D#h$vbv{AV@lg=rNU zHjZV`bnf@bt(Jkq*?Dn*E5$vW0Ix1C%soEbEh%K%ik10;q@E2zBT>|U!Go8{v0+4b zvJrSQL@%yR(cItqo~?~k9eIKicW9;Zc{6TfaOLm&3YP2$%jT8I81%~G(~Q`LDnf6i?xf|(%OC3_c>WIwp#^P4G&TV6D_aHyW;a@|?h*O_P3l!tao$1VH{&x2Q45#h8FkcEjnLb+dy_PV8JvA^DFG`+0)OXgs{ZMFz6go)Y>51_|^KR z8e&b%^5I)&!ox($mYK)sY1i)U@8|0Nj~C#)S`;xJGU}X*?Yt~h)F>alX6CoSXda`+KG{*KO{PswTXgK=E zNDcl@Sz%mGw(FHhYlJcyA!t2~u{cl3oJ;`kD33BRY4!dKtp*+I#U0xJMbot5k-c8K zZa6E=r@x^aQ$#E1@V-z{v#&vPa4)*~K{`g(h!6%TS-Fyz-tkw# zi7jHQ7qPv3RBwh`YdW)DGi<~QzvmZmOdslNuPd!44?6Y11BG8qD+v^E?7(?(F&~SS zZ`g|ZK747;;!7&8^UhHivZenuhSw+H0c(+Vev||z0B8OEX0An7xv{N@{$-k(F}?T7 zesSKira0@(USk2zE$gAt2C4PXKtCQ4+R7$z9}2Et)#*iDx&PCp2na4K!h2V97=gHD z(9vL;P>OHd-Ce-tJE~l%dS^={#&<#Nd7zI_VjLnW-<9{~>n`y<+(ny3*%6F-DW z-iS1=U0UFLlIa_#T&aW6O^kV8;oS;MNgLy6<-1F1d>6DnyRVM(?>o;XKPFjAFw}m% z)iKe&KE1ft_>2ORVW_PV$D^OO_mV_9YfhcUxXD?u{CG2-ksml$<#$=VD@@+cX0-8w zY64?2Zynv!cbJpNiVI{?lko%T8N;u_IlDlklXvX!=Xw4*yq$V zpyTz*Zw{-}QZ3GaroxMN^D6b(zv&rYMaJAiIn_yNAebMy*U^+C4iCk$aEfPVj*t7)JeQv$TUsvbFs4$_^ zv;3wxs6=~@*)HxEmi{XTGBe+o*p)jXI<)PdJJf&=|7aozC+#wethg@YhBaZ?eiM^* zcRJjKs%*=5bGHAjv0>1>*w*g2R+Z*76F$Qc5qCU$OkJ#tt95fAJntAj+%n}_c=l>L^R2QB`ES4PpUTd#& zXj;D-q*HW{Azge*`lou?&UGU*pJL3`Pf8hV+ZiIMat2N1q(4^?^=~ay1Ws?M*W!d< zh!j-_m+NaU%IpP_oj%E=K{W&oF&V$d{#GJq_kPMC8Q-Ga%NdD(=SS$VSO*D>*a5|9 zZIfAm8g;$5RJm5q)}rQ%`T0-e_r#;KyIyj0oYB58Z;e1v%Q1pn-jgT{qre=_4dwQX zp+w7_Z>G=K`64PC0BnP}?|k0}_C1MJdnO|D)Nfk#W~W7|*Es$hUq|uky(<-P7~rI9 z4GxdUUVJ*z*&riw7&^MJ{`!^$nWj=0OZnRN1LK=q8W>{DsVt=e$>!Q$#v4EOoUm#+ zE8?E$P7%A=ZBIYu8LelpRr96;g9J{PzLC6T^SBZqGU%X+*Bkd5%5@Yx`;oEf_@4ro z>4yDK8h4HLLR!(?t5p}Zco=zU+<-iZf8%m3gwTKz#G+P_62(>HF}l9io}yXc=jYdV^@BMb!IHOh4B6+L)) zjza76*Q<0y3pjCwa|^$Va+B*edjr`cT~8GqnvMC4n|R(+GT#%J4erY(oekaD361Rc zX*nz(VzXlA@yK45aAfT;`aTrS@MC7$=7;%Y@z0GF^;79E@;O#~G59Y8{?+r{<>>2- zZSEv0`;APe91KU7>NFo`pmZN~*Jw98Uf=~Fi5Y!&lAg$uv5Y)*lnERD79+V^WN)U} zBil{}4oxBVI5WcNiUdltx?i((X($*5G&t1%`d)J`@RpZN``wbz#Bx0g~Iw zn&#PK5xN=aCQi3;^XY#oI?^_qJU(S?_B|_})d(7KGdBHVHaF)dh25Cz5btk)DKImtiHlh<7Talw zvMBeK$+udm%hHrucAjFS#vo6Pl6^V4{W3x?lWNh3KS~o* zrm4Jn25yv$7H{H2Nk85w5c3NLd#`}_b5HO`$t5Kk;e{P7-28JEnd`i1e5M$!C9A!z zv6=dt*{+`yW~dPfu0BosNdn9H_M&J={|T2E>fK7^q-wcJeD$IU(~{8S_7@pmT+Bj$k(ojw zDnVAfSb9DqO-)_UB$lbxK50W;#KW&he@sHa{~zmifCpx4=vy?0!bo+w*%-;duy6@d zabt$M`4{A%;+_)`hPJZvP{zqy%~cCd>(e~d7rFP8o;@N|2NNk);XkSWp7`OWkpZS(`WGq@7C`9FvYUEoC(UVZUqhthW9)s^1o z45l3}3V;4#>%I36^DK@J`tx*E=}2;4=;N$MU!B=NHMYz%RYOUK?g)=HJ{83xe5u1q z3c8Qgbf5h4ROn1|qMpP8)<$k?e&^ur%fCNG${Z$=^PTDRMi}rpRCHmM$*4l28kau- zeFmOm(u^^<_FxBBivRH&-Pu9hs=XdL>dP7lg2N3${ZcaZbFsq`aZfaQG`}?q!+PUF z%X?Pmv1Bb;eP=)RDC@bcBAPM-ly_nf7Qac)k9{4Ll2j?)2{nJo!kG}kFgF~2khfjbJZKZpDy3QhB#Er*D%_E~VBd!af3W33#T+&Lk zVJ^a9?&Wkvmd|Zo;YhbbSg*SN=;LPbJ*oe&F(_GHQENAfuCnNyp_A|B(Ko8`ccjuP zc-@`OZOVV7Nx{7Td)f4_+J9O~Em z>~J#N$PcUV9rCG3o?lvcv6Mh(BczzuD|AvI~|5RRr zdnbDdpi~iT*B@OjDY7!4VifA~B!D+q%+h^v8HpD64mRIh-uD zZD~OFRObIFOOQ_}pg_K`-~h7{lfZ+ynx#DVXL05JH=FLDG6u`@Pu5I)N1i*+E-YA@ z9qe9~YVAHY!r~{ID7}OTu8|gSG2-`zy>pSDi}^GE6dgq>|{iB;Z1AXy#oCUB=c*%xs#Sykr;9}=(0Tct(!g|JL z?=3bm#YNlS5?u;N=mx=Py6f&q=#!DAF*p0^4B@|jrdsptq6=EmkOn&t7S4S-JA8N$cJY@W4iRpP$=&`vj4|- zEe_QKsW?9C*!i5?Q*N!;`x3iKP>YAPJtA2DIU|C9Bct#Wo+LiiI&jZf0fF+EdUt+$ ztKLQHHAcHaI7}!Wo@MSo*@LBmQ0qfgNMw6Mk!juHwU=UkO3ruxjk8Z}?;9LEzm9QT z{Y*l~J5y$YJ3LBLyT)%3O|NI_zj91z&$(E}9_5QcWd1&0@t8U0%06r%V%S}I#)93) zdAyH%TY{9Raj01SUpalsf2(W!&!Nvt;m9NEDFWsNNd?j_{y(|akB>lWNVU8t^W{`< zu5A7}9n;^0ir`TQn-6QLB@^SZaD)hr(kkXSZF&qw%J|cxXcP~NEWJ%C;;(Ty_(}{e z39%unta?iq$!h$U?{MktthXpbBY3p3A`%$)frJTaQkLkCy-^!Eh@8nY6Z}y>J#-)Wf z=i{?of8eYa4oq>Gq6a~(h!>`Cp%y;TYq&GsnE9LbIU5z(Vh^D^xA}H^GDntWXuky_v)aMfmuos%RA_*Ee0? zrEEuZo5qrw_q`kQF>h;X#BrDf)7Ccz&U9wz*aJhM;>(J(E873i<$b1@pMkw<3Krhf zTl+4v_ZM{jjK~aCbd9&gm#k~Ou2ZbC>fk8~uCu!6zM@xQtgZg|x5r-PtMBd|pTme! zxgQL@Nh(0G`0a7&hx5N0PhR6r_EAsBXYm12z^KZf6pI|-n- z2b<3f05DTP$3>YxZkoM39|#{``KxT$|9j3A%L;Q+Nj+x&y+;c_3gSU}W_{%^vo}HH z1v}DGs{a?QC%9R_x_q8v;@`O=*bvwG(&G=q_vZu;70C5~1aQd%b7!!Z!|!*hFAW>L z-3a_=!o)mMc6pkBO6W@CPXqn211c#Fn0S{vfwsn4V>grvkV@r$Gr?fHG2GS%g2m4v zz(=rr&vg6_MxB|QXUtkvJPET7>I5GVAgFA7^_U5-HZa|?DNXdB)s++xh5 zV$|x?P`|LbK_@=bqyiJJ5BB1AG!e9Cjy^n_nFqSY0aYBY(AkHisqpWUN$Ln;3@tNx z*7Xvo9fL=Q;Uy8cBtP{Dt|fv@JoX^h0?bt4zhy9wPm~@V>ZucHA-KL_qo2Mi5}Z^0 zxyqDY0IFaYxD(*anjpu{77596Bn)K|;V!Ami*By}rAag9He`&AI@+6#-ym4G;Qc8< zJ_2ks@mZ2S`G(l*lDdTb^ZT&N>+=;P4B!aBX)tu$a`>-uhH8DroV=V#}LBH9*Q8pZy&w8`pviS*H^$sNP%dT6OITD+>vnrG`WN+iz#prWy zYso~Pq(z;>BuV)7CpZWWJxrGW>K{U_@O6_jvU~LUj^f$g8?Q*H5fj7RY{2>kNjp6d z_dfv4=N)1ZwREJ%J6rAxtu#x*>3vf-#>n~@K!(OaCcmLh7`S7K$ppN&-wH4fZ(X+m z6?^d@b&9AlIw%8XRz=VtD|>f<{> zYJx(plQ95C(ST|_*jYFTvCRYm*FlkSa-6^_H#l$KvMy?0Zjvcj>cf=Vh1duOP@JBd)k(<%o&-W zD`wtKB|J`WIVD%a%PaQ#G!6$@8n_IL%vgUDjARi1ew&Z{yiLFeRIFjLnocWC>qOj8 zmz%5-;?7fpGe&@pYnFE;640Bq0Xx(bK^h#1PWMp%ti3{RMogYsON^jTe|yH=Ow2oU zTx0UXFQD0um^AOr6Pbnn0GoYIn+t~3om7KL(Vm|xie`ARa7zPG0uyNuzbdOks@sPh zmJjreRPZp=;bJ3p3Kp>);Pe6y-ayX*EMClm+1jnv^N%l>oaA zV1&$;r#b0{&Vys53!}04p4j5mo&K&o(8>p_@hTd^!&n8s(30_v_nXxyh7A-O&U4WV@kF|0G%_-HCzd0 zMbghoJo-#5=gk(%zC%4^30#(etXxYS@PQE-D}7(EJlP>mL{UY3ZO~45y-ya;NRFam z^CQ!AE_@VrCvk8n4|BPuZ6XDjvO|ntEU0dmvQnBi=qG%<=3B=AvE7Z>DbfnNzTEhq zhz`&4o0GW~RB@N|NWC&{awLZEo2b1cfS}|61jRC@LCb+f9GOJQe2N0JZo6sjwND0T zJ*$Slb|mHPY^A?eYuUlggFi?B@M#s%N^Fnv^7rhr?6nd<<0L)o<(w=A+=F~&{pdyu z5hUkCry{Hase*-qQ9+r%0P%kyYCm!53QuA9DlD4DGW2f;vD$){kpmu~y7(KT=j4J& zS!h)uXYxLSs2}Irz(6cd2w8Zn{judNrQcLGrbi>XeSj*qDC~Q^4@Ha-*Wq%r|TY#D^i$a;Ox2UO>~^8F^qr zGw%M1hJ(L^lxvw9$zn?-x0q8-Z60~YfOo)+Vjx8s@(prvjF-xNl28vW z4?e?BQKjuE%9=+1nwASWSHyT8-564+ICeu48fD3~xO$i9WTb#cYT^>W!JMBA3tkWl z`j13&%bn|wVqzT7-$FhO+TxK>f806~OWCL*y-^P$S2+6MO~`{`iAm9PIF0;A(@nsZ zy0E54(esyol{F`C(S!ttF^;r~a1Ni<4`WnHP$GmW!jhnwRis?l1g!^@jG=Uk4XpjdI8rWsiIp<&-3AW`M<7sTMnKIcz~9V0 z2ky$}-LLC;f+6+_b`>POfI26a$+m&|Mi+*EBSLx+rM_Rh!?HYr;EH6GXS+#_3H6g7 zpkCx-3!Vp4nuY+GrUOhL11f(sP41!5c#R)Dtm_&vjN@i|pVk&Soc6L>_(>y5znHL- ze=7&zQMshq$p_BJa72>=8FUzu+*B_o22MFqIFb>u+a`yzLQcgJaQ0g?0ib+T5+#ibJiZXB z&GPl6`9EE1Hj+bsmy+!#do@w^~gaHh3{!Tcp;DCQW zwLIGGbMD}q!E`~Z;>Xj!*c&OS9yZ8&)Y;uJq`XT4Y?~~s~y_3sg3`8R#AWv%hDhp$ zReX; zj?{0`#=NjTmxCJrgo-ZID;H*}V1O%0wNBbRh_1j-|uU(8@lAq!1j$KL?2It|~U(XnzT7#OUE)GnH zVqvmU1(L&0Qj)%teAO8i1VK>TYSjT%fJOu?RZcGfa&I)Hy*m=_5KaNTD?S{s zhT@+Gk$uJR*P$muxe8GgEwbtgL9Sw58e38EM(`l1X_Xl;Q9~_j<8N@IL;mu24*~K_Wd=Qu(YW4+t1#Vuk1k&iK*}U z%0F#~loKdui{a-Zp*c~4*OZ&3wp5X`b>{BJ)@N@EIT$0u1!$nG)G^NelQW!;AnctW zxMGiCb*rB1$x-;Y(i8#)vy?Z2mz5BNp zOp>-m%U+r63dh6b=-ig>cSg5JjSm@IR&q}-QNH6kGk^$Y#d7LOJr>MEMqM>=s%PWT656bQ4|L3hHNSL z$x94>!JHgy>Sd1n$}b2Wlzi=%r&|*8f4l%(Z9}C~+P&Q)w|C4Kg3v>V{!6*YswdIn zw?SDiB}}Sou5ULn6E+CQmb;IZPTb)6{$fS;VCGIQyCe847GP1fFLaoTMXzD8Ndgpk z=-t(^M!wn85lPB_g;5ohEOHCNvkuA0SxoQ;>UoYV<%t_#M*#3Wo=#9aOPp(c6J zC@g8?ZgXJ2;k@FGnSYpKGXdCm(zqAuaiOM@f-&`7?Ckos5uGPayiTyc!TjLdX6j3a*w877-twn{PAkYQ`1a!O*t?*{4 zPzATsoA6AzYBg|?@a!UR^RNL4mbYA$8@Z+?Y5)CI(ESo!RbxT3uVg^$}^7ato+ttdGzQJPRh%9+fPFyVWtc0 zm6%Z2yoeA~LByWl5ouwtyDYv@aK2(&>1KK7@|=Gy^H$ zpaYr-Pol^O1cVb(Ni^?+q`?u05E&rBrVHdt6q*6!OoT!1_N41HY=&ywxjUl~eT6sq z_Ghm4j6I;wM&OjFnFcR?3%M3dRtj0`2I2*G13wN!C=Vm%nVv$}0vjyOgvpETFrc+r zFt?1ete#E$SIdhl`tDPzY91qVR~*ZIHj5U1@=p_3%Bgv_d8F^$Ssl zi!<8r-#Ve>2wlAGyR4Fx=J|^VxVniiX34nB-lO{za&95%_M^{^!jV+s!oU~qNEa;N zF{;ye+!n_R3#qgcWfEklSPaMsUM>}ghVgtRkzzAso&WB=yIS@fVT&mVAi{(WL0`ZH zy?N3bvoC36DG`plU#{S9t*bjP%B`y@>yBqYS2xDgA4%%dvOAn6~%ejVNZd zKi7$fr8G-NybWTIQ9+8c?C_BAO@g?ALwQM=e!4M@+tG*gdl>=UQkPc0rYlL6568wo z9^ym3@|&;zneR6FFb~Vt*shysPAd`qaUWJ;>)g7Gh=i8;2EoG#(XJ0SA~m+IE@;eG zKzbN^gAGqwqldRmK!_QW*~Gsya1G190yy#kPOY+(wlL!n|ch4R5<%~#fkep;9N^I4rm zp8R43O}6^pEAWe9_i%sfD9n*~(ilf+R~Q+5w#K?I zcI5l1xyb@AF{@?*b9e-OM8w#8j}lV)BX3}VrhIkXT{Q;p2q*G`l?n~Kd-n(_G!9-W zx+HErzLfHOI{kOTrYqL2szx?N%7cAseX8PLt`hi{{hu|GnOa|!S#$#xIk~$JMcQ&T z6!fwJBs|+}e?1IhR2amelhn~;{aQ`cLmFKd5+yt-X-K~jHG}3;bix{`LDhhYq*>hc zWN|*Xokg`0at;%^%Z68qxynZ6;rSHA_(i|G_(=xO9S98N0enHEb^rt&;?|D7>I3;b zW*REhl6v!!8=5T?#%vJ4;*|Q?z9XubSbKD#!)c!5RV3bHQ8Z!s4Egq-*#Wm?l-i?d zRl@1@kFZH-bNQkY%^e(i&3@?aRc(p|j#;0f93^tRDOfR|as|d*u7ZduJ6uRQH=X4W z_`N$??W`RCKJ9?+r`KBMlq`YJ0cvuQRu`)JE<C!v6;TS@DuU6C8jdTXl~~o^!Td z73I73&CoQPTI(BYv=%MzsxBlEW|kx)H^Djg57oMsqUN>T2ew9!JgSpA{YFl8gFYrOMOvF`NlINjwn_uR#d`b zzHr_Mo3NS+?++w(T%l=`Z+!zfwBD z{1!#ms#|Z|CzCesT^Uqg{VPRWzAM9a>#W4t`0TqWk{)YV3CY%N<6YF|*39Y;aC&vW zO#=oFB%J`T;4Ju%e=&>D>_@+2e zI)d{{5RD(b_?|n!sKMMtJjCi-@*lIdj`EOaJZl93k~KnQnhM@Rr}t>S#`6alzrR8B zRXrhW$t2%?wmlP*@w1pi`mWt<_mxTMM5;i}29Lvch8GRAX`Fr&7%PF%VDzdd%{$YO z1HF^LN0Gl&n2$DR(b_KCeG7xo(&bEV1*-by-iTn;@_qA&-y6Y5vD{6ov$eFUQFP$t zBi>{i*5b{cYO&N3yP4V&M`vjA!8gaG%b+Og9$V$-L6mDyMZ@j}Po@f$S=u?A)jvl^0=G3&p<= z|I55=L0R6f&>>ph#~ukoH;&8xM&{{`i}(HeU#XGUj5jpR^rkCfb)IlZ%2RZNP21(E z0$Slqs&Rjbo%X&v+N$1XEojEKBF?DU$gytOL}U4 z)-&0x6SkT&mo+|^(^yiWWXqIk7RqF1+WXl^R8L>L=rCR#8k~!Hp z#<`LDKMFjEWh&ef0!tL(a|yZhYS%eaDhS~LX_S7SOBHY=-4=r*DTMzO(U&23qqeVd zT`u5DwN1fd2D=x`*Mct7RLw=0ci~KWubVKa(MbCr-FI#_)r&I- zZk$9)Ut46V-n97L1a}jIP|uQ1(&yG~kK#4_TN|K`Wid-;XP14VOOh zyQzni%pI{ZA}T9NW4wu|bb-9YPwcNd_1Iry7jSDGYm$_tcpugnt^9kd$;gAbKPJNb zQoF|j*|#*SEw;@&(xu4>~;v^=j#m^POGicT-rgsa$VwGTZDj)E5sWUjlu8 zNuWUGX2D=-l@BFfgnfg4pYML&Q(3?5()%69NT<)li-+|5rk#!FDU*5ReYR#IOHZD@ za6LM(vzc7uA9p{LAKLRwbRlMqau#yn@b}sjANthn!JtHw!l@~L{b?jS*K|uHJJ1yp z2=gCRGhibL04mAt14rEVmOH5+61@u~1fx3r{?s~Tmzp#hyv(a~nN!0gum%w-${T8# zoHqS=iGZdk`ayl;-g8z$&>p74#Y>J+XAvO~Y~{$w$tgC?9Q|Hnr$?A>O7NZukR=dy z{u-ENioK(m{V{obc1+)ntovopo>?aGsFjhlT;YZIzREsNW%$9LbTr(Elm5KLHb;e7 zQ>Mpe9FYQf>b$XwI;rpqyWXuF>VPiXBT;>!G8&dC@gYoP&)!x3&-rYRHcPu7#j8s< z%)bg(azq;G2cW-Iv96E>lRCE>A|k4t+LDn`50cPK-xfCM^JHH(I6vO*bmX#J2`5Wo zs$FaEcQV^zofB50A${*DN74I&J0&<(a`;QhZ!-2sM}#~m^yVOV5oj9(x|o#jocVUAnrRaP zUGQwznmWJMc)0o=JKqw|Vk$2$pZx37`U`&3reuIJYv69Gm!IIJ{EzAg16@L74~o)M zAUJh^1dtD3t6-0i@VOXXO+0Gx5P}zIfTaAai4lq#Jo(#$oX~*>#0OjPQHiU{&@u#1 zwZ&T%T^@Mtb$x&R(0{VO2dwRbYbBUlJ!C7L>EyWC723tac`ROY{9B?4QCrviq$xTu zfP^FRNsZT!XV{0bbnjtOyNsA3K~5wQcXM)muwd)yyo*O}xFYGadhEl(J$1GcwzFdF z8`4WI>e-nc9PYbx=++k}!$OX)yGz^{nj6WNPY<&P7B<3&bt`TVjVQeoSmTv(P-1?@ zttPcuh&-5l#xm0yt=G1oFN*p#1DbzlO(2pHXiZxyzx5(B@-RuAAW`USQI3F1A-TQY zYxA|x(~6mTkJf8(1Jh5$CP)G^zbD3^o?w_w4c4UV*> za9>>#!@ikW5G3ziW`+kg6|`X8Pox!V4g zwd^r1v!!48wUw02qswZ5o8L%bD*i)t;N!UM0kOLiJK;$C(GToJZ*?>jKKQNcVX~HW zc!Sy*ITG*et`eAVYk%}=iV^SNF4Rly&Vu%YlvWYV9y)A5fu^8mchWgH&H6Yr;RDfot-N zh9LUs&|W?X3Y(iUk?PX@BIC=NvpV*EcjR}(Yv|jOTB7OIhaCaWd1f3!d^1c-g;rO} zQ&9!qjFKr0N6Ivsaz>~&}le{id%L5*N+0*o8%#m7p0hw&R5*=bd$6Bu#79Xu^R&eZ*>SIX)Y z9Z;T?+a<@>VD25mDBL+a@y`J{;JIba4XezH_pM7N$1xCj+x_Lp8eu!&sYAr`SfQ)~ zAcI~{c$B=T6cZ2iI+uef%hH`I%ep2Bmu_0Xa(I5jimdN?PuS?l~jOC zOzjWnWIuAaLfj8qOa_(+ZLW=Fp((*|qGJg0N!D^98{+!VakFdr*c7c}yI7bLF0{qF zum!<|L6dGJS_cEsi#mnFa&)Cxl20RiM_v_6`5oM=qA%v9l8}j6V83wrs~(Q?F}Idj zeCUbD&C#w5WWP!1gN=&yH!)X7HUD@osYX5@3|~sH)P1*gUSQ{i{f62gnL9l=cK8q# zA0XAYVUdG$w(ruS6>n4hW=xB!;CvMCRrNrEBr$8O!!AaCqIU92OS&~e0fMP{a`UgY zVa>!#$MAD^guv;tEv{Xk)2)I<3YPI!c;uo@ZK!9p?Zf{dITM|X&V5A1T*#UchFgqY02AD2YD z1v7>IwhsF8e)dS;M_GwILAdtkB`O%&{RU7(n+v$2GOg%`U(%w=t%?hjX^Z@wwlA_r zY<-*(4riTsB9xee)O2*L1PxmKuMpm`#!bPMB5qw3md<~<6z=P}wF`j34yY=4aHQ8^ zod6IDX@w1eChp0n#&1@h|7G5b=)xW?ly}(%y_`1{FhW{?%BtEyN@N$zhknd?NOurL zjkx;&`^l@bUG|^3ljWT9qK}7d!SnsciMHd0yuIzzXE`6g%=O}aW_uaRWw7IoP~V_1 z2E^`o^Nq-Bi-Gij9(YdR_%NeNB##$;>kZDGRPk$u&j@Wo@zFbXk8s!b){)}!N*F5a{Jb>%mqw<^+o z9m8{-{sif$w{9VGNBv{p2nzoEJMpRP1P6IgA^7#vt)3C)91Kpib}SQ5W3Rh0Yp(Sr zk)QXmuDQW}m%bv6N59|f3t{g9!RsTEtp{5XhU=Vi+rB>DIuFo>cfqu%xOL~YpO$fm z11F~hvgfzuR%LvV0Z)gKTuC=Q&OhE^_(FV5RXi6NHAyl(O^|m9b=z8Leq%b}Zyx^5 z?wd{AeJ>D?9$(a^T9}^cE$P0dNLebKJmb2mw3d#U4>_$Jc2+XE-|NPgk& zhUD-&hiuyRbcLqXQT>a5d*L7R@kA;|p}#S>2*XGMSHXw?XI5k-|4vTmde6M$dWJrG z2P=D-wN%sGhoSwZQf-0v9%u+8%72*Zuk=B6J+uC!!1nI?^w$(*B+bG(pN)8#JdLq~ zTfx@Lne|nh-`R!_evr8ZwC8+rKHhn{ef+E5FV1*?NX6H?+Y9utt7W z2HrPsWG(~=P>NBQsn82~c1bt-b2Jr-{xpRkRWlznh}zvSR8X)O=+<|3`>l&f6??OK zXG4An)&c70zxvxCkq4=cNc#WD4t`K#>@e(i(h=q`O;u^)lr)Yv&nR{NdfkQQ_E?oT z_B)iP?W4RzkEu<}9ms0~U0{VtW>_=?cSl~n^zosC3bOB0z8pM-b@dx;-V*QlH};N&`upX(H8zwRDedGZ z1+sHlC8H#AHUtRiib`d;4w%`-z5aJgd$>ktdz!&_5Hjn=V9T`B)P# z0D()!%iiayyc|J9Qt%og>Z{BXd)~|Ec|VJDcKV@&NxmJ_u+wsSax?wMYYV3*$|6OV zDqztZ+dX$h0#3slZrcS8i=!oiJtC$#TAYaHAJ3V$(TIzPZ>yp{>I%|$LBRr*oR4~% zf=W+&U%#kf61czmrzoqvLQ;KDHJ!&}bzFO>mb?&VYYlW)ZY;=fRUncyvaMCx?bp}(b#~74rb?}u3*NH- z5^v~Wj8LLs@GlZ4K#d%L3OB>cJ$d3pxWVfK($+5^Cw!pSWL1H}lN+W(w`e|*k-K~y zy<^q=l6UsPRq3Di*yry}E-I^MMVNMIZEUtX9DG??HJN7Xncou zX=dPSy+oT()niyVP`%|)kjk^ruhMjFm=`zyuPPrWsAB2n@cnCSf1bLGij0DUdje=q zZAVitJc*N0H%xrN*N0}cB&09NcAtCRf{Re|-m>J~Bx2vNkH8;%!l;>d0g<`!byk|# zSCOWbxGxR5=$|h{Ii42Z-}uVpfD#WSoHF|=X&lCj_i<7f-1Dn^**1!@4(ql`Ao>w9 zYX85YUkT8_Xf>(4)~@6sH~!R2wvQ67F+YM84!#FgS7OYwVjo#zTsNmE5^J^4otZ(i z`i{NHKtJIn_SJ`Pt+(i^E7is#(sIm|&mkO7b{|H*yisOOX?^u?Ap^$zmEvCwLRSoo zjzo`&cy8t0`5mWvC+FAZy)4_NRpq{~Yqwr1X*~Szzv~(pl-&J%q~>TfPQb^{fq|wC zpqURJo^9fc;*9tZprA$Lb;{@zT7L5*8Zjhj5l#9b0p5CD<4LJ zVIT=C3|zXU&ynZ^S&fNu*`TvHm>^p3uG~9ce)Sua;eVkACAhJT6&zaypGwaIm)3c< zn}N%;>4VXGcU2gp=Uu$_TN z0;O_wPI2xtkd-esY3b7?yO4y;$8?bGYSE-WN;!SOuGLF(1IxuF=rubx!Za@s)y||F zWCkTM^1u3E%Ngx&zKff7W|Ad48Z+G{C7H4Io{_AV@S=_buDd)y~3`|rqZwJxx zYiNYKFLH)C&(W+9SDSICbxAS^HiY%PZ9Yv!wqn}{K7|m-Ad3u}L!~LHAx+6WKMJr# zpTmCIDHI%W%2XUHs+F2BEQtLn(^R=Z$;fIEP3zS4z%O4Li$68(0F+3HnaIv6s8yiG3-icCsDun!T3}lh7nl@urhA{?$pv7zp@jSLe5w~p zn7NVi8h}V&WsAGa&NnE269noDH^3azz{JoA3)Wn>CBbk@Sh$B1;npps0;@s2^WP{~ zWBKzCYF}Qke0<;T2HS5J;F1XJ@qU=Ce5zA_DUrV_?YmbAB+-90{t#F|LMEYh`1EGB zF9N*tiG-X$xoLBG`6uRbosv{n=yPC+nyNT#@ZDM(Qoq7)Qu?Qj`Ez0kA>dFK98@0p zVh>L4P2Q8RgcIDYFXwTrHaD`frHK1tqa|m?Zbke0muIqqW2Kwwc9r>^jbCP#HD)_S zmT4$%|6?7-oq3J``#*(O9`7dR3b^cF)MvWJb3TfSa9l^%kOSk9`^iPa~UV5D7Sj6}F{fh?n zyN|QWF@$+weZXl`a3EzrfzEhP85d3(4P)F9l9`T6cpBu$dNAZUa0IsKBe^St-EK{L zQ^}5B;|V5zf|Vb*nl)&8pJ~T1R;Opk5pV?OsigG^`1%F_tabYk=6M|i zU@77iVb-(5y~8_r9YMN5b+!;bVk`=LOsZ#zi2lNMyA8rBK9G18-GoWw_vjB|PUb!D9x{kM!XLS<6h}=l485;y@IUCcaGw24 zQV);$C%@n8E$S4qwqB&w@Ou@zJg8cn%*Qd_lff&P%BQU<&u`L{EX0zMq7Ij-v~qms zPQ42%S|mg8aUJpfVon#mvefm>fYkHx%$R9goqUHS-3`m-U$9{OL}XI3jxuhVG;Qx02;+K*~1pur5HwIAcz44NfD>)dAZ zf${YQpxlb=*5#T&FRt=G_TK(>1qJQ{f~v*SaB5U<#)cjOg20^4A2d!IN; z6pM#v%`NdQ2u@)Ehx->M;slayp)i4D=?e(oNnPgVw#*MYG{;bmCn`}q1q4^+EEp~> z+!SJqynXuZ?%};2x}7luh*8xsF&t6{OB< zNMam|WaSq2Z4^c)$R!bRu%FH-+*sp@c&^KNkLP~R`*n|(;3=Pl#HmzS!V+;%@7dd;T$Aa(Iop~o<6Tc4&>bxn zZ@6Q4qxAdI$9H==>DT?#FQv_2=(Os9&Fo9K|NgscYmQ!R0%al?Z1B>wnFDem4N6%( z+akI$*?UQ1K-uJVv9)B+y`u>&))kbMmK%AFC)rm9SM5eAsSi!>nYk#$_Cfa1$$vxg zLZ3bJ*@k)Zs#H}r-^KW;&tx*&v9#kI^o?}fp(m;Zxmoz)&>`Wyw~CB)*(}=zk}*7x z7nw~9CFTkt4atDQ-d&Z^jpZYbYapSRGQERTVOu9x; z>uO+cH1p6i)_#=++v~lEI0&+9fiuj!pD#CKsNqPqHsIqBi2*Tl4SN4YV))~8VR9e@ zvWcTwUN2yGxPN9M%PvCl+8zmfoDGCu0ZGlBd#k$mp_d6_*8xq_>28lar{?o1_49|6N?p|4PE z=`<1JSLSg#%Z^kR<+iaOK)Xi!FCf2+qj^D{hUvY|QwR&J0z|5RfbmW!E zjFXfd5w|25dJeP_!df2-GchyC09M?A|9k_U)eu@1UCR`1FclN~`@UAz>`o`D^s+ z9|BSjI&+hzXcmn>E%@loPxZ(F1L~Pv{a~&7RN0;qdV5G!wV@9_E_6*vu%z#eYHMf& z5iRrf&|DgxBxDwv@c8fl>e(xI|F7C5Y_(zk)yj zDdX8X8kp=Bwu%MpIbmb{`#oLz+Bh4q{+Y%zj@SYux5^Bmep8NyGbj(KWSSYGw*BQH4j&U$tLH zBCe#HY=h-Htd7PrHnl$3Kxw^i@P!mt9jHk~@GuiuFZ~qf0P#LGY-#5}Ino;qcs{T} zhvihS_BLS?4fxPb#6<{H&bmZUc-=9c?i*WMJ}mJ!>(bT|>Tcp#>B+N~Z@rmh6H2=0 z-Zn7zePjKM41rr(>d5s~3)AuC%U2!a3*L>%c*sc^h%U~!kg*|Rh87;q=Vok&xYj!d z{E_RoY}xmF9?}u61lwWl9+j)b_xrh|xQlJb<95szGar{qgc}ZBje`T-5wnc?*|m7 zB2b4k_!feRZcDK-2_3m(D#ZX_W4G#&|K8a-NPVoF|y92hovSMT3G2#uS1LVS?yk7^fCC{ql&i@4 z0E1CRqIOGiL3Yp0%c1xWLfWG}gKbWJgNKLhrrDE{Ek3Q+@2h9rKQ*A9WM#43e>n-W zT6l`Vyn?Z4fm(vmhdkAsF@|(^of$sA>NbDGQ(bM#_Vf%<7?@wh;XY-)H(w~sWXo>D z1S}rc5P7P=Kg6sEmFF=#j7zHb8g68IaZ9!0m5`RSQ03Noz3Yu-O2&q4_V@A8A$*$h ztQ*6msmSAvJeSU(>_a2i@aSv&mlE3VBcU=zO-Lg08X@I{TFKv`GW!j8q8Y`!yG!is zVp#6f*fSGRhsdwfZED!Ri6XXa@xs}P)15kbRQt?_PZ2hw?|XR1nc{GBYqfpLr$XMn zlg#Fm?RY(WT(8cLPM%2mv8*K#mU2eNAn?aQ6m zqRF?PUrC4Gy5ty}=AooB9Y?#RI*hEf?I#lRUKTt!(f5dGdgp6GNkEE}zT$b)HvM$L zph8d5A%g;SI=w_ubArP2Q!Z_Um{@+Ng~mwJ>a6vXY$G!c-{mf1x_8#3+Oi30iGyKi zpMtKc+W-(ChVB?wT{emB65xmpSe?I1VAgsi)D^5rcERq}>~ig(q^4ns`%B6(T3(!J zr-RP9VVZ10MU1CUX=-Zj3ue^K8F}wrEs%uKdf_Cjbpvc-E2Vn zNob2o$d9ks6E9LwxaB1obi~1J+tGu)mvs(*Z0)J`sq0isb=_%{Dlp>`F3D$EA>8tFUW9yjCEyHyDd%7f zhW0LK&vzquu{P8KclrJCj8~b44$#X_dP(OVg0mlgi!p2)k(71>@-Uh`gO!Vf5@9PW z_0jX8ofH#K-2`kzndVW{^O$Wa^5B8)Jy}%IwG?O7Yv;NdO$2lsw^ub;FBA>=;JJ^< z!;s}JYsT6aJqlCNiq~}0BxztQl}g_k(L_%Jza5(<0rdT5Sw9LscWW=x>6F>I|Wi`oaSKt{X2IubS*J(zFN>8J_g&!9xw2iNLb(1 zWynO+O8rLyd6zKah)h2sr#8D}FqBT5Q=(W%aiD)xo5{$_0692G-*!`R;l^dAxy`Y={kl#>5KBuurmqE~UM})dzbAa$X^5+PBMwax1M?C%`LMMX70NVtT#nN5?$w7 zMhwkHj|0e#7wn}|6M-WnyXI)vICW|`)I(Ap3ub7U))W;JwumT=_W<`?tlet7d8xADe zMgzXQM2|pg@LUNC6usDIobK$rs%yF!5L7hm7ZsxDOb_j8(rqZyHKLiv;puKUXm5N(SR6)8iv}et77pz3eYf=vfs7$bn%zn!epPRk3m^41bA?L@rNA2j^+^6WpN05syCXOGGPn{$v zhJ8#(_Ys^AaS9Uj-2`g$rh}*gebK%b5AZStZo#oG1y!)OXNNn85bo2F?}6F;9=>cZ zh^Z3{Q?lz^f}6RC4hRN1y!FA6MI$Z}czBtxn9|#|&!R-@xp|qc?4U8+HFWG4j>WPK zLSC&c(ndha2i0H(H5?EU`>zvJVY>MUz_a&So>MXoVa2&^Yn{HtUYe{u({+J5Kkl?B zfY)=pjv7R`)@W|Qjm0Z?*lF_vs5G?1NGQu97099CPd=J*Pv*Uh`y+*|ekcGjZIQnd|X^ z zq+ROShGb~`=ND(m+S9Q-WeO0}%Dw+k6%bD$`OK$(PB)OnS0maqz7Fg5UR9TWiQYSc zQsQwaW`@sgNm@aM7|Tzs1HmC95v0J!wpTtRPX+BP3pgN3LW!_efPrue(-SgWA$W(% z&LVRco(NGIKF>Dc=^Q3EyuT+^%esQ>? zcUD=GE-eu|@br%IKB#6Z5;sWAcBlNV217;DS~;?s{wo;W&c~`^)t~n5LkLdNiqG=j zVQ_ULHWvg02J;2O+F1;H<2to;V;L&*rzdikPnyCINe{00yVUGCQ@SU)=rOHK4-)rq+;t}GJYE|LO@<@${rZ=*q zbsVuX{`UmlV5dg9WLOH?ae}omqvwdIgv8_g zo-;R;GTpaLH{!i3Q`y_E>_GbQ9^}^O4_1+1RbZ&+VPAYrzdv3ouchFC;yN8l-7CPi zG*{fpri?+p>uOF=NiIhA>#+M6lo>#Vw(4z2&d;^I`Sr`!w{*7gePI54m4yr0h`T;R zV)+-qp8eSP3XrbCK|p|m;O4J`02ze3n@k4_DH{{6Z?u6hUQpH=Qucm`@~nj>+yK!W zReUs+@<$v$=VBC^;VzEl_F|l6M+>?G&gVBQJpXTt`8gsvs4S_~3wiP?r0gzdaU=e2 zfjo@tOXGvhkdu(Q0HabJ5UH&3XY>Sv80lK2qQ%2)f4Bo#=w4PtIIy50#tlfk|4bV= z!;I<-$|5|(=f2?dMrZe`AC7}hIR=Y&W#XlW?H`$*6rg=uCT^ME8ZYJPoS9N>5y$gP zwg@sKiYY3Hf$*vQNH2W-l@I;Z3z~I0c}tRNS)#PC4rH&^UVUd>6;H;-%8l?NHJ^mw zF;*quxBNAk;sQ%ppZHG|P$yw4S5UaXt@F4MuBBr_ir=C3DY;=zq*xYQ7|7BF)jx?p ze$D?Zx^qmz1|s>{*91xJ_0qOi7Mzj@Ix?euqB%eBJ!qKNLGNImyPN%K4lLZEfuh$A z4yra^zw0V2kL7u)&39UBY;BQx%wfz7YP#xQvf9G58__{$ z-^Iy62fnhw;M$g<`2H3r$TiRmil)~;zljKndI`Bc{1oEv#iBv0BbtRFlf~*DAUjo{ z5^Ks=oaF_cUCmm5EvcVBuVkv81+)_`?$1Gu`Mvl2{ZgldOr1}akPYFl7XG|bQE+^Q zUz~OLdzSop|7r*wtbJOX&iQ{n^!JDz@XEzoA literal 126476 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 From 0b863de74fdea804d25f514d939ee0b91e37b577 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Mon, 19 Feb 2024 19:35:52 +0000 Subject: [PATCH 002/120] Add example config yaml --- config-template/example-config.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 config-template/example-config.yaml diff --git a/config-template/example-config.yaml b/config-template/example-config.yaml new file mode 100644 index 000000000..20000f3cd --- /dev/null +++ b/config-template/example-config.yaml @@ -0,0 +1,10 @@ +project: + name: "myproject" + modalities: ["DX", "CR"] + +tag_operations: + base_profile: "orthanc/orthanc-anon/plugin/tag-operations.yaml" # WARNING: might change! + +destination: + dicom: "ftps" + parquet: "ftps" From 028be0991b45d54089ff40ca19ec104004c17beb Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Mon, 19 Feb 2024 19:41:37 +0000 Subject: [PATCH 003/120] Add pydantic model for project configuration --- pixl_core/src/core/config.py | 64 ++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 pixl_core/src/core/config.py diff --git a/pixl_core/src/core/config.py b/pixl_core/src/core/config.py new file mode 100644 index 000000000..65c672cc2 --- /dev/null +++ b/pixl_core/src/core/config.py @@ -0,0 +1,64 @@ +"""Project-specific configuration for Pixl.""" +from __future__ import annotations + +from enum import Enum +from pathlib import Path +from typing import Optional + +from pydantic import BaseModel, validator + + +class _Project(BaseModel): + name: str + modalities: list[str] + + +class _TagOperations(BaseModel): + base_profile: Path + extension_profile: Optional[Path] + + @classmethod + @validator("base_profile") + def valid_base_path(cls, v: Path) -> Path: + if not v.exists(): + msg = "Base profile should be an existing path" + raise ValueError(msg) + return v + + @classmethod + @validator("extension_profile") + def valid_extenstion_path(cls, v: Path) -> Path: + if isinstance(v, Path) & (not v.exists()): + msg = "Extension profile should be an existing path" + raise ValueError(msg) + return v + + +class _DestinationEnum(str, Enum): + """Defines the valid upload destinations.""" + + none = "none" + ftps = "ftps" + azure = "azure" + dicomweb = "dicomweb" + + +class _Destination(BaseModel): + dicom: _DestinationEnum + parquet: _DestinationEnum + + @classmethod + @validator("parquet") + def valid_parquet_destination(cls, v: str) -> str: + if v == "dicomweb": + msg = "Parquet destination cannot be dicomweb" + raise ValueError(msg) + return v + + +class PixlConfig(BaseModel): + """Project-specific configuration for Pixl.""" + + project: _Project + tag_operations: _TagOperations + destination: _Destination From 1cbedbc290b4c09f2a2352f4db229f259588f958 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Mon, 19 Feb 2024 19:41:42 +0000 Subject: [PATCH 004/120] Add tests for pydantic config model --- pixl_core/tests/test_config.py | 63 ++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 pixl_core/tests/test_config.py diff --git a/pixl_core/tests/test_config.py b/pixl_core/tests/test_config.py new file mode 100644 index 000000000..08318859d --- /dev/null +++ b/pixl_core/tests/test_config.py @@ -0,0 +1,63 @@ +from pathlib import Path + +import pytest +import yaml +from core.config import PixlConfig +from pydantic import ValidationError + +EXAMPLE_CONFIG = Path(__file__).parents[2] / "config-template" / "example-config.yaml" + + +def test_config(): + """Test whether config file is correctly parsed and validated.""" + config_data = yaml.safe_load(EXAMPLE_CONFIG.read_text()) + config = PixlConfig.parse_obj(config_data) + + assert config.project.name == "myproject" + assert config.project.modalities == ["DX", "CR"] + assert config.tag_operations.base_profile == Path( + "orthanc/orthanc-anon/plugin/tag-operations.yaml" + ) + assert config.destination.dicom == "ftps" + assert config.destination.parquet == "ftps" + + +def test_config_fails(): + """ + Test that the config validation fails for non-valid values: + - 'dicomweb' not allowed for parquet destionation + - Invalid destinations + - Non-existing base_profile path + - Non-exisiting extension_profile path + """ + with pytest.raises(ValidationError): + PixlConfig( + project={"name": "myproject", "modalities": ["DX", "CR"]}, + tag_operations={ + "base_profile": "orthanc/orthanc-anon/plugin/tag-operations.yaml", + }, + destination={"dicom": "ftps", "parquet": "dicomweb"}, + ) + with pytest.raises(ValidationError): + PixlConfig( + project={"name": "myproject", "modalities": ["DX", "CR"]}, + tag_operations={ + "base_profile": "orthanc/orthanc-anon/plugin/tag-operations.yaml", + }, + destination={"dicom": "nope", "parquet": "nope"}, + ) + with pytest.raises(ValidationError): + PixlConfig( + project={"name": "myproject", "modalities": ["DX", "CR"]}, + tag_operations={"base_profile": "/i/dont/exist.yaml"}, + destination={"dicom": "ftps", "parquet": "ftps"}, + ) + with pytest.raises(ValidationError): + PixlConfig( + project={"name": "myproject", "modalities": ["DX", "CR"]}, + tag_operations={ + "base_profile": "orthanc/orthanc-anon/plugin/tag-operations.yaml", + "extension_profile": "/i/dont/exist.yaml", + }, + destination={"dicom": "ftps", "parquet": "ftps"}, + ) From 5a74734d0a467def8a4cb765fb088384f6e9bd05 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Tue, 20 Feb 2024 13:54:51 +0000 Subject: [PATCH 005/120] Add config loading helper --- pixl_core/src/core/config.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/pixl_core/src/core/config.py b/pixl_core/src/core/config.py index 65c672cc2..56f06cee1 100644 --- a/pixl_core/src/core/config.py +++ b/pixl_core/src/core/config.py @@ -3,10 +3,14 @@ from enum import Enum from pathlib import Path -from typing import Optional +from typing import TYPE_CHECKING, Optional +import yaml from pydantic import BaseModel, validator +if TYPE_CHECKING: + from typing_extensions import Any + class _Project(BaseModel): name: str @@ -62,3 +66,12 @@ class PixlConfig(BaseModel): project: _Project tag_operations: _TagOperations destination: _Destination + + +def load_config(filename: Path) -> Any: + """ + Load configuration from a yaml file. + :param filename: Path to the yaml file + """ + yaml_data = yaml.safe_load(filename.read_text()) + return PixlConfig.parse_obj(yaml_data) From 4fa71ef2b70c72a9bf29287eb89cfa7d24e241db Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Tue, 20 Feb 2024 15:10:28 +0000 Subject: [PATCH 006/120] fix: Remove `classmethod` decorators and tell ruff that pydantic's validator is also a classmethod Setting the validators as `classmethod` will fail for some validations --- pixl_core/pyproject.toml | 18 +++++++----------- pixl_core/src/core/config.py | 3 --- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/pixl_core/pyproject.toml b/pixl_core/pyproject.toml index 26071427b..25fc9b9d8 100644 --- a/pixl_core/pyproject.toml +++ b/pixl_core/pyproject.toml @@ -1,15 +1,11 @@ [project] name = "core" version = "0.0.1" -authors = [ - { name="PIXL core functionality" }, -] +authors = [{ name = "PIXL core functionality" }] description = "" readme = "README.md" requires-python = ">=3.9" -classifiers = [ - "Programming Language :: Python :: 3" -] +classifiers = ["Programming Language :: Python :: 3"] dependencies = [ "fastapi==0.109.1", "token-bucket==0.3.0", @@ -33,11 +29,7 @@ test = [ "httpx==0.24.*", "pytest-pixl", ] -dev = [ - "mypy", - "pre-commit", - "ruff", -] +dev = ["mypy", "pre-commit", "ruff"] [build-system] requires = ["setuptools>=61.0"] @@ -51,3 +43,7 @@ extend = "../ruff.toml" [tool.ruff.extend-per-file-ignores] "./tests/**" = ["D100"] + +[tool.ruff.pep8-naming] +# Allow Pydantic's `@validator` decorator to trigger class method treatment. +classmethod-decorators = ["classmethod", "pydantic.validator"] diff --git a/pixl_core/src/core/config.py b/pixl_core/src/core/config.py index 56f06cee1..1318801a2 100644 --- a/pixl_core/src/core/config.py +++ b/pixl_core/src/core/config.py @@ -21,7 +21,6 @@ class _TagOperations(BaseModel): base_profile: Path extension_profile: Optional[Path] - @classmethod @validator("base_profile") def valid_base_path(cls, v: Path) -> Path: if not v.exists(): @@ -29,7 +28,6 @@ def valid_base_path(cls, v: Path) -> Path: raise ValueError(msg) return v - @classmethod @validator("extension_profile") def valid_extenstion_path(cls, v: Path) -> Path: if isinstance(v, Path) & (not v.exists()): @@ -51,7 +49,6 @@ class _Destination(BaseModel): dicom: _DestinationEnum parquet: _DestinationEnum - @classmethod @validator("parquet") def valid_parquet_destination(cls, v: str) -> str: if v == "dicomweb": From 07188ff626525bbdbdba8790d94578886b7d0e10 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Tue, 20 Feb 2024 15:13:43 +0000 Subject: [PATCH 007/120] Refactor config tests: split up invalid fields tests and create common base data object --- pixl_core/tests/test_config.py | 79 ++++++++++++++++++++-------------- 1 file changed, 46 insertions(+), 33 deletions(-) diff --git a/pixl_core/tests/test_config.py b/pixl_core/tests/test_config.py index 08318859d..cd8580c1f 100644 --- a/pixl_core/tests/test_config.py +++ b/pixl_core/tests/test_config.py @@ -22,42 +22,55 @@ def test_config(): assert config.destination.parquet == "ftps" -def test_config_fails(): +BASE_YAML_DATA = { + "project": {"name": "myproject", "modalities": ["DX", "CR"]}, + "tag_operations": { + "base_profile": "orthanc/orthanc-anon/plugin/tag-operations.yaml", + "extension_profile": None, + }, + "destination": {"dicom": "ftps", "parquet": "ftps"}, +} + + +def test_valid_extension_profile(): + """Test that the config validation passes for valid extension profile.""" + config_data = BASE_YAML_DATA + config_data["tag_operations"][ + "extension_profile" + ] = "orthanc/orthanc-anon/plugin/tag-operations.yaml" + + config = PixlConfig.parse_obj(config_data) + assert config.tag_operations.extension_profile.exists() + + +def test_parquet_dicom_fails(): """ - Test that the config validation fails for non-valid values: - - 'dicomweb' not allowed for parquet destionation - - Invalid destinations - - Non-existing base_profile path - - Non-exisiting extension_profile path + Test that the config validation fails for non-valid values: 'dicomweb' not allowed for + parquet destionation """ + config_data = BASE_YAML_DATA + config_data["destination"]["parquet"] = "dicomweb" with pytest.raises(ValidationError): - PixlConfig( - project={"name": "myproject", "modalities": ["DX", "CR"]}, - tag_operations={ - "base_profile": "orthanc/orthanc-anon/plugin/tag-operations.yaml", - }, - destination={"dicom": "ftps", "parquet": "dicomweb"}, - ) + PixlConfig.parse_obj(config_data) + + +def test_invalid_destinations(): + """Test that the config validation fails for invalid destinations.""" + config_data = BASE_YAML_DATA + config_data["destination"]["dicom"] = "nope" + config_data["destination"]["parquet"] = "nope" with pytest.raises(ValidationError): - PixlConfig( - project={"name": "myproject", "modalities": ["DX", "CR"]}, - tag_operations={ - "base_profile": "orthanc/orthanc-anon/plugin/tag-operations.yaml", - }, - destination={"dicom": "nope", "parquet": "nope"}, - ) + PixlConfig.parse_obj(config_data) + + +def test_invalid_paths(): + """Test that the config validation fails for invalid tag-operation paths.""" + config_data_wrong_base = BASE_YAML_DATA + config_data_wrong_base["tag_operations"]["base_profile"] = "/i/dont/exist.yaml" with pytest.raises(ValidationError): - PixlConfig( - project={"name": "myproject", "modalities": ["DX", "CR"]}, - tag_operations={"base_profile": "/i/dont/exist.yaml"}, - destination={"dicom": "ftps", "parquet": "ftps"}, - ) + PixlConfig.parse_obj(config_data_wrong_base) + + config_data_wrong_extension = BASE_YAML_DATA + config_data_wrong_extension["tag_operations"]["extension_profile"] = "/i/dont/exist.yaml" with pytest.raises(ValidationError): - PixlConfig( - project={"name": "myproject", "modalities": ["DX", "CR"]}, - tag_operations={ - "base_profile": "orthanc/orthanc-anon/plugin/tag-operations.yaml", - "extension_profile": "/i/dont/exist.yaml", - }, - destination={"dicom": "ftps", "parquet": "ftps"}, - ) + PixlConfig.parse_obj(config_data_wrong_extension) From 1741888f5e633db762f1768d0a905a92cdcd8121 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Tue, 20 Feb 2024 15:14:15 +0000 Subject: [PATCH 008/120] Add some more asserts to config test and use `load_config()` helper --- pixl_core/tests/test_config.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pixl_core/tests/test_config.py b/pixl_core/tests/test_config.py index cd8580c1f..afe3f22c5 100644 --- a/pixl_core/tests/test_config.py +++ b/pixl_core/tests/test_config.py @@ -1,23 +1,23 @@ from pathlib import Path import pytest -import yaml -from core.config import PixlConfig +from core.config import PixlConfig, load_config from pydantic import ValidationError EXAMPLE_CONFIG = Path(__file__).parents[2] / "config-template" / "example-config.yaml" -def test_config(): +def test_config_from_file(): """Test whether config file is correctly parsed and validated.""" - config_data = yaml.safe_load(EXAMPLE_CONFIG.read_text()) - config = PixlConfig.parse_obj(config_data) + config = load_config(EXAMPLE_CONFIG) assert config.project.name == "myproject" assert config.project.modalities == ["DX", "CR"] assert config.tag_operations.base_profile == Path( "orthanc/orthanc-anon/plugin/tag-operations.yaml" ) + assert config.tag_operations.base_profile.exists() + assert config.tag_operations.extension_profile is None assert config.destination.dicom == "ftps" assert config.destination.parquet == "ftps" From 96980e707057fe2746a46296a7521956305b1331 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Tue, 20 Feb 2024 17:07:36 +0000 Subject: [PATCH 009/120] Add PyYAML as core dependency `config.py` needs it --- cli/pyproject.toml | 9 ++------- pixl_core/pyproject.toml | 1 + pixl_dcmd/pyproject.toml | 12 ++++-------- pixl_ehr/pyproject.toml | 9 ++------- 4 files changed, 9 insertions(+), 22 deletions(-) diff --git a/cli/pyproject.toml b/cli/pyproject.toml index 3c7f01d03..c350e972b 100644 --- a/cli/pyproject.toml +++ b/cli/pyproject.toml @@ -1,22 +1,17 @@ [project] name = "pixl_cli" version = "0.0.4" -authors = [ - { name="PIXL authors" }, -] +authors = [{ name = "PIXL authors" }] description = "PIXL command line interface" readme = "README.md" requires-python = "<3.12" -classifiers = [ - "Programming Language :: Python :: 3" -] +classifiers = ["Programming Language :: Python :: 3"] dependencies = [ "core", "click==8.1.3", "coloredlogs==15.0.1", "pandas==1.5.1", "pyarrow==14.0.1", - "PyYAML==6.0.1" ] [project.optional-dependencies] diff --git a/pixl_core/pyproject.toml b/pixl_core/pyproject.toml index 25fc9b9d8..49d39ff7e 100644 --- a/pixl_core/pyproject.toml +++ b/pixl_core/pyproject.toml @@ -20,6 +20,7 @@ dependencies = [ "psycopg2-binary==2.9.9", "pandas==1.5.1", "pyarrow==14.0.1", + "PyYAML==6.0.1", ] [project.optional-dependencies] diff --git a/pixl_dcmd/pyproject.toml b/pixl_dcmd/pyproject.toml index 6a4cfa96e..57839a2f0 100644 --- a/pixl_dcmd/pyproject.toml +++ b/pixl_dcmd/pyproject.toml @@ -1,21 +1,16 @@ [project] name = "pixl_dcmd" version = "0.0.2" -authors = [ - { name="PIXL authors" }, -] +authors = [{ name = "PIXL authors" }] description = "DICOM header anonymisation functions" readme = "README.md" requires-python = "~=3.9" -classifiers = [ - "Programming Language :: Python :: 3" -] +classifiers = ["Programming Language :: Python :: 3"] dependencies = [ "arrow==1.2.3", "pydicom==2.4.4", "pydicom-data", "logger==1.4", - "pyyaml==6.0.1", "requests==2.31.0", "python-decouple==3.6", "types-requests~=2.28", @@ -41,4 +36,5 @@ build-backend = "setuptools.build_meta" extend = "./ruff.toml" [tool.ruff.extend-per-file-ignores] -"./tests/**" = ["D100"] \ No newline at end of file +"./tests/**" = ["D100"] + diff --git a/pixl_ehr/pyproject.toml b/pixl_ehr/pyproject.toml index 1193c20ba..9b0c44f13 100644 --- a/pixl_ehr/pyproject.toml +++ b/pixl_ehr/pyproject.toml @@ -1,15 +1,11 @@ [project] name = "pixl_ehr" version = "0.0.2" -authors = [ - { name="PIXL authors" }, -] +authors = [{ name = "PIXL authors" }] description = "PIXL electronic health record extractor" readme = "README.md" requires-python = ">=3.10" -classifiers = [ - "Programming Language :: Python :: 3" -] +classifiers = ["Programming Language :: Python :: 3"] dependencies = [ "core", "uvicorn==0.23.2", @@ -18,7 +14,6 @@ dependencies = [ "azure-identity==1.12.0", "azure-storage-blob==12.14.1", "pyarrow==14.0.1", - "PyYAML==6.0.1", ] [project.optional-dependencies] From d65667917279f03521b0d184b91dae19d2d68552 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Tue, 20 Feb 2024 17:11:34 +0000 Subject: [PATCH 010/120] Add copyright headers --- pixl_core/src/core/config.py | 14 ++++++++++++++ pixl_core/tests/test_config.py | 14 ++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/pixl_core/src/core/config.py b/pixl_core/src/core/config.py index 1318801a2..75770e04e 100644 --- a/pixl_core/src/core/config.py +++ b/pixl_core/src/core/config.py @@ -1,3 +1,17 @@ +# Copyright (c) University College London Hospitals NHS Foundation Trust +# +# 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 +# +# http://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. + """Project-specific configuration for Pixl.""" from __future__ import annotations diff --git a/pixl_core/tests/test_config.py b/pixl_core/tests/test_config.py index afe3f22c5..16e7bfb33 100644 --- a/pixl_core/tests/test_config.py +++ b/pixl_core/tests/test_config.py @@ -1,3 +1,17 @@ +# Copyright (c) University College London Hospitals NHS Foundation Trust +# +# 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 +# +# http://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 pathlib import Path import pytest From 2703bba3f8f059c8ffc61d41d138a9387d2aa74c Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Tue, 20 Feb 2024 17:20:26 +0000 Subject: [PATCH 011/120] Move `load_config` to top of file --- pixl_core/src/core/config.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pixl_core/src/core/config.py b/pixl_core/src/core/config.py index 75770e04e..987713bfd 100644 --- a/pixl_core/src/core/config.py +++ b/pixl_core/src/core/config.py @@ -26,6 +26,15 @@ from typing_extensions import Any +def load_config(filename: Path) -> Any: + """ + Load configuration from a yaml file. + :param filename: Path to the yaml file + """ + yaml_data = yaml.safe_load(filename.read_text()) + return PixlConfig.parse_obj(yaml_data) + + class _Project(BaseModel): name: str modalities: list[str] @@ -77,12 +86,3 @@ class PixlConfig(BaseModel): project: _Project tag_operations: _TagOperations destination: _Destination - - -def load_config(filename: Path) -> Any: - """ - Load configuration from a yaml file. - :param filename: Path to the yaml file - """ - yaml_data = yaml.safe_load(filename.read_text()) - return PixlConfig.parse_obj(yaml_data) From 158b663fb4fd3bb8333d9ad941eaafb3923e7e0d Mon Sep 17 00:00:00 2001 From: Peter Tsrunchev Date: Thu, 22 Feb 2024 14:25:01 +0100 Subject: [PATCH 012/120] feat: orthanc-anon project config (#312) ## Project configs ## Common - add env variable for project configs location - mount project configs location - make tag operations a list in config (limited to length 1 for now) - make config functions return `PixlConfig | Any` to get type hints - replace deprecated pydantic features - add utility function to load project config by slug only ### CLI - install cli as editable to find env files correctly - add function to check env for all env.sample keys ### orthanc-anon project config - orthanc anon now cofigurable (tag ops, modalities, destionation) - add query for project slug by non-hashed values - move anonymisation logic to dcmd package ### EHR project config - mark only processing tests with run_containers fixture - use project config to determine destination --------- Co-authored-by: Milan Malfait --- .env.sample | 3 + .github/workflows/main.yml | 2 +- cli/src/pixl_cli/main.py | 30 + cli/tests/test_check_env.py | 43 ++ config-template/example-config.yaml | 10 - config-template/project-slug.yaml | 23 + docker-compose.yml | 4 + docker/orthanc-anon/Dockerfile | 1 - orthanc/orthanc-anon/plugin/pixl.py | 68 +- pixl_core/pyproject.toml | 2 +- pixl_core/src/core/db/queries.py | 21 +- .../src/core/{config.py => project_config.py} | 65 +- pixl_core/src/core/upload.py | 4 +- pixl_core/tests/conftest.py | 1 + ...{test_config.py => test_project_config.py} | 54 +- pixl_core/tests/test_upload.py | 4 +- pixl_dcmd/src/pixl_dcmd/main.py | 66 +- pixl_dcmd/tests/conftest.py | 3 + pixl_dcmd/tests/test_main.py | 11 +- pixl_ehr/src/pixl_ehr/main.py | 20 +- pixl_ehr/tests/conftest.py | 3 +- pixl_ehr/tests/test_processing.py | 3 + ...-extract-uclh-omop-cdm-tag-operations.yaml | 0 ...-ngt-only-full-dataset-tag-operations.yaml | 593 ++++++++++++++++++ .../test-extract-uclh-omop-cdm.yaml | 23 + ...ic-tube-project-ngt-only-full-dataset.yaml | 24 + test/.env | 3 + test/run-system-test.sh | 2 +- 28 files changed, 951 insertions(+), 135 deletions(-) create mode 100644 cli/tests/test_check_env.py delete mode 100644 config-template/example-config.yaml create mode 100644 config-template/project-slug.yaml rename pixl_core/src/core/{config.py => project_config.py} (52%) rename pixl_core/tests/{test_config.py => test_project_config.py} (52%) rename orthanc/orthanc-anon/plugin/tag-operations.yaml => project_configs/tag-operations/test-extract-uclh-omop-cdm-tag-operations.yaml (100%) create mode 100644 project_configs/tag-operations/uclh-nasogastric-tube-project-ngt-only-full-dataset-tag-operations.yaml create mode 100644 project_configs/test-extract-uclh-omop-cdm.yaml create mode 100644 project_configs/uclh-nasogastric-tube-project-ngt-only-full-dataset.yaml diff --git a/.env.sample b/.env.sample index d24aa3de8..a143733ac 100644 --- a/.env.sample +++ b/.env.sample @@ -89,3 +89,6 @@ FTP_HOST= FTP_USER_NAME= FTP_USER_PASSWORD= FTP_PORT= + +# Project configs directory +PROJECT_CONFIGS_DIR= \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 30ae15d95..61a6697ed 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -134,7 +134,7 @@ jobs: - name: Install Python dependencies run: | - pip install pixl_core/ cli/[test] + pip install -e pixl_core/ -e cli/[test] - name: Run tests working-directory: cli/tests diff --git a/cli/src/pixl_cli/main.py b/cli/src/pixl_cli/main.py index f206368fd..a1f50034d 100644 --- a/cli/src/pixl_cli/main.py +++ b/cli/src/pixl_cli/main.py @@ -24,6 +24,7 @@ import requests from core.patient_queue.producer import PixlProducer from core.patient_queue.subscriber import PixlBlockingConsumer +from decouple import RepositoryEnv, UndefinedValueError, config from pixl_cli._config import cli_config from pixl_cli._database import filter_exported_or_add_to_db @@ -47,6 +48,35 @@ def cli(*, debug: bool) -> None: set_log_level("WARNING" if not debug else "DEBUG") +@cli.command() +@click.option( + "--error", + is_flag=True, + show_default=True, + default=False, + help="Exit with error on missing env vars", +) +@click.option( + "--sample_env_file", + show_default=True, + default=None, + type=click.Path(exists=True), + help="Path to the sample env file", +) +def check_env(*, error: bool, sample_env_file: click.Path) -> None: + """Check that all variables from .env.sample are set either in .env or in environ""" + if not sample_env_file: + sample_env_file = Path(__file__).parents[3] / ".env.sample" + sample_config = RepositoryEnv(sample_env_file) + for key in sample_config.data: + try: + config(key) + except UndefinedValueError: # noqa: PERF203 + logger.warning("Environment variable %s is not set", key) + if error: + raise + + @cli.command() @click.option( "--queues", diff --git a/cli/tests/test_check_env.py b/cli/tests/test_check_env.py new file mode 100644 index 000000000..d968f5605 --- /dev/null +++ b/cli/tests/test_check_env.py @@ -0,0 +1,43 @@ +# Copyright (c) University College London Hospitals NHS Foundation Trust +# +# 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 +# +# http://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. +"""Tests for check_env function of CLI.""" +from pathlib import Path + +from click.testing import CliRunner +from pixl_cli.main import check_env + +SAMPLE_ENV_FILE = Path(__file__).parents[2] / ".env.sample" + + +def test_check_env(): + """ + Test that the check_env command runs without error. + - check_env works + - current test env file matches the sample env file + """ + runner = CliRunner() + result = runner.invoke(check_env) + assert result.exit_code == 0 + + +def test_check_env_fails(tmp_path): + """ + Test that check_env fails when the current test env file does not match the sample env file. + """ # noqa: D200 either this or it's 102 chars + tmp_sample_env_file = tmp_path / ".env.sample" + tmp_sample_env_file.write_text("NONEXISTENT_VARIABLE=") + + runner = CliRunner() + result = runner.invoke(check_env, tmp_sample_env_file.as_posix()) + assert result.exit_code != 0 diff --git a/config-template/example-config.yaml b/config-template/example-config.yaml deleted file mode 100644 index 20000f3cd..000000000 --- a/config-template/example-config.yaml +++ /dev/null @@ -1,10 +0,0 @@ -project: - name: "myproject" - modalities: ["DX", "CR"] - -tag_operations: - base_profile: "orthanc/orthanc-anon/plugin/tag-operations.yaml" # WARNING: might change! - -destination: - dicom: "ftps" - parquet: "ftps" diff --git a/config-template/project-slug.yaml b/config-template/project-slug.yaml new file mode 100644 index 000000000..aa18d6fa9 --- /dev/null +++ b/config-template/project-slug.yaml @@ -0,0 +1,23 @@ +# Copyright (c) 2022 University College London Hospitals NHS Foundation Trust +# +# 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 +# +# http://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. + +project: + name: "project-slug" + modalities: ["DX", "CR"] + +tag_operation_files: ["base-tag-operations.yaml"] + +destination: + dicom: "ftps" + parquet: "ftps" diff --git a/docker-compose.yml b/docker-compose.yml index 7b184df9b..de6300be2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -143,6 +143,7 @@ services: AZ_DICOM_TOKEN_REFRESH_SECS: "600" TIME_OFFSET: "${STUDY_TIME_OFFSET}" SALT_VALUE: ${SALT_VALUE}" + PROJECT_CONFIGS_DIR: ${PROJECT_CONFIGS_DIR:-/project_configs} ports: - "${ORTHANC_ANON_DICOM_PORT}:4242" - "${ORTHANC_ANON_WEB_PORT}:8042" @@ -151,6 +152,7 @@ services: source: orthanc-anon-data target: /var/lib/orthanc/db - ${PWD}/orthanc/orthanc-anon/config:/run/secrets:ro + - ${PWD}/project_configs:${PROJECT_CONFIGS_DIR:-/project_configs}:ro networks: - pixl-net depends_on: @@ -236,6 +238,7 @@ services: AZ_STORAGE_ACCOUNT_NAME: ${PIXL_EHR_API_AZ_STORAGE_ACCOUNT_NAME} AZ_STORAGE_CONTAINER_NAME: ${PIXL_EHR_API_AZ_STORAGE_CONTAINER_NAME} COGSTACK_REDACT_URL: ${PIXL_EHR_COGSTACK_REDACT_URL} + PROJECT_CONFIGS_DIR: ${PROJECT_CONFIGS_DIR:-/project_configs} env_file: - ./docker/common.env depends_on: @@ -255,6 +258,7 @@ services: - pixl-net volumes: - ${PWD}/exports:/run/exports + - ${PWD}/project_configs:${PROJECT_CONFIGS_DIR:-/project_configs}:ro imaging-api: build: diff --git a/docker/orthanc-anon/Dockerfile b/docker/orthanc-anon/Dockerfile index 8aa3e7cea..818cfbd31 100644 --- a/docker/orthanc-anon/Dockerfile +++ b/docker/orthanc-anon/Dockerfile @@ -22,7 +22,6 @@ RUN --mount=type=cache,target=/root/.cache \ && pip3 install pixl_dcmd/ COPY ./orthanc/orthanc-anon/plugin/pixl.py /etc/orthanc/pixl.py -COPY ./orthanc/orthanc-anon/plugin/tag-operations.yaml /etc/orthanc/tag-operations.yaml COPY ./pixl_core/ ./pixl_core RUN --mount=type=cache,target=/root/.cache \ diff --git a/orthanc/orthanc-anon/plugin/pixl.py b/orthanc/orthanc-anon/plugin/pixl.py index e97b6933f..27ce84dcd 100644 --- a/orthanc/orthanc-anon/plugin/pixl.py +++ b/orthanc/orthanc-anon/plugin/pixl.py @@ -26,18 +26,18 @@ import threading import traceback from io import BytesIO -from pathlib import Path from time import sleep from typing import TYPE_CHECKING import requests -import yaml from core import upload +from core.db.queries import get_project_slug_from_hashid +from core.project_config import load_project_config from decouple import config from pydicom import dcmread import orthanc -import pixl_dcmd +from pixl_dcmd.main import anonymise_dicom, write_dataset_to_bytes if TYPE_CHECKING: from typing import Any @@ -132,6 +132,23 @@ def AzureDICOMTokenRefresh(): return None +def Send(resourceId: str) -> None: + """Send the resource to the appropriate destination""" + msg = f"Sending {resourceId}" + logger.debug(msg) + + hashed_patient_id = _get_patient_id(resourceId) + slug = get_project_slug_from_hashid(hashed_patient_id) + project_config = load_project_config(slug) + + # send to destination + if project_config.destination.dicom == "ftps": + SendViaFTPS(resourceId) + else: + msg = f"Invalid destination: {project_config.destination.dicom}" + raise ValueError(msg) + + def SendViaStow(resourceId): """ Makes a POST API call to upload the resource to a dicom-web server @@ -223,7 +240,7 @@ def OnChange(changeType, level, resource): # noqa: ARG001 """ Three ChangeTypes included in this function: - If a study is stable and if ShouldAutoRoute returns true - then SendViaFTPS is called + then Send is called - If orthanc has started then message added to Orthanc LogWarning and AzureDICOMTokenRefresh called - If orthanc has stopped and TIMER is not none then message added @@ -235,7 +252,7 @@ def OnChange(changeType, level, resource): # noqa: ARG001 if changeType == orthanc.ChangeType.STABLE_STUDY: msg = f"Stable study: {resource}" logger.info(msg) - SendViaFTPS(resource) + Send(resource) if changeType == orthanc.ChangeType.ORTHANC_STARTED and _azure_available(): orthanc.LogWarning("Starting the scheduler") @@ -262,52 +279,15 @@ def ReceivedInstanceCallback(receivedDicom: bytes, origin: str) -> Any: # Read the bytes as DICOM/ dataset = dcmread(BytesIO(receivedDicom)) - # Drop anything that is not an X-Ray - if dataset.Modality not in ("DX", "CR"): - msg = f"Dropping DICOM Modality: {dataset.Modality}" - orthanc.LogError(msg) - return orthanc.ReceivedInstanceAction.DISCARD, None - # Attempt to anonymise and drop the study if any exceptions occur try: - return AnonymiseCallback(dataset) + dataset = anonymise_dicom(dataset) + return orthanc.ReceivedInstanceAction.MODIFY, write_dataset_to_bytes(dataset) except Exception: # noqa: BLE001 orthanc.LogError("Failed to anonymize study due to\n" + traceback.format_exc()) return orthanc.ReceivedInstanceAction.DISCARD, None -def AnonymiseCallback(dataset): - """ - Anonymisation of a dataset - Involves removing private tags and overlays and applying the - tag operations through functions in pixl_dcmd module - Returns writing anonymised dataset to disk - """ - orthanc.LogWarning("Anonymising received instance") - # Rip out all private tags/ - dataset.remove_private_tags() - orthanc.LogInfo("Removed private tags") - - # Rip out overlays/ - dataset = pixl_dcmd.remove_overlays(dataset) - orthanc.LogInfo("Removed overlays") - - # Apply anonymisation. - with Path("/etc/orthanc/tag-operations.yaml").open() as file: - # Load tag operations scheme from YAML. - tags = yaml.safe_load(file) - # Apply scheme to instance - dataset = pixl_dcmd.apply_tag_scheme(dataset, tags) - # Apply whitelist - dataset = pixl_dcmd.enforce_whitelist(dataset, tags) - orthanc.LogInfo("DICOM tag anonymisation applied") - - orthanc.LogWarning("DICOM tag anonymisation applied") - - # Write anonymised instance to disk. - return orthanc.ReceivedInstanceAction.MODIFY, pixl_dcmd.write_dataset_to_bytes(dataset) - - orthanc.RegisterOnChangeCallback(OnChange) orthanc.RegisterReceivedInstanceCallback(ReceivedInstanceCallback) orthanc.RegisterRestCallback("/heart-beat", OnHeartBeat) diff --git a/pixl_core/pyproject.toml b/pixl_core/pyproject.toml index 49d39ff7e..abde7af26 100644 --- a/pixl_core/pyproject.toml +++ b/pixl_core/pyproject.toml @@ -47,4 +47,4 @@ extend = "../ruff.toml" [tool.ruff.pep8-naming] # Allow Pydantic's `@validator` decorator to trigger class method treatment. -classmethod-decorators = ["classmethod", "pydantic.validator"] +classmethod-decorators = ["classmethod", "pydantic.field_validator"] diff --git a/pixl_core/src/core/db/queries.py b/pixl_core/src/core/db/queries.py index b35978be3..743b246f8 100644 --- a/pixl_core/src/core/db/queries.py +++ b/pixl_core/src/core/db/queries.py @@ -34,7 +34,7 @@ engine = create_engine(url) -def get_project_slug_from_db(hashed_value: str) -> str: +def get_project_slug_from_hashid(hashed_value: str) -> str: """ Get the project slug from the PIXL database for a given hashed identifier. Throws an exception if the image has already been exported. @@ -75,3 +75,22 @@ def _query_existing_image(pixl_session: Session, hashed_value: str) -> Image: ) .one() ) + + +def get_project_slug(patientid: str, accession_number: str) -> str: + """Get the project slug from the PIXL database for a given patientid.""" + PixlSession = sessionmaker(engine) + with PixlSession() as pixl_session, pixl_session.begin(): + return str( + pixl_session.query(Extract) + .join(Image, Extract.extract_id == Image.extract_id) + .filter( + Image.mrn == patientid, + Image.accession_number == accession_number, + ) + .filter( + Extract.extract_id == Image.extract_id, + ) + .one() + .slug + ) diff --git a/pixl_core/src/core/config.py b/pixl_core/src/core/project_config.py similarity index 52% rename from pixl_core/src/core/config.py rename to pixl_core/src/core/project_config.py index 987713bfd..dd84a4448 100644 --- a/pixl_core/src/core/config.py +++ b/pixl_core/src/core/project_config.py @@ -15,24 +15,39 @@ """Project-specific configuration for Pixl.""" from __future__ import annotations +import logging from enum import Enum from pathlib import Path -from typing import TYPE_CHECKING, Optional +from typing import Any import yaml -from pydantic import BaseModel, validator +from decouple import config +from pydantic import BaseModel, field_validator -if TYPE_CHECKING: - from typing_extensions import Any +PROJECT_CONFIGS_DIR = Path(config("PROJECT_CONFIGS_DIR")) +logger = logging.getLogger(__name__) -def load_config(filename: Path) -> Any: + +def load_project_config(project_slug: str) -> PixlConfig | Any: + """ + Load configuration for a project based on its slug. + Project needs to have a corresponding yaml file in the `$PROJECT_CONFIGS_DIR` directory. + """ + configpath = PROJECT_CONFIGS_DIR / f"{project_slug}.yaml" + logger.warning(f"Loading config for {project_slug} from {configpath}") # noqa: G004 + if not configpath.exists(): + raise FileNotFoundError(f"No config for {project_slug}. Please submit PR and redeploy.") # noqa: EM102, TRY003 + return _load_project_config(configpath) + + +def _load_project_config(filename: Path) -> PixlConfig | Any: """ Load configuration from a yaml file. :param filename: Path to the yaml file """ yaml_data = yaml.safe_load(filename.read_text()) - return PixlConfig.parse_obj(yaml_data) + return PixlConfig.model_validate(yaml_data) class _Project(BaseModel): @@ -40,25 +55,6 @@ class _Project(BaseModel): modalities: list[str] -class _TagOperations(BaseModel): - base_profile: Path - extension_profile: Optional[Path] - - @validator("base_profile") - def valid_base_path(cls, v: Path) -> Path: - if not v.exists(): - msg = "Base profile should be an existing path" - raise ValueError(msg) - return v - - @validator("extension_profile") - def valid_extenstion_path(cls, v: Path) -> Path: - if isinstance(v, Path) & (not v.exists()): - msg = "Extension profile should be an existing path" - raise ValueError(msg) - return v - - class _DestinationEnum(str, Enum): """Defines the valid upload destinations.""" @@ -72,7 +68,7 @@ class _Destination(BaseModel): dicom: _DestinationEnum parquet: _DestinationEnum - @validator("parquet") + @field_validator("parquet") def valid_parquet_destination(cls, v: str) -> str: if v == "dicomweb": msg = "Parquet destination cannot be dicomweb" @@ -84,5 +80,20 @@ class PixlConfig(BaseModel): """Project-specific configuration for Pixl.""" project: _Project - tag_operations: _TagOperations + tag_operation_files: list[Path] destination: _Destination + + @field_validator("tag_operation_files", mode="before") + def _valid_tag_operations(cls, tag_ops_files: list[str]) -> list[Path]: + if not tag_ops_files or len(tag_ops_files) == 0: + msg = "There should be at least 1 tag operations file" + raise ValueError(msg) + + if len(tag_ops_files) > 1: + msg = "There should currently be at most 1 tag operations file." + raise ValueError(msg) + + # Pydantic will automatically check if the file exists + return [ + PROJECT_CONFIGS_DIR / "tag-operations" / tag_ops_file for tag_ops_file in tag_ops_files + ] diff --git a/pixl_core/src/core/upload.py b/pixl_core/src/core/upload.py index 5a9f76e81..db5c08e4b 100644 --- a/pixl_core/src/core/upload.py +++ b/pixl_core/src/core/upload.py @@ -31,7 +31,7 @@ from core.exports import ParquetExport -from core.db.queries import get_project_slug_from_db, update_exported_at +from core.db.queries import get_project_slug_from_hashid, update_exported_at logger = logging.getLogger(__name__) @@ -66,7 +66,7 @@ def upload_dicom_image(zip_content: BinaryIO, pseudo_anon_id: str) -> None: logger.info("Starting FTPS upload of '%s'", pseudo_anon_id) # rename destination to {project-slug}/{study-pseduonymised-id}.zip - remote_directory = get_project_slug_from_db(pseudo_anon_id) + remote_directory = get_project_slug_from_hashid(pseudo_anon_id) # Create the remote directory if it doesn't exist ftp = _connect_to_ftp() diff --git a/pixl_core/tests/conftest.py b/pixl_core/tests/conftest.py index b9579756e..2c62e46d1 100644 --- a/pixl_core/tests/conftest.py +++ b/pixl_core/tests/conftest.py @@ -42,6 +42,7 @@ os.environ["FTP_USER_NAME"] = "pixl" os.environ["FTP_USER_PASSWORD"] = "longpassword" # noqa: S105 Hardcoding password os.environ["FTP_PORT"] = "20021" +os.environ["PROJECT_CONFIGS_DIR"] = str(TEST_DIR.parents[1] / "project_configs") @pytest.fixture(scope="package") diff --git a/pixl_core/tests/test_config.py b/pixl_core/tests/test_project_config.py similarity index 52% rename from pixl_core/tests/test_config.py rename to pixl_core/tests/test_project_config.py index 16e7bfb33..66448763e 100644 --- a/pixl_core/tests/test_config.py +++ b/pixl_core/tests/test_project_config.py @@ -12,51 +12,38 @@ # See the License for the specific language governing permissions and # limitations under the License. + from pathlib import Path import pytest -from core.config import PixlConfig, load_config +from core.project_config import PixlConfig, _load_project_config +from decouple import config from pydantic import ValidationError -EXAMPLE_CONFIG = Path(__file__).parents[2] / "config-template" / "example-config.yaml" +PROJECT_CONFIGS_DIR = Path(config("PROJECT_CONFIGS_DIR")) +TEST_CONFIG = PROJECT_CONFIGS_DIR / "test-extract-uclh-omop-cdm.yaml" def test_config_from_file(): """Test whether config file is correctly parsed and validated.""" - config = load_config(EXAMPLE_CONFIG) + project_config = _load_project_config(TEST_CONFIG) - assert config.project.name == "myproject" - assert config.project.modalities == ["DX", "CR"] - assert config.tag_operations.base_profile == Path( - "orthanc/orthanc-anon/plugin/tag-operations.yaml" - ) - assert config.tag_operations.base_profile.exists() - assert config.tag_operations.extension_profile is None - assert config.destination.dicom == "ftps" - assert config.destination.parquet == "ftps" + assert project_config.project.name == "test-extract-uclh-omop-cdm" + assert project_config.project.modalities == ["DX", "CR"] + assert project_config.tag_operation_files == [ + PROJECT_CONFIGS_DIR / "tag-operations" / "test-extract-uclh-omop-cdm-tag-operations.yaml" + ] + assert project_config.destination.dicom == "ftps" + assert project_config.destination.parquet == "ftps" BASE_YAML_DATA = { "project": {"name": "myproject", "modalities": ["DX", "CR"]}, - "tag_operations": { - "base_profile": "orthanc/orthanc-anon/plugin/tag-operations.yaml", - "extension_profile": None, - }, + "tag_operations": ["test-extract-uclh-omop-cdm-tag-operations.yaml"], "destination": {"dicom": "ftps", "parquet": "ftps"}, } -def test_valid_extension_profile(): - """Test that the config validation passes for valid extension profile.""" - config_data = BASE_YAML_DATA - config_data["tag_operations"][ - "extension_profile" - ] = "orthanc/orthanc-anon/plugin/tag-operations.yaml" - - config = PixlConfig.parse_obj(config_data) - assert config.tag_operations.extension_profile.exists() - - def test_parquet_dicom_fails(): """ Test that the config validation fails for non-valid values: 'dicomweb' not allowed for @@ -65,7 +52,7 @@ def test_parquet_dicom_fails(): config_data = BASE_YAML_DATA config_data["destination"]["parquet"] = "dicomweb" with pytest.raises(ValidationError): - PixlConfig.parse_obj(config_data) + PixlConfig.model_validate(config_data) def test_invalid_destinations(): @@ -74,17 +61,12 @@ def test_invalid_destinations(): config_data["destination"]["dicom"] = "nope" config_data["destination"]["parquet"] = "nope" with pytest.raises(ValidationError): - PixlConfig.parse_obj(config_data) + PixlConfig.model_validate(config_data) def test_invalid_paths(): """Test that the config validation fails for invalid tag-operation paths.""" config_data_wrong_base = BASE_YAML_DATA - config_data_wrong_base["tag_operations"]["base_profile"] = "/i/dont/exist.yaml" - with pytest.raises(ValidationError): - PixlConfig.parse_obj(config_data_wrong_base) - - config_data_wrong_extension = BASE_YAML_DATA - config_data_wrong_extension["tag_operations"]["extension_profile"] = "/i/dont/exist.yaml" + config_data_wrong_base["tag_operations"][0] = "/i/dont/exist.yaml" with pytest.raises(ValidationError): - PixlConfig.parse_obj(config_data_wrong_extension) + PixlConfig.model_validate(config_data_wrong_base) diff --git a/pixl_core/tests/test_upload.py b/pixl_core/tests/test_upload.py index c6abf706e..0e4d5a344 100644 --- a/pixl_core/tests/test_upload.py +++ b/pixl_core/tests/test_upload.py @@ -19,7 +19,7 @@ import pandas as pd import pytest from core.db.models import Image -from core.db.queries import get_project_slug_from_db, update_exported_at +from core.db.queries import get_project_slug_from_hashid, update_exported_at from core.upload import upload_dicom_image, upload_parquet_files @@ -29,7 +29,7 @@ def test_upload_dicom_image(test_zip_content, not_yet_exported_dicom_image, ftps # ARRANGE # Get the pseudo identifier from the test image pseudo_anon_id = not_yet_exported_dicom_image.hashed_identifier - project_slug = get_project_slug_from_db(pseudo_anon_id) + project_slug = get_project_slug_from_hashid(pseudo_anon_id) expected_output_file = ftps_home_dir / project_slug / (pseudo_anon_id + ".zip") # ACT diff --git a/pixl_dcmd/src/pixl_dcmd/main.py b/pixl_dcmd/src/pixl_dcmd/main.py index 435eba5ae..87ad51509 100644 --- a/pixl_dcmd/src/pixl_dcmd/main.py +++ b/pixl_dcmd/src/pixl_dcmd/main.py @@ -15,8 +15,11 @@ from io import BytesIO from os import PathLike +from pathlib import Path from typing import Any, BinaryIO, Union from logging import getLogger +from core.db.queries import get_project_slug +from core.project_config import load_project_config import requests from decouple import config @@ -25,6 +28,7 @@ from pixl_dcmd._database import add_hashed_identifier_and_save, query_db from pixl_dcmd._datetime import combine_date_time, format_date_time from pixl_dcmd._deid_helpers import get_bounded_age, get_encrypted_uid +import yaml DicomDataSetType = Union[Union[str, bytes, PathLike[Any]], BinaryIO] @@ -44,6 +48,64 @@ def write_dataset_to_bytes(dataset: Dataset) -> bytes: return buffer.read() +def anonymise_dicom(dataset: Dataset) -> Dataset: + """ + Anonymises a DICOM dataset as Received by Orthanc. + Finds appropriate configuration based on project name and anonymises by + - dropping datasets of the wrong modality + - removing private tags + - removing overlays + - applying tag operations based on the config file + Returns anonymised dataset. + """ + slug = get_project_slug(dataset.PatientID, dataset.AccessionNumber) + project_config = load_project_config(slug) + logger.error(f"Received instance for project {slug}") + # Drop anything that is not an X-Ray + if dataset.Modality not in project_config.project.modalities: + msg = f"Dropping DICOM Modality: {dataset.Modality}" + logger.error(msg) + raise ValueError(msg) + + logger.warning("Anonymising received instance") + # Rip out all private tags/ + dataset.remove_private_tags() + logger.info("Removed private tags") + + # Rip out overlays/ + dataset = remove_overlays(dataset) + logger.info("Removed overlays") + + # Merge tag schemes + all_tags = merge_tag_schemes(project_config.tag_operation_files) + + # Apply scheme to instance + dataset = apply_tag_scheme(dataset, all_tags) + # Apply whitelist + dataset = enforce_whitelist(dataset, all_tags) + + logger.info( + f"DICOM tag anonymisation applied according to {project_config.tag_operation_files}" + ) + logger.warning("DICOM tag anonymisation applied") + + # Write anonymised instance to disk. + return dataset + + +def merge_tag_schemes(tag_operation_files: list[Path]) -> Any: + """ + NOT IMPLEMENTED, WORKS ONLY WITH A SINGLE TAG SCHEME + Merge multiple tag schemes into a single dictionary. + """ + if len(tag_operation_files) > 1: + raise NotImplementedError("Multiple tag schemes not supported") + with tag_operation_files[0].open() as file: + # Load tag operations scheme from YAML. + tags = yaml.safe_load(file) + return tags + + def remove_overlays(dataset: Dataset) -> Dataset: """ Search for overlays planes and remove them. @@ -76,7 +138,7 @@ def remove_overlays(dataset: Dataset) -> Dataset: return dataset -def enforce_whitelist(dataset: dict, tags: dict) -> dict: +def enforce_whitelist(dataset: dict, tags: list[dict]) -> dict: """Delete any tags not in the tagging scheme.""" # For every element: logger.debug("Enforcing whitelist") @@ -105,7 +167,7 @@ def enforce_whitelist(dataset: dict, tags: dict) -> dict: return dataset -def apply_tag_scheme(dataset: dict, tags: dict) -> dict: +def apply_tag_scheme(dataset: dict, tags: list[dict]) -> dict: """ Apply anonymisation operations for a given set of tags to a dataset. The original study time is kept before any operations are applied. diff --git a/pixl_dcmd/tests/conftest.py b/pixl_dcmd/tests/conftest.py index d9c266e8c..d52e7429e 100644 --- a/pixl_dcmd/tests/conftest.py +++ b/pixl_dcmd/tests/conftest.py @@ -28,6 +28,9 @@ os.environ["HASHER_API_AZ_NAME"] = "test_hash_API" os.environ["HASHER_API_PORT"] = "test_hash_API_port" os.environ["TIME_OFFSET"] = "5" +os.environ["PROJECT_CONFIGS_DIR"] = str( + pathlib.Path(__file__).parents[2] / "project_configs" +) STUDY_DATE = datetime.date.fromisoformat("2023-01-01") diff --git a/pixl_dcmd/tests/test_main.py b/pixl_dcmd/tests/test_main.py index 15834fcaf..f1e0fb1a4 100644 --- a/pixl_dcmd/tests/test_main.py +++ b/pixl_dcmd/tests/test_main.py @@ -27,6 +27,7 @@ from core.db.models import Image from pixl_dcmd.main import ( apply_tag_scheme, + merge_tag_schemes, remove_overlays, ) @@ -36,7 +37,7 @@ def tag_scheme() -> dict: """Read the tag scheme from orthanc raw.""" tag_file = ( pathlib.Path(__file__).parents[2] - / "orthanc/orthanc-anon/plugin/tag-operations.yaml" + / "project_configs/tag-operations/test-extract-uclh-omop-cdm-tag-operations.yaml" ) return yaml.safe_load(tag_file.read_text()) @@ -136,3 +137,11 @@ def test_can_nifti_convert_post_anonymisation( assert anon_nifti.shape == ident_nifti.shape assert np.all(anon_nifti.header.get_sform() == ident_nifti.header.get_sform()) assert np.all(anon_nifti.get_fdata() == ident_nifti.get_fdata()) + + +def test_merge_tag_schemes_single_file(): + tag_ops_file = ( + pathlib.Path(__file__).parents[2] + / "project_configs/tag-operations/test-extract-uclh-omop-cdm-tag-operations.yaml" + ) + merge_tag_schemes([tag_ops_file]) diff --git a/pixl_ehr/src/pixl_ehr/main.py b/pixl_ehr/src/pixl_ehr/main.py index 40ba50ef6..2ca76289f 100644 --- a/pixl_ehr/src/pixl_ehr/main.py +++ b/pixl_ehr/src/pixl_ehr/main.py @@ -27,12 +27,14 @@ from azure.storage.blob import BlobServiceClient from core.exports import ParquetExport from core.patient_queue import PixlConsumer +from core.project_config import load_project_config from core.rest_api.router import router, state from core.upload import upload_parquet_files from decouple import config -from fastapi import FastAPI +from fastapi import FastAPI, HTTPException from fastapi.responses import JSONResponse from pydantic import BaseModel +from slugify import slugify from ._databases import PIXLDatabase from ._processing import process_message @@ -92,12 +94,20 @@ def export_patient_data(export_params: ExportRadiologyData) -> None: logger.info("Exporting Patient Data for '%s'", export_params.project_name) export_radiology_as_parquet(export_params) + project_slug = slugify(export_params.project_name) + project_config = load_project_config(project_slug) # Upload Parquet files to the appropriate endpoint - upload_parquet_files( - ParquetExport( - export_params.project_name, export_params.extract_datetime, export_params.output_dir + + if project_config.destination.parquet == "ftps": + upload_parquet_files( + ParquetExport( + export_params.project_name, export_params.extract_datetime, export_params.output_dir + ) ) - ) + else: + msg = f"Destination {project_config.destination.parquet} for parquet files unavailable" + logger.error(msg) + raise HTTPException(status_code=400, detail=msg) def export_radiology_as_parquet(export_params: ExportRadiologyData) -> None: diff --git a/pixl_ehr/tests/conftest.py b/pixl_ehr/tests/conftest.py index 10e2c7c91..289d668cd 100644 --- a/pixl_ehr/tests/conftest.py +++ b/pixl_ehr/tests/conftest.py @@ -34,11 +34,12 @@ os.environ["EMAP_UDS_PASSWORD"] = "postgres" # noqa: S105 os.environ["EMAP_UDS_SCHEMA_NAME"] = "star" os.environ["COGSTACK_REDACT_URL"] = "test" +os.environ["PROJECT_CONFIGS_DIR"] = str(Path(__file__).parents[2] / "project_configs") TEST_DIR = Path(__file__).parent -@pytest.fixture(scope="package", autouse=True) +@pytest.fixture(scope="package") def run_containers() -> subprocess.CompletedProcess[bytes]: """Run docker containers for tests which require them.""" yield subprocess.run( diff --git a/pixl_ehr/tests/test_processing.py b/pixl_ehr/tests/test_processing.py index 602c7e824..231d144fb 100644 --- a/pixl_ehr/tests/test_processing.py +++ b/pixl_ehr/tests/test_processing.py @@ -235,6 +235,7 @@ def insert_data_into_emap_star_schema() -> None: @pytest.mark.processing() @pytest.mark.asyncio() +@pytest.mark.usefixtures("run_containers") async def test_message_processing(example_messages) -> None: """ GIVEN some patient metadata in Emap @@ -299,6 +300,7 @@ async def test_message_processing(example_messages) -> None: @pytest.mark.processing() @pytest.mark.asyncio() +@pytest.mark.usefixtures("run_containers") async def test_radiology_export(example_messages, tmp_path) -> None: """ GIVEN a message processed by the EHR API @@ -334,6 +336,7 @@ async def test_radiology_export(example_messages, tmp_path) -> None: @pytest.mark.processing() @pytest.mark.asyncio() +@pytest.mark.usefixtures("run_containers") async def test_radiology_export_multiple_projects(example_messages, tmp_path) -> None: """ GIVEN EHR API has processed four messages, each from a different project+extract combination diff --git a/orthanc/orthanc-anon/plugin/tag-operations.yaml b/project_configs/tag-operations/test-extract-uclh-omop-cdm-tag-operations.yaml similarity index 100% rename from orthanc/orthanc-anon/plugin/tag-operations.yaml rename to project_configs/tag-operations/test-extract-uclh-omop-cdm-tag-operations.yaml diff --git a/project_configs/tag-operations/uclh-nasogastric-tube-project-ngt-only-full-dataset-tag-operations.yaml b/project_configs/tag-operations/uclh-nasogastric-tube-project-ngt-only-full-dataset-tag-operations.yaml new file mode 100644 index 000000000..201b34a2e --- /dev/null +++ b/project_configs/tag-operations/uclh-nasogastric-tube-project-ngt-only-full-dataset-tag-operations.yaml @@ -0,0 +1,593 @@ +# Copyright (c) University College London Hospitals NHS Foundation Trust +# +# 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 +# +# http://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. +- name: "Specific Character Set" + group: 0x0008 + element: 0x0005 + op: "keep" +- name: "Image Type" + group: 0x0008 + element: 0x0008 + op: "keep" +- name: "SOP Class UID" + group: 0x0008 + element: 0x0016 + op: "keep" +- name: "SOP Instance UID" + group: 0x0008 + element: 0x0018 + op: "hash-uid" +- name: "Study Date" + group: 0x0008 + element: 0x0020 + op: "delete" +- name: "Series Date" + group: 0x0008 + element: 0x0021 + op: "delete" +- name: "Acquisition Date" + group: 0x0008 + element: 0x0022 + op: "delete" +- name: "Image Date" + group: 0x0008 + element: 0x0023 + op: "delete" +- name: "Acquisition Date Time" + group: 0x0008 + element: 0x002a + op: "delete" +- name: "Study Time" + group: 0x0008 + element: 0x0030 + op: "delete" +- name: "Series Time" + group: 0x0008 + element: 0x0031 + op: "delete" +- name: "Acquisition Time" + group: 0x0008 + element: 0x0032 + op: "delete" +- name: "Image Time" + group: 0x0008 + element: 0x0033 + op: "delete" +- name: "Accession Number" + group: 0x0008 + element: 0x0050 + op: "secure-hash" +- name: "Modality" + group: 0x0008 + element: 0x0060 + op: "keep" +- name: "Modalities In Study" + group: 0x0008 + element: 0x0061 + op: "keep" +- name: "Presentation Intent Type" + group: 0x0008 + element: 0x0068 + op: "delete" +- name: "Manufacturer" + group: 0x0008 + element: 0x0070 + op: "keep" +- name: "Institution Name" + group: 0x0008 + element: 0x0080 + op: "delete" +- name: "Institution Address" + group: 0x0008 + element: 0x0081 + op: "delete" +- name: "Referring Physicians Name" + group: 0x0008 + element: 0x0090 + op: "delete" +- name: "Station Name" + group: 0x0008 + element: 0x1010 + op: "delete" +- name: "Study Description" + group: 0x0008 + element: 0x1030 + op: "keep" +- name: "Series Description" + group: 0x0008 + element: 0x103e + op: "keep" +- name: "Institutional Department Name" + group: 0x0008 + element: 0x1040 + op: "delete" +- name: "Performing Physicians Name" + group: 0x0008 + element: 0x1050 + op: "delete" +- name: "Operators Name" + group: 0x0008 + element: 0x1070 + op: "delete" +- name: "Manufacturers Model Name" + group: 0x0008 + element: 0x1090 + op: "keep" +- name: "Referenced Study Sequence" + group: 0x0008 + element: 0x1110 + op: "delete" +- name: "Referenced Patient Sequence" + group: 0x0008 + element: 0x1120 + op: "delete" +- name: "Source Image Sequence" + group: 0x0008 + element: 0x2112 + op: "delete" +- name: "Anatomic Region Sequence" + group: 0x0008 + element: 0x2218 + op: "delete" +- name: "Irradiation Event UID" + group: 0x0008 + element: 0x3010 + op: "delete" +- name: "Patients Name" + group: 0x0010 + element: 0x0010 + op: "secure-hash" +- name: "Patient ID" + group: 0x0010 + element: 0x0020 + op: "secure-hash" +- name: "Issuer Of Patient ID" + group: 0x0010 + element: 0x0021 + op: "delete" +- name: "Patients Birth Date" + group: 0x0010 + element: 0x0030 + op: "delete" +- name: "Patients Birth Time" + group: 0x0010 + element: 0x0032 + op: "delete" +- name: "Patients Sex" + group: 0x0010 + element: 0x0040 + op: "delete" +- name: "Other Patient IDs" + group: 0x0010 + element: 0x1000 + op: "delete" +- name: "Other Patient Names" + group: 0x0010 + element: 0x1001 + op: "delete" +- name: "Patients Age" + group: 0x0010 + element: 0x1010 + op: "delete" +- name: "Patients Size" + group: 0x0010 + element: 0x1020 + op: "keep" +- name: "Patients Weight" + group: 0x0010 + element: 0x1030 + op: "keep" +- name: "Patients Address" + group: 0x0010 + element: 0x1040 + op: "delete" +- name: "Medical Alerts" + group: 0x0010 + element: 0x2000 + op: "delete" +- name: "Contrast Allergies" + group: 0x0010 + element: 0x2110 + op: "delete" +- name: "Patient Comments" + group: 0x0010 + element: 0x4000 + op: "delete" +- name: "Private Creator Data Element" + group: 0x0011 + element: 0x0010 + op: "delete" +- name: "Body Part Examined" + group: 0x0018 + element: 0x0015 + op: "keep" +- name: "kVp" + group: 0x0018 + element: 0x0060 + op: "keep" +- name: "Software Version" + group: 0x0018 + element: 0x1020 + op: "keep" +- name: "Protocol Name" + group: 0x0018 + element: 0x1030 + op: "delete" +- name: "Field Of View Dimension" + group: 0x0018 + element: 0x1149 + op: "keep" +- name: "Exposure Time" + group: 0x0018 + element: 0x1150 + op: "keep" +- name: "X Ray Tube Current" + group: 0x0018 + element: 0x1151 + op: "keep" +- name: "Exposure" + group: 0x0018 + element: 0x1152 + op: "keep" +- name: "Exposure In Uas" + group: 0x0018 + element: 0x1153 + op: "keep" +- name: "Image Area Dose Product" + group: 0x0018 + element: 0x115e + op: "keep" +- name: "Imager Pixel Spacing" + group: 0x0018 + element: 0x1164 + op: "keep" +- name: "Grid" + group: 0x0018 + element: 0x1166 + op: "keep" +- name: "Focal Spot" + group: 0x0018 + element: 0x1190 + op: "keep" +- name: "Acquisition Device Processing Description" + group: 0x0018 + element: 0x1400 + op: "keep" +- name: "Exposure Index" + group: 0x0018 + element: 0x1411 + op: "keep" +- name: "Target Exposure Index" + group: 0x0018 + element: 0x1412 + op: "keep" +- name: "Deviation Index" + group: 0x0018 + element: 0x1413 + op: "keep" +- name: "Positioner Type" + group: 0x0018 + element: 0x1508 + op: "keep" +- name: "Collemator Shape" + group: 0x0018 + element: 0x1700 + op: "keep" +- name: "Vertices Of The Polygonal Collimator" + group: 0x0018 + element: 0x1720 + op: "keep" +- name: "View Position" + group: 0x0018 + element: 0x5101 + op: "keep" +- name: "Sensitivity" + group: 0x0018 + element: 0x6000 + op: "keep" +- name: "Detector Temperature" + group: 0x0018 + element: 0x7001 + op: "keep" +- name: "Detector Type" + group: 0x0018 + element: 0x7004 + op: "keep" +- name: "Detector Configuration" + group: 0x0018 + element: 0x7005 + op: "keep" +- name: "Detector ID" + group: 0x0018 + element: 0x700a + op: "keep" +- name: "Detector Binning" + group: 0x0018 + element: 0x701a + op: "keep" +- name: "Detector Element Physical Size" + group: 0x0018 + element: 0x7020 + op: "keep" +- name: "Detector Element Spacing" + group: 0x0018 + element: 0x7022 + op: "keep" +- name: "Detector Active Shape" + group: 0x0018 + element: 0x7024 + op: "keep" +- name: "Detector Active Dimensions" + group: 0x0018 + element: 0x7026 + op: "keep" +- name: "Field Of View Origin" + group: 0x0018 + element: 0x7030 + op: "keep" +- name: "Field Of View Rotation" + group: 0x0018 + element: 0x7032 + op: "keep" +- name: "Field Of View Horizontal Flip" + group: 0x0018 + element: 0x7034 + op: "keep" +- name: "Grid Focal Distance" + group: 0x0018 + element: 0x704c + op: "keep" +- name: "Exposure Control Mode" + group: 0x0018 + element: 0x7060 + op: "keep" +- name: "Study Instance UID" + group: 0x0020 + element: 0x000d + op: "hash-uid" +- name: "Series Instance UID" + group: 0x0020 + element: 0x000e + op: "hash-uid" +- name: "Study ID" + group: 0x0020 + element: 0x0010 + op: "fixed" +- name: "Series Number" + group: 0x0020 + element: 0x0011 + op: "keep" +- name: "Image Number" + group: 0x0020 + element: 0x0013 + op: "keep" +- name: "Patient Orientation" + group: 0x0020 + element: 0x0020 + op: "keep" +- name: "Image Laterality" + group: 0x0020 + element: 0x0062 + op: "keep" +- name: "Number Of Study Related Images" + group: 0x0020 + element: 0x1208 + op: "delete" +- name: "Samples Per Pixel" + group: 0x0028 + element: 0x0002 + op: "keep" +- name: "Photometric Interpretation" + group: 0x0028 + element: 0x0004 + op: "keep" +- name: "Rows" + group: 0x0028 + element: 0x0010 + op: "keep" +- name: "Columns" + group: 0x0028 + element: 0x0011 + op: "keep" +- name: "Pixel Spacing" + group: 0x0028 + element: 0x0030 + op: "keep" +- name: "Bits Allocated" + group: 0x0028 + element: 0x0100 + op: "keep" +- name: "Bits Stored" + group: 0x0028 + element: 0x0101 + op: "keep" +- name: "High Bit" + group: 0x0028 + element: 0x0102 + op: "keep" +- name: "Pixel Representation" + group: 0x0028 + element: 0x0103 + op: "keep" +- name: "Quality Control Image" + group: 0x0028 + element: 0x0300 + op: "keep" +- name: "Burned In Annotation" + group: 0x0028 + element: 0x0301 + op: "keep" +- name: "Pixel Spacing Calibration Type" + group: 0x0028 + element: 0x0a02 + op: "keep" +- name: "Pixel Spacing Calibration Description" + group: 0x0028 + element: 0x0a04 + op: "keep" +- name: "Pixel Intensity Relationship" + group: 0x0028 + element: 0x1040 + op: "keep" +- name: "Pixel Intensity Relationship Sign" + group: 0x0028 + element: 0x1041 + op: "keep" +- name: "Window Center" + group: 0x0028 + element: 0x1050 + op: "keep" +- name: "Window Width" + group: 0x0028 + element: 0x1051 + op: "keep" +- name: "Rescale Intercept" + group: 0x0028 + element: 0x1052 + op: "keep" +- name: "Rescale Slope" + group: 0x0028 + element: 0x1053 + op: "keep" +- name: "Rescale Type" + group: 0x0028 + element: 0x1054 + op: "keep" +- name: "Window Center And Width Explanation" + group: 0x0028 + element: 0x1055 + op: "keep" +- name: "Lossy Image Compression" + group: 0x0028 + element: 0x2110 + op: "keep" +- name: "VOI LUT Sequence" + group: 0x0028 + element: 0x3010 + op: "keep" +- name: "Current Patient Location" + group: 0x0038 + element: 0x0300 + op: "delete" +- name: "Patient State" + group: 0x0038 + element: 0x0500 + op: "delete" +- name: "Performed Procedure Start Date" + group: 0x0040 + element: 0x0244 + op: "delete" +- name: "Performed Procedure Start Time" + group: 0x0040 + element: 0x0245 + op: "delete" +- name: "Performed Procedure Step ID" + group: 0x0040 + element: 0x0253 + op: "delete" +- name: "Performed Procedure Step Description" + group: 0x0040 + element: 0x0254 + op: "delete" +- name: "Performed Action Item Sequence" + group: 0x0040 + element: 0x0260 + op: "delete" +- name: "Request Attributes Sequence" + group: 0x0040 + element: 0x0275 + op: "delete" +- name: "Acquisition Context Sequence" + group: 0x0040 + element: 0x0555 + op: "delete" +- name: "Confidentiality Code" + group: 0x0040 + element: 0x1008 + op: "delete" +- name: "Private Creator Data Element" + group: 0x0045 + element: 0x0010 + op: "delete" +- name: "View Code Sequence" + group: 0x0054 + element: 0x0220 + op: "keep" +- name: "Image Comments" + group: 0x0020 + element: 0x4000 + op: "delete" +- name: "Instance Creator UID" + group: 0x0008 + element: 0x0014 + op: "hash-uid" +- name: "Referenced SOP Instance UID" + group: 0x0008 + element: 0x1155 + op: "hash-uid" +- name: "Frame of Reference UID" + group: 0x0020 + element: 0x0052 + op: "hash-uid" +- name: "Synchronization Frame of Reference UID" + group: 0x0020 + element: 0x0200 + op: "hash-uid" +- name: "Storage Media File-set UID" + group: 0x0088 + element: 0x0140 + op: "hash-uid" +- name: "Referenced Frame of Reference UID" + group: 0x3006 + element: 0x0024 + op: "hash-uid" +- name: "Related Frame of Reference UID" + group: 0x3006 + element: 0x00C2 + op: "hash-uid" +- name: "UID" + group: 0x0040 + element: 0xA124 + op: "hash-uid" +- name: "Study Comments [Retired]" + group: 0x0032 + element: 0x4000 + op: "delete" +- name: "Ethnic Group" + group: 0x0010 + element: 0x2160 + op: "delete" +- name: "Physicians Of Record" + group: 0x0008 + element: 0x1048 + op: "delete" +- name: "Name Of Physicians Reading Study" + group: 0x0008 + element: 0x1060 + op: "delete" +- name: "Device Serial Number" + group: 0x0018 + element: 0x1000 + op: "delete" +- name: "Additional Patient History" + group: 0x0010 + element: 0x21b0 + op: "delete" +- name: "Pregnancy Status" + group: 0x0010 + element: 0x21c0 + op: "delete" +- name: "Pixel Data" + group: 0x7fe0 + element: 0x0010 + op: "keep" diff --git a/project_configs/test-extract-uclh-omop-cdm.yaml b/project_configs/test-extract-uclh-omop-cdm.yaml new file mode 100644 index 000000000..eebdea178 --- /dev/null +++ b/project_configs/test-extract-uclh-omop-cdm.yaml @@ -0,0 +1,23 @@ +# Copyright (c) 2022 University College London Hospitals NHS Foundation Trust +# +# 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 +# +# http://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. + +project: + name: "test-extract-uclh-omop-cdm" + modalities: ["DX", "CR"] + +tag_operation_files: ["test-extract-uclh-omop-cdm-tag-operations.yaml"] + +destination: + dicom: "ftps" + parquet: "ftps" diff --git a/project_configs/uclh-nasogastric-tube-project-ngt-only-full-dataset.yaml b/project_configs/uclh-nasogastric-tube-project-ngt-only-full-dataset.yaml new file mode 100644 index 000000000..eb5fab111 --- /dev/null +++ b/project_configs/uclh-nasogastric-tube-project-ngt-only-full-dataset.yaml @@ -0,0 +1,24 @@ +# Copyright (c) 2022 University College London Hospitals NHS Foundation Trust +# +# 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 +# +# http://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. + +project: + name: "uclh-nasogastric-tube-project-ngt-only-full-dataset" + modalities: ["DX", "CR"] + +tag_operation_files: + ["uclh-nasogastric-tube-project-ngt-only-full-dataset-tag-operations.yaml"] + +destination: + dicom: "ftps" + parquet: "ftps" diff --git a/test/.env b/test/.env index 3dadc3eed..b3aaa097b 100644 --- a/test/.env +++ b/test/.env @@ -66,3 +66,6 @@ RABBITMQ_PASSWORD=rabbitmq_password FTP_HOST=ftp-server FTP_USER_NAME=ftp_username FTP_USER_PASSWORD=longpassword + +# Project configs directory +PROJECT_CONFIGS_DIR=/project_configs \ No newline at end of file diff --git a/test/run-system-test.sh b/test/run-system-test.sh index d26123276..b3da0a22b 100755 --- a/test/run-system-test.sh +++ b/test/run-system-test.sh @@ -57,4 +57,4 @@ docker exec system-test-ehr-api-1 rm -r /run/exports/test-extract-uclh-omop-cdm/ ./scripts/check_max_storage_in_orthanc_raw.py cd "${PACKAGE_DIR}" -docker compose -f docker-compose.yml -f test/docker-compose.yml -p system-test down --volumes +docker compose --env-file test/.env -f docker-compose.yml -f test/docker-compose.yml -p system-test down --volumes From 582c995e11dbd2549f1bcc7ba1931818eb7cd756 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Thu, 22 Feb 2024 10:39:00 +0000 Subject: [PATCH 013/120] Add project config template --- template_config.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 template_config.yaml diff --git a/template_config.yaml b/template_config.yaml new file mode 100644 index 000000000..d3ed5dd15 --- /dev/null +++ b/template_config.yaml @@ -0,0 +1,9 @@ +project: + name: "project-slug" + modalities: ["DX", "CR"] # DICOM dataset modalities to retain + +tag_operation_files: ["base-tag-operations.yaml"] # DICOM tag anonymisation operations, can specify multiple files + +destination: + dicom: "ftps" # alternatives: "none", "dicomweb", "azure" + parquet: "ftps" # alternatives: "none", "azure" From c24e86c6519ed1576fbba3994330513a6bac3893 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Tue, 20 Feb 2024 12:50:05 +0000 Subject: [PATCH 014/120] Add documentation for Parquet files and export process (#280) * Add documentation for Parquet files and export process * Formatting * Move `TODO` to issue #306 * Remove PR references * Formatting * Move specific details to `pixl_core` docs and add links * Update directory structure on the FTPS server * Formatting * Rename docs/data -> docs/file_types * Link to `file_types` documentation * Add directory structures to docstrings * Update upload.py Co-authored-by: Stef Piatek * Fix docs link Co-authored-by: Jeremy Stein * Clarify that the radiology reports go through Cogstack Co-authored-by: Jeremy Stein * Add note about test files Co-authored-by: Jeremy Stein --------- Co-authored-by: Stef Piatek Co-authored-by: Jeremy Stein --- README.md | 10 +++++-- docs/file_types/parquet_files.md | 46 ++++++++++++++++++++++++++++++++ docs/services/ftp-server.md | 8 +++--- pixl_core/README.md | 43 ++++++++++++++++++++++------- pixl_core/src/core/exports.py | 12 +++++++++ pixl_core/src/core/upload.py | 17 +++++++++++- 6 files changed, 119 insertions(+), 17 deletions(-) create mode 100644 docs/file_types/parquet_files.md diff --git a/README.md b/README.md index 0b1f8d5d1..2b4573e9d 100644 --- a/README.md +++ b/README.md @@ -249,12 +249,14 @@ PIXL data extracts include the below assumptions - (MRN, Accession number) is unique identifier for a report/DICOM study pair - Patients have a single _relevant_ MRN - - ## File journey overview + Files that are present at each step of the pipeline. +A more detailed description of the relevant file types is available in [`docs/file_types/parquet_files.md`](./docs/file_types/parquet_files.md). + ### Resources in source repo (for test only) + ``` test/resources/omop/public /*.parquet ....................private/*.parquet @@ -262,7 +264,9 @@ test/resources/omop/public /*.parquet ``` ### OMOP ES extract dir (input to PIXL) + EXTRACT_DIR is the directory passed to `pixl populate` + ``` EXTRACT_DIR/public /*.parquet ............private/*.parquet @@ -270,12 +274,14 @@ EXTRACT_DIR/public /*.parquet ``` ### PIXL Export dir (PIXL intermediate) + ``` EXPORT_ROOT/PROJECT_SLUG/all_extracts/EXTRACT_DATETIME/radiology/radiology.parquet ....................................................../omop/public/*.parquet ``` ### FTP server + ``` FTPROOT/PROJECT_SLUG/EXTRACT_DATETIME/parquet/radiology/radiology.parquet ..............................................omop/public/*.parquet diff --git a/docs/file_types/parquet_files.md b/docs/file_types/parquet_files.md new file mode 100644 index 000000000..0502202fe --- /dev/null +++ b/docs/file_types/parquet_files.md @@ -0,0 +1,46 @@ +# Parquet files you might encounter throughout PIXL + +## OMOP-ES files + +From +[OMOP-ES](https://github.com/UCLH-Foundry/the-rolling-skeleton/blob/main/docs/design/100-day-design.md#data-flow-through-components) +we receive parquet files defining the data we need to export. These input files appear as 2 groups: + +1. **Public** parquet files: have had identifiers removed and replaced with a sequential ID for the + export +2. **Private** parquet files: map sequential identifiers to patient identifiers (e.g. MRNs, + Accession numbers, NHS numbers) + +## Radiology reports + +The PIXL pipeline generates **Radiology** parquet files, which +contain the radiology reports for the given extract. These are generated by calling the CogStack API, which returns a de-identified radiology report given a full radiology report. + +The functionality for this is defined in the [EHR API](../../pixl_ehr/README.md), specifically in +[`PIXLDatabase.get_radiology_reports`](../../pixl_ehr/src/pixl_ehr/_databases.py), which queries the +PIXL database for the de-identified radiology reports of the current extract and collects them +in a single _parquet_ file together with the `image_identifier` and `procedure_occurrence_id`. + +## Exporting (copying from OMOP ES) + +As part of the PIXL pipeline, we copy the OMOP-ES public _parquet_ files to an export directory, to +prepare them for upload to the DSH. The exporting details are in the +[`pixl_core` documentation](../../pixl_core/README.md#omop-es-files). + +## Uploading to the DSH + +The final step in the journey of the _parquet_ files is to upload them to the DSH. This is +implemented and documented in [`pixl_core`](../../pixl_core/README.md#uploading-to-an-ftps-server). + +## Testing + +Various _parquet_ files are provided throughout the repo to enable unit and system testing: + +- `cli/tests/resources/omop/` contains public and private parquet files together with an + `extract_summary.json` file to mimic the input received from OMOP-ES for the unit tests. (This directory is identical to that below and should be deleted at some point). +- `test/resources/omop/` contains public and private parquet files together with an + `extract_summary.json` file to mimic the input received from OMOP-ES for the system tests + +During the system test, a `radiology.parquet` file is generated and temporarily stored in +`exports/test-extract-uclh-omop-cdm/latest/radiology/radiology.parquet` to check the successful +de-identification before the DSH upload. This file is then deleted after the test. diff --git a/docs/services/ftp-server.md b/docs/services/ftp-server.md index 43258c32e..7564f281b 100644 --- a/docs/services/ftp-server.md +++ b/docs/services/ftp-server.md @@ -7,10 +7,10 @@ The [`core.upload`](../../pixl_core/src/core/upload.py) module implements functi DICOM tags and parquet files to an **FTPS server**. This requires the following environment variables to be set: -- `FTP_HOST`: URL to the FTPS server -- `FTP_PORT`: port on which the FTPS server is listening -- `FTP_USER_NAME`: name of user with access to the FTPS server -- `FTP_USER_PASSWORD`: password for the authorised user +- `FTP_HOST`: URL to the FTPS server +- `FTP_PORT`: port on which the FTPS server is listening +- `FTP_USER_NAME`: name of user with access to the FTPS server +- `FTP_USER_PASSWORD`: password for the authorised user We provide mock values for these for the unit tests (see [`./tests/conftest.py`](./tests/conftest.py)). When running in production, these should be defined diff --git a/pixl_core/README.md b/pixl_core/README.md index 417baaf07..cf669fe87 100644 --- a/pixl_core/README.md +++ b/pixl_core/README.md @@ -6,13 +6,13 @@ upstream services. Specifically, it defines: -- The [Token buffer](#token-buffer) for rate limiting requests to the upstream services -- The [RabbitMQ queue](#patient-queue) implementation shared by the EHR and Imaging APIs -- The PIXL `postgres` internal database for storing exported images and extracts from the messages +- The [Token buffer](#token-buffer) for rate limiting requests to the upstream services +- The [RabbitMQ queue](#patient-queue) implementation shared by the EHR and Imaging APIs +- The PIXL `postgres` internal database for storing exported images and extracts from the messages processed by the CLI driver -- The [`ParquetExport`](./src/core/exports.py) class for exporting OMOP and EMAP extracts to +- The [`ParquetExport`](./src/core/exports.py) class for exporting OMOP and EMAP extracts to parquet files -- Handling of [uploads over FTPS](./src/core/upload.py), used to transfer images and parquet files +- Handling of [uploads over FTPS](./src/core/upload.py), used to transfer images and parquet files to the DSH (Data Safe Haven) ## Installation @@ -90,14 +90,37 @@ for convenience `latest` is a symlink to the most recent extract. ## Uploading to an FTPS server -The `core.upload` module implements functionality to upload DICOM tags and parquet files to an +The `core.upload` module implements functionality to upload DICOM images and parquet files to an **FTPS server**. This requires the following environment variables to be set: -- `FTP_HOST`: URL to the FTPS server -- `FTP_PORT`: port on which the FTPS server is listening -- `FTP_USER_NAME`: name of user with access to the FTPS server -- `FTP_USER_PASSWORD`: password for the authorised user +- `FTP_HOST`: URL to the FTPS server +- `FTP_PORT`: port on which the FTPS server is listening +- `FTP_USER_NAME`: name of user with access to the FTPS server +- `FTP_USER_PASSWORD`: password for the authorised user We provide mock values for these for the unit tests (see [`./tests/conftest.py`](./tests/conftest.py)). When running in production, these should be defined in the `.env` file (see [the example](../.env.sample)). + +When an extract is ready to be published to the DSH, the PIXL pipeline will upload the **Public** +and **Radiology** [_parquet_ files](../docs/data/parquet_files.md) to the `` directory +where the DICOM datasets are stored (see the directory structure below). The uploading is controlled +by `upload_parquet_files` in [`upload.py`](./src/core/upload.py) which takes a `ParquetExport` +object as input to define where the _parquet_ files are located. `upload_parquet_files` is called +by the `export-patient-data` API endpoint defined in the +[EHR API](../pixl_ehr/src/pixl_ehr/main.py), which in turn is called by the `extract_radiology_reports` command in the [PIXL CLI](../cli/README.md). + +Once the parquet files have been uploaded to the DSH, the directory structure will look like this: + +```sh + + ├── + │   └── parquet + │   ├── omop + │   │   └── public + │   │   └── PROCEDURE_OCCURRENCE.parquet + │   └── radiology + │   └── radiology.parquet + ├── .zip + └── .zip +``` diff --git a/pixl_core/src/core/exports.py b/pixl_core/src/core/exports.py index b9a013d2d..20bcaf0b4 100644 --- a/pixl_core/src/core/exports.py +++ b/pixl_core/src/core/exports.py @@ -65,6 +65,18 @@ def copy_to_exports(self, input_omop_dir: pathlib.Path) -> str: :param input_omop_dir: parent path for input omop data, with a "public" subdirectory :raises FileNotFoundError: if there is no public subdirectory in `omop_dir` :returns str: the project slug, so this can be registered for export to the DSH + + The final directory structure will look like this: + exports + └── + ├── all_extracts + │ └── + │ ├── omop + │ │ └── public + │ │ └── PROCEDURE_OCCURRENCE.parquet + │ └── radiology + │ └── radiology.parquet + └── latest -> """ public_input = input_omop_dir / "public" diff --git a/pixl_core/src/core/upload.py b/pixl_core/src/core/upload.py index db5c08e4b..34e4e1333 100644 --- a/pixl_core/src/core/upload.py +++ b/pixl_core/src/core/upload.py @@ -90,7 +90,22 @@ def upload_dicom_image(zip_content: BinaryIO, pseudo_anon_id: str) -> None: def upload_parquet_files(parquet_export: ParquetExport) -> None: - """Upload parquet to FTPS under //parquet.""" + """ + Upload parquet to FTPS under //parquet. + :param parquet_export: instance of the ParquetExport class + The final directory structure will look like this: + + ├── + │ └── parquet + │ ├── omop + │ │ └── public + │ │ └── PROCEDURE_OCCURRENCE.parquet + │ └── radiology + │ └── radiology.parquet + ├── .zip + └── .zip + ... + """ logger.info("Starting FTPS upload of files for '%s'", parquet_export.project_slug) source_root_dir = parquet_export.current_extract_base From 948414df60b4b25d493500e867b9e5ec6f96c63a Mon Sep 17 00:00:00 2001 From: Peter Tsrunchev Date: Thu, 22 Feb 2024 11:25:58 +0100 Subject: [PATCH 015/120] Correct instructions for editable installs (#314) * docs: better editable install instructions * docs: editable install of pytest-pixl --- cli/README.md | 4 ++-- pixl_core/README.md | 2 +- pixl_dcmd/README.md | 2 +- pixl_ehr/README.md | 4 ++-- pixl_imaging/README.md | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cli/README.md b/cli/README.md index b2c8d7160..3e6d5b57a 100644 --- a/cli/README.md +++ b/cli/README.md @@ -25,7 +25,7 @@ management tool such as [conda](https://docs.conda.io/en/latest/) or Then install in editable mode by running ```bash -pip install -e ../pixl_core/ . +pip install -e ../pixl_core/ -e . ``` ## Usage @@ -108,7 +108,7 @@ Commands: Install locally in editable mode with the development and testing dependencies by running ```bash -pip install -e ../pixl_core/ .[test] +pip install -e ../pixl_core/ -e .[test] ``` ### Running tests diff --git a/pixl_core/README.md b/pixl_core/README.md index cf669fe87..fbc68471b 100644 --- a/pixl_core/README.md +++ b/pixl_core/README.md @@ -24,7 +24,7 @@ pip install -e . ## Testing ```bash -pip install -e .[test] && pip install ../pytest-pixl +pip install -e .[test] && pip install -e ../pytest-pixl pytest ``` diff --git a/pixl_dcmd/README.md b/pixl_dcmd/README.md index 266f5a51a..1f4c7e534 100644 --- a/pixl_dcmd/README.md +++ b/pixl_dcmd/README.md @@ -20,7 +20,7 @@ Specifically, the `pixl_dcmd` package provides the following functionality: Install the Python dependencies with ```bash -pip install -e ../pixl_core/ .[test,dev] +pip install -e ../pixl_core/ -e .[test,dev] ``` ## Test diff --git a/pixl_ehr/README.md b/pixl_ehr/README.md index 8f9eda259..8eb65c4a4 100644 --- a/pixl_ehr/README.md +++ b/pixl_ehr/README.md @@ -27,13 +27,13 @@ On Windows, follow [these instructions](https://www.postgresqltutorial.com/postg Then install the Python dependencies with ```bash -pip install -e ../pixl_core/ . +pip install -e ../pixl_core/ -e . ``` ## Test ```bash -pip install -e ../pixl_core/ .[test] +pip install -e ../pixl_core/ -e .[test] pytest -m "not processing" ``` diff --git a/pixl_imaging/README.md b/pixl_imaging/README.md index 9610b97c3..989262303 100644 --- a/pixl_imaging/README.md +++ b/pixl_imaging/README.md @@ -12,7 +12,7 @@ for the requested imaging study, if it didn't already exist. ## Installation ```bash -pip install -e ../pixl_core/ . +pip install -e ../pixl_core/ -e . ``` ## Test From f0fb25db9030130e7649cdf79cad7b05f51312b1 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Thu, 22 Feb 2024 15:14:45 +0000 Subject: [PATCH 016/120] Add copyright header to config template --- template_config.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/template_config.yaml b/template_config.yaml index d3ed5dd15..752c4a749 100644 --- a/template_config.yaml +++ b/template_config.yaml @@ -1,3 +1,17 @@ +# Copyright (c) University College London Hospitals NHS Foundation Trust +# +# 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 +# +# http://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. + project: name: "project-slug" modalities: ["DX", "CR"] # DICOM dataset modalities to retain From 40d7c6bd9248aece3b8c37406a2e2929dc0a2a47 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Thu, 22 Feb 2024 22:54:14 +0000 Subject: [PATCH 017/120] Add secret fetching from Azure keyvault --- pixl_core/src/core/_secrets.py | 48 ++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 pixl_core/src/core/_secrets.py diff --git a/pixl_core/src/core/_secrets.py b/pixl_core/src/core/_secrets.py new file mode 100644 index 000000000..d5db340b4 --- /dev/null +++ b/pixl_core/src/core/_secrets.py @@ -0,0 +1,48 @@ +# Copyright (c) 2022 University College London Hospitals NHS Foundation Trust +# +# 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 +# +# http://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. + +"""Handles fetching of project secrets from the Azure Keyvault""" + +from azure.identity import DefaultAzureCredential +from azure.keyvault.secrets import SecretClient +from decouple import config + +AZURE_KEY_VAULT_NAME = config("EXPORT_AZ_KEY_VAULT_NAME") + + +def _fetch_secret(secret_name: str) -> str: + """ + Fetch a secret from the Azure Key Vault instance specified in the environment variables. + Creates an EnvironmentCredential via AzureDefaultCredential to connect with a + ServicePrincipal and secret configured via environment variables. + + This requires the following environment variables to be set, which will be picked up by the + Azure SDK: + - AZURE_CLIENT_ID + - AZURE_CLIENT_SECRET + - AZURE_TENANT_ID + - AZURE_KEY_VAULT_NAME + + :return: the requested secret's value + """ + key_vault_uri = f"https://{AZURE_KEY_VAULT_NAME}.vault.azure.net" + credentials = DefaultAzureCredential() + client = SecretClient(vault_url=key_vault_uri, credential=credentials) + + secret = client.get_secret(secret_name).value + if secret is None: + msg = "Azure Key Vault secret is None" + raise ValueError(msg) + + return str(secret) From 60e95a9c3c93ff381707e7e0c25dd9593bf309bf Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Thu, 22 Feb 2024 22:54:53 +0000 Subject: [PATCH 018/120] Ignore local secrets --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index ed0409bdb..ea9a25403 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ .env local.env pixl_config.yml +.secrets.env # Byte-compiled / optimized / DLL files __pycache__/ From e1aadb3cb29710ee7aa8f4454dc7d99f61a6785a Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Thu, 22 Feb 2024 22:55:09 +0000 Subject: [PATCH 019/120] Add Azure dependenices --- pixl_core/pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pixl_core/pyproject.toml b/pixl_core/pyproject.toml index abde7af26..906b48e3b 100644 --- a/pixl_core/pyproject.toml +++ b/pixl_core/pyproject.toml @@ -7,6 +7,8 @@ readme = "README.md" requires-python = ">=3.9" classifiers = ["Programming Language :: Python :: 3"] dependencies = [ + "azure-identity==1.12.0", + "azure-keyvault==4.2.0", "fastapi==0.109.1", "token-bucket==0.3.0", "python-decouple==3.6", From 1442354a9481fb11e875d22044d185c2d7221a0c Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Thu, 22 Feb 2024 23:03:42 +0000 Subject: [PATCH 020/120] Refactor: move FTPS setup to separet module --- pixl_core/src/core/_upload_ftps.py | 99 ++++++++++++++++++++++++++++++ pixl_core/src/core/upload.py | 81 ++---------------------- 2 files changed, 105 insertions(+), 75 deletions(-) create mode 100644 pixl_core/src/core/_upload_ftps.py diff --git a/pixl_core/src/core/_upload_ftps.py b/pixl_core/src/core/_upload_ftps.py new file mode 100644 index 000000000..f58d3f7bd --- /dev/null +++ b/pixl_core/src/core/_upload_ftps.py @@ -0,0 +1,99 @@ +# Copyright (c) University College London Hospitals NHS Foundation Trust +# +# 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 +# +# http://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. + +"""Helper functions for setting up a connection to an FTPS server.""" + +from __future__ import annotations + +import ftplib +import logging +import ssl +from ftplib import FTP_TLS +from typing import TYPE_CHECKING, Any + +from decouple import config + +if TYPE_CHECKING: + from pathlib import Path + from socket import socket + +logger = logging.getLogger(__name__) + + +class ImplicitFtpTls(ftplib.FTP_TLS): + """ + FTP_TLS subclass that automatically wraps sockets in SSL to support implicit FTPS. + + https://stackoverflow.com/questions/12164470/python-ftp-implicit-tls-connection-issue + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Create instance from parent class.""" + super().__init__(*args, **kwargs) + self._sock: socket | None = None + + @property + def sock(self) -> socket | None: + """Return the socket.""" + return self._sock + + @sock.setter + def sock(self, value: socket) -> None: + """When modifying the socket, ensure that it is ssl wrapped.""" + if value is not None and not isinstance(value, ssl.SSLSocket): + value = self.context.wrap_socket(value) + self._sock = value + + +def _connect_to_ftp() -> FTP_TLS: + # Set your FTP server details + ftp_host = config("FTP_HOST") + ftp_port = config("FTP_PORT") # FTPS usually uses port 21 + ftp_user = config("FTP_USER_NAME") + ftp_password = config("FTP_USER_PASSWORD") + + # Connect to the server and login + try: + ftp = ImplicitFtpTls() + ftp.connect(ftp_host, int(ftp_port)) + ftp.login(ftp_user, ftp_password) + ftp.prot_p() + except ftplib.all_errors as ftp_error: + error_msg = "Failed to connect to FTPS server: '%s'" + raise ConnectionError(error_msg, ftp_error) from ftp_error + return ftp + + +def _create_and_set_as_cwd_multi_path(ftp: FTP_TLS, remote_multi_dir: Path) -> None: + """Create (and cwd into) a multi dir path, analogously to mkdir -p""" + if remote_multi_dir.is_absolute(): + # would require some special handling and we don't need it + err = "must be relative path" + raise ValueError(err) + logger.info("_create_and_set_as_cwd_multi_path %s", remote_multi_dir) + # path should be pretty normalised, so assume split is safe + sub_dirs = str(remote_multi_dir).split("/") + for sd in sub_dirs: + _create_and_set_as_cwd(ftp, sd) + + +def _create_and_set_as_cwd(ftp: FTP_TLS, project_dir: str) -> None: + try: + ftp.cwd(project_dir) + logger.debug("'%s' exists on remote ftp, so moving into it", project_dir) + except ftplib.error_perm: + logger.info("creating '%s' on remote ftp and moving into it", project_dir) + # Directory doesn't exist, so create it + ftp.mkd(project_dir) + ftp.cwd(project_dir) diff --git a/pixl_core/src/core/upload.py b/pixl_core/src/core/upload.py index 34e4e1333..9cf70a441 100644 --- a/pixl_core/src/core/upload.py +++ b/pixl_core/src/core/upload.py @@ -17,50 +17,24 @@ import ftplib import logging -import ssl from datetime import datetime, timezone -from ftplib import FTP_TLS from pathlib import Path -from typing import TYPE_CHECKING, Any, BinaryIO - -from decouple import config +from typing import TYPE_CHECKING, BinaryIO if TYPE_CHECKING: - from socket import socket - from core.exports import ParquetExport +from core._upload_ftps import ( + _connect_to_ftp, + _create_and_set_as_cwd, + _create_and_set_as_cwd_multi_path, +) from core.db.queries import get_project_slug_from_hashid, update_exported_at logger = logging.getLogger(__name__) -class ImplicitFtpTls(ftplib.FTP_TLS): - """ - FTP_TLS subclass that automatically wraps sockets in SSL to support implicit FTPS. - - https://stackoverflow.com/questions/12164470/python-ftp-implicit-tls-connection-issue - """ - - def __init__(self, *args: Any, **kwargs: Any) -> None: - """Create instance from parent class.""" - super().__init__(*args, **kwargs) - self._sock: socket | None = None - - @property - def sock(self) -> socket | None: - """Return the socket.""" - return self._sock - - @sock.setter - def sock(self, value: socket) -> None: - """When modifying the socket, ensure that it is ssl wrapped.""" - if value is not None and not isinstance(value, ssl.SSLSocket): - value = self.context.wrap_socket(value) - self._sock = value - - def upload_dicom_image(zip_content: BinaryIO, pseudo_anon_id: str) -> None: """Top level way to upload an image.""" logger.info("Starting FTPS upload of '%s'", pseudo_anon_id) @@ -143,46 +117,3 @@ def upload_parquet_files(parquet_export: ParquetExport) -> None: # Close the FTP connection ftp.quit() logger.info("Finished FTPS upload of files for '%s'", parquet_export.project_slug) - - -def _connect_to_ftp() -> FTP_TLS: - # Set your FTP server details - ftp_host = config("FTP_HOST") - ftp_port = config("FTP_PORT") # FTPS usually uses port 21 - ftp_user = config("FTP_USER_NAME") - ftp_password = config("FTP_USER_PASSWORD") - - # Connect to the server and login - try: - ftp = ImplicitFtpTls() - ftp.connect(ftp_host, int(ftp_port)) - ftp.login(ftp_user, ftp_password) - ftp.prot_p() - except ftplib.all_errors as ftp_error: - error_msg = "Failed to connect to FTPS server: '%s'" - raise ConnectionError(error_msg, ftp_error) from ftp_error - return ftp - - -def _create_and_set_as_cwd_multi_path(ftp: FTP_TLS, remote_multi_dir: Path) -> None: - """Create (and cwd into) a multi dir path, analogously to mkdir -p""" - if remote_multi_dir.is_absolute(): - # would require some special handling and we don't need it - err = "must be relative path" - raise ValueError(err) - logger.info("_create_and_set_as_cwd_multi_path %s", remote_multi_dir) - # path should be pretty normalised, so assume split is safe - sub_dirs = str(remote_multi_dir).split("/") - for sd in sub_dirs: - _create_and_set_as_cwd(ftp, sd) - - -def _create_and_set_as_cwd(ftp: FTP_TLS, project_dir: str) -> None: - try: - ftp.cwd(project_dir) - logger.debug("'%s' exists on remote ftp, so moving into it", project_dir) - except ftplib.error_perm: - logger.info("creating '%s' on remote ftp and moving into it", project_dir) - # Directory doesn't exist, so create it - ftp.mkd(project_dir) - ftp.cwd(project_dir) From 6dba81ed3b73ac571684b7f5713ef250fb545a18 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Fri, 23 Feb 2024 02:13:38 +0000 Subject: [PATCH 021/120] Create AzureKeyVault class to handle project secrets fetching --- pixl_core/src/core/_secrets.py | 75 ++++++++++++++++++++++++---------- 1 file changed, 53 insertions(+), 22 deletions(-) diff --git a/pixl_core/src/core/_secrets.py b/pixl_core/src/core/_secrets.py index d5db340b4..f67dd8cd6 100644 --- a/pixl_core/src/core/_secrets.py +++ b/pixl_core/src/core/_secrets.py @@ -14,35 +14,66 @@ """Handles fetching of project secrets from the Azure Keyvault""" +import subprocess + from azure.identity import DefaultAzureCredential from azure.keyvault.secrets import SecretClient from decouple import config -AZURE_KEY_VAULT_NAME = config("EXPORT_AZ_KEY_VAULT_NAME") +class AzureKeyVault: + """Handles fetching of project secrets from the Azure Keyvault""" + + def __init__(self) -> None: + """ + Initialise the AzureKeyVault instance + + Creates an EnvironmentCredential via AzureDefaultCredential to connect with a + ServicePrincipal and secret configured via environment variables. + + This requires the following environment variables to be set, which will be picked up by the + Azure SDK: + - AZURE_CLIENT_ID + - AZURE_CLIENT_SECRET + - AZURE_TENANT_ID + - AZURE_KEY_VAULT_NAME + """ + self._check_envvars() + self.kv_name = config("EXPORT_AZ_KEY_VAULT_NAME") + self.client = self._connect_to_keyvault() + + def _connect_to_keyvault(self) -> SecretClient: + key_vault_uri = f"https://{self.kv_name}.vault.azure.net" + + credentials = DefaultAzureCredential() + return SecretClient(vault_url=key_vault_uri, credential=credentials) -def _fetch_secret(secret_name: str) -> str: - """ - Fetch a secret from the Azure Key Vault instance specified in the environment variables. - Creates an EnvironmentCredential via AzureDefaultCredential to connect with a - ServicePrincipal and secret configured via environment variables. + def _check_envvars(self) -> None: + """ + Check if the required environment variables are set. + These need to be set system-wide, as the Azure SDK picks them up from the environment. + :raises OSError: if any of the environment variables are not set + """ + _check_system_envvar("AZURE_CLIENT_ID") + _check_system_envvar("AZURE_CLIENT_SECRET") + _check_system_envvar("AZURE_TENANT_ID") + _check_system_envvar("AZURE_KEY_VAULT_NAME") - This requires the following environment variables to be set, which will be picked up by the - Azure SDK: - - AZURE_CLIENT_ID - - AZURE_CLIENT_SECRET - - AZURE_TENANT_ID - - AZURE_KEY_VAULT_NAME + def fetch_secret(self, secret_name: str) -> str: + """ + Fetch a secret from the Azure Key Vault instance specified in the environment variables. + :return: the requested secret's value + """ + secret = self.client.get_secret(secret_name).value + if secret is None: + msg = "Azure Key Vault secret is None" + raise ValueError(msg) - :return: the requested secret's value - """ - key_vault_uri = f"https://{AZURE_KEY_VAULT_NAME}.vault.azure.net" - credentials = DefaultAzureCredential() - client = SecretClient(vault_url=key_vault_uri, credential=credentials) + return str(secret) - secret = client.get_secret(secret_name).value - if secret is None: - msg = "Azure Key Vault secret is None" - raise ValueError(msg) - return str(secret) +def _check_system_envvar(var_name: str) -> None: + """Check if an environment variable is set system-wide""" + error_msg = f"Environment variable {var_name} not set" + if not subprocess.check_output(f"echo ${var_name}", shell=True).decode().strip(): # noqa: S602 + raise OSError(error_msg) From c68eaf909288b86a678d41c94cd7c0914af10e26 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Fri, 23 Feb 2024 02:14:36 +0000 Subject: [PATCH 022/120] Refactor `_connect_to_ftp` to take FTP settings as parameters --- pixl_core/src/core/_upload_ftps.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/pixl_core/src/core/_upload_ftps.py b/pixl_core/src/core/_upload_ftps.py index f58d3f7bd..37426eab2 100644 --- a/pixl_core/src/core/_upload_ftps.py +++ b/pixl_core/src/core/_upload_ftps.py @@ -22,8 +22,6 @@ from ftplib import FTP_TLS from typing import TYPE_CHECKING, Any -from decouple import config - if TYPE_CHECKING: from pathlib import Path from socket import socket @@ -56,13 +54,7 @@ def sock(self, value: socket) -> None: self._sock = value -def _connect_to_ftp() -> FTP_TLS: - # Set your FTP server details - ftp_host = config("FTP_HOST") - ftp_port = config("FTP_PORT") # FTPS usually uses port 21 - ftp_user = config("FTP_USER_NAME") - ftp_password = config("FTP_USER_PASSWORD") - +def _connect_to_ftp(ftp_host: str, ftp_port: int, ftp_user: str, ftp_password: str) -> FTP_TLS: # Connect to the server and login try: ftp = ImplicitFtpTls() From 5dad1bebb955d0fe9cd83532b576228961222d23 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Fri, 23 Feb 2024 02:18:36 +0000 Subject: [PATCH 023/120] Add abstract `Uploader` and `FTPSUploader` classes to define uploading interface --- pixl_core/src/core/upload.py | 193 +++++++++++++++++++++-------------- 1 file changed, 114 insertions(+), 79 deletions(-) diff --git a/pixl_core/src/core/upload.py b/pixl_core/src/core/upload.py index 9cf70a441..6c23f63ba 100644 --- a/pixl_core/src/core/upload.py +++ b/pixl_core/src/core/upload.py @@ -11,20 +11,24 @@ # 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. -"""Functionality to upload files to an endpoint.""" +"""Functionality to upload files to a remote server.""" from __future__ import annotations import ftplib import logging +from abc import ABC, abstractmethod from datetime import datetime, timezone from pathlib import Path from typing import TYPE_CHECKING, BinaryIO +from decouple import config + if TYPE_CHECKING: from core.exports import ParquetExport +from core._secrets import AzureKeyVault from core._upload_ftps import ( _connect_to_ftp, _create_and_set_as_cwd, @@ -35,85 +39,116 @@ logger = logging.getLogger(__name__) -def upload_dicom_image(zip_content: BinaryIO, pseudo_anon_id: str) -> None: - """Top level way to upload an image.""" - logger.info("Starting FTPS upload of '%s'", pseudo_anon_id) +class Uploader(ABC): + """Upload strategy interface for DICOM datasets.""" + + @abstractmethod + def __init__(self, project: str) -> None: + """ + Initialise the uploader for a specific project with the destination configuration and an + AzureKeyvault instance. The keyvault is used to fetch the secrets required to connect to + the remote destination. + + :param project: The project name for which the uploader is being initialised. Used to fetch + the correct secrets from the keyvault. + """ + self.project = project + self.keyvault = AzureKeyVault() + + +class FTPSUploader(Uploader): + """Upload strategy for an FTPS server.""" + + def __init__(self, project: str) -> None: + """Initialise the uploader with the destination configuration.""" + Uploader.__init__(self, project) + self._set_config() - # rename destination to {project-slug}/{study-pseduonymised-id}.zip - remote_directory = get_project_slug_from_hashid(pseudo_anon_id) + def _set_config(self) -> None: + self.host = self.keyvault.fetch_secret(f"{self.project}--ftp--host") + self.user = self.keyvault.fetch_secret(f"{self.project}--ftp--username") + self.password = self.keyvault.fetch_secret(f"{self.project}--ftp--password") + self.port = config("FTP_PORT", default=21, cast=int) - # Create the remote directory if it doesn't exist - ftp = _connect_to_ftp() - _create_and_set_as_cwd(ftp, remote_directory) - command = f"STOR {pseudo_anon_id}.zip" - logger.debug("Running %s", command) + def upload_dicom_image(self, zip_content: BinaryIO, pseudo_anon_id: str) -> None: + """Upload a DICOM image to the FTPS server.""" + logger.info("Starting FTPS upload of '%s'", pseudo_anon_id) + + # rename destination to {project-slug}/{study-pseduonymised-id}.zip + remote_directory = get_project_slug_from_hashid(pseudo_anon_id) + + # Create the remote directory if it doesn't exist + ftp = _connect_to_ftp(self.host, self.port, self.user, self.password) + _create_and_set_as_cwd(ftp, remote_directory) + command = f"STOR {pseudo_anon_id}.zip" + logger.debug("Running %s", command) + + # Store the file using a binary handler + try: + ftp.storbinary(command, zip_content) + except ftplib.all_errors as ftp_error: + ftp.quit() + error_msg = "Failed to run STOR command '%s': '%s'" + raise ConnectionError(error_msg, command, ftp_error) from ftp_error + + # Close the FTP connection + ftp.quit() - # Store the file using a binary handler - try: - ftp.storbinary(command, zip_content) - except ftplib.all_errors as ftp_error: + # Update the exported_at timestamp in the PIXL database + update_exported_at(pseudo_anon_id, datetime.now(tz=timezone.utc)) + logger.info("Finished FTPS upload of '%s'", pseudo_anon_id) + + def upload_parquet_files(self, parquet_export: ParquetExport) -> None: + """ + Upload parquet to FTPS under //parquet. + :param parquet_export: instance of the ParquetExport class + The final directory structure will look like this: + + ├── + │ └── parquet + │ ├── omop + │ │ └── public + │ │ └── PROCEDURE_OCCURRENCE.parquet + │ └── radiology + │ └── radiology.parquet + ├── .zip + └── .zip + ... + """ + logger.info("Starting FTPS upload of files for '%s'", parquet_export.project_slug) + + source_root_dir = parquet_export.current_extract_base + # Create the remote directory if it doesn't exist + ftp = _connect_to_ftp(self.host, self.port, self.user, self.password) + _create_and_set_as_cwd(ftp, parquet_export.project_slug) + _create_and_set_as_cwd(ftp, parquet_export.extract_time_slug) + _create_and_set_as_cwd(ftp, "parquet") + + # get the upload root directory before we do anything as we'll need + # to return to it (will it always be absolute?) + upload_root_dir = Path(ftp.pwd()) + if not upload_root_dir.is_absolute(): + logger.error("server remote path is not absolute, what are we going to do?") + + # absolute paths of the source + source_files = [x for x in source_root_dir.rglob("*.parquet") if x.is_file()] + if not source_files: + msg = f"No files found in {source_root_dir}" + raise FileNotFoundError(msg) + + # throw exception if empty dir + for source_path in source_files: + _create_and_set_as_cwd(ftp, str(upload_root_dir)) + source_rel_path = source_path.relative_to(source_root_dir) + source_rel_dir = source_rel_path.parent + source_filename_only = source_rel_path.relative_to(source_rel_dir) + _create_and_set_as_cwd_multi_path(ftp, source_rel_dir) + with source_path.open("rb") as handle: + command = f"STOR {source_filename_only}" + + # Store the file using a binary handler + ftp.storbinary(command, handle) + + # Close the FTP connection ftp.quit() - error_msg = "Failed to run STOR command '%s': '%s'" - raise ConnectionError(error_msg, command, ftp_error) from ftp_error - - # Close the FTP connection - ftp.quit() - - update_exported_at(pseudo_anon_id, datetime.now(tz=timezone.utc)) - logger.info("Finished FTPS upload of '%s'", pseudo_anon_id) - - -def upload_parquet_files(parquet_export: ParquetExport) -> None: - """ - Upload parquet to FTPS under //parquet. - :param parquet_export: instance of the ParquetExport class - The final directory structure will look like this: - - ├── - │ └── parquet - │ ├── omop - │ │ └── public - │ │ └── PROCEDURE_OCCURRENCE.parquet - │ └── radiology - │ └── radiology.parquet - ├── .zip - └── .zip - ... - """ - logger.info("Starting FTPS upload of files for '%s'", parquet_export.project_slug) - - source_root_dir = parquet_export.current_extract_base - # Create the remote directory if it doesn't exist - ftp = _connect_to_ftp() - _create_and_set_as_cwd(ftp, parquet_export.project_slug) - _create_and_set_as_cwd(ftp, parquet_export.extract_time_slug) - _create_and_set_as_cwd(ftp, "parquet") - - # get the upload root directory before we do anything as we'll need - # to return to it (will it always be absolute?) - upload_root_dir = Path(ftp.pwd()) - if not upload_root_dir.is_absolute(): - logger.error("server remote path is not absolute, what are we going to do?") - - # absolute paths of the source - source_files = [x for x in source_root_dir.rglob("*.parquet") if x.is_file()] - if not source_files: - msg = f"No files found in {source_root_dir}" - raise FileNotFoundError(msg) - - # throw exception if empty dir - for source_path in source_files: - _create_and_set_as_cwd(ftp, str(upload_root_dir)) - source_rel_path = source_path.relative_to(source_root_dir) - source_rel_dir = source_rel_path.parent - source_filename_only = source_rel_path.relative_to(source_rel_dir) - _create_and_set_as_cwd_multi_path(ftp, source_rel_dir) - with source_path.open("rb") as handle: - command = f"STOR {source_filename_only}" - - # Store the file using a binary handler - ftp.storbinary(command, handle) - - # Close the FTP connection - ftp.quit() - logger.info("Finished FTPS upload of files for '%s'", parquet_export.project_slug) + logger.info("Finished FTPS upload of files for '%s'", parquet_export.project_slug) From 58afdbb0034d1fd08a97f11bd9e636e32d169c13 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Fri, 23 Feb 2024 02:20:38 +0000 Subject: [PATCH 024/120] Update upload tests to use `Uploader` interface --- pixl_core/tests/test_upload.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pixl_core/tests/test_upload.py b/pixl_core/tests/test_upload.py index 0e4d5a344..3e89cc93c 100644 --- a/pixl_core/tests/test_upload.py +++ b/pixl_core/tests/test_upload.py @@ -20,7 +20,9 @@ import pytest from core.db.models import Image from core.db.queries import get_project_slug_from_hashid, update_exported_at -from core.upload import upload_dicom_image, upload_parquet_files +from core.upload import FTPSUploader + +FTPS_UPLOADER = FTPSUploader("test") @pytest.mark.usefixtures("ftps_server") @@ -33,7 +35,7 @@ def test_upload_dicom_image(test_zip_content, not_yet_exported_dicom_image, ftps expected_output_file = ftps_home_dir / project_slug / (pseudo_anon_id + ".zip") # ACT - upload_dicom_image(test_zip_content, pseudo_anon_id) + FTPS_UPLOADER.upload_dicom_image(test_zip_content, pseudo_anon_id) # ASSERT assert expected_output_file.exists() @@ -48,7 +50,7 @@ def test_upload_dicom_image_throws(test_zip_content, already_exported_dicom_imag # ASSERT with pytest.raises(RuntimeError, match="Image already exported"): - upload_dicom_image(test_zip_content, pseudo_anon_id) + FTPS_UPLOADER.upload_dicom_image(test_zip_content, pseudo_anon_id) def test_update_exported_and_save(rows_in_session) -> None: @@ -78,7 +80,7 @@ def test_upload_parquet(parquet_export, ftps_home_dir) -> None: parquet_export.export_radiology(pd.DataFrame(list("dummy"), columns=["D"])) # ACT - upload_parquet_files(parquet_export) + FTPS_UPLOADER.upload_parquet_files(parquet_export) # ASSERT expected_public_parquet_dir = ( ftps_home_dir / parquet_export.project_slug / parquet_export.extract_time_slug / "parquet" @@ -99,4 +101,4 @@ def test_no_export_to_upload(parquet_export) -> None: """If there is nothing in the export directly, an exception is thrown""" parquet_export.public_output.mkdir(parents=True, exist_ok=True) with pytest.raises(FileNotFoundError): - upload_parquet_files(parquet_export) + FTPS_UPLOADER.upload_parquet_files(parquet_export) From b01fd16050aa428bc10589edab803b4d9e89f780 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Fri, 23 Feb 2024 22:14:00 +0000 Subject: [PATCH 025/120] Revert back to `pydantic.validator` `field_validator` is only available in `pydantic>-2.0` --- pixl_core/pyproject.toml | 2 +- pixl_core/src/core/project_config.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pixl_core/pyproject.toml b/pixl_core/pyproject.toml index 906b48e3b..a0399ef26 100644 --- a/pixl_core/pyproject.toml +++ b/pixl_core/pyproject.toml @@ -49,4 +49,4 @@ extend = "../ruff.toml" [tool.ruff.pep8-naming] # Allow Pydantic's `@validator` decorator to trigger class method treatment. -classmethod-decorators = ["classmethod", "pydantic.field_validator"] +classmethod-decorators = ["classmethod", "pydantic.validator"] diff --git a/pixl_core/src/core/project_config.py b/pixl_core/src/core/project_config.py index dd84a4448..1fa68c8e2 100644 --- a/pixl_core/src/core/project_config.py +++ b/pixl_core/src/core/project_config.py @@ -22,7 +22,7 @@ import yaml from decouple import config -from pydantic import BaseModel, field_validator +from pydantic import BaseModel, validator PROJECT_CONFIGS_DIR = Path(config("PROJECT_CONFIGS_DIR")) @@ -68,7 +68,7 @@ class _Destination(BaseModel): dicom: _DestinationEnum parquet: _DestinationEnum - @field_validator("parquet") + @validator("parquet") def valid_parquet_destination(cls, v: str) -> str: if v == "dicomweb": msg = "Parquet destination cannot be dicomweb" @@ -83,7 +83,7 @@ class PixlConfig(BaseModel): tag_operation_files: list[Path] destination: _Destination - @field_validator("tag_operation_files", mode="before") + @validator("tag_operation_files") def _valid_tag_operations(cls, tag_ops_files: list[str]) -> list[Path]: if not tag_ops_files or len(tag_ops_files) == 0: msg = "There should be at least 1 tag operations file" From 1bc787130e72688581919005b09733d1fbe94e36 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Fri, 23 Feb 2024 22:17:53 +0000 Subject: [PATCH 026/120] Add `MockFTPSUploader` for tests Opted to mock out the `FTPSUploader` for tests instead of the `AzureKeyVault` to set the configuration fields directly. --- pixl_core/tests/conftest.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/pixl_core/tests/conftest.py b/pixl_core/tests/conftest.py index 2c62e46d1..26ae5275b 100644 --- a/pixl_core/tests/conftest.py +++ b/pixl_core/tests/conftest.py @@ -23,6 +23,7 @@ import pytest from core.db.models import Base, Extract, Image from core.exports import ParquetExport +from core.upload import FTPSUploader from sqlalchemy import Engine, create_engine from sqlalchemy.orm import Session, sessionmaker @@ -38,10 +39,6 @@ os.environ["RABBITMQ_PASSWORD"] = "guest" # noqa: S105 Hardcoding password os.environ["RABBITMQ_HOST"] = "localhost" os.environ["RABBITMQ_PORT"] = "25672" -os.environ["FTP_HOST"] = "localhost" -os.environ["FTP_USER_NAME"] = "pixl" -os.environ["FTP_USER_PASSWORD"] = "longpassword" # noqa: S105 Hardcoding password -os.environ["FTP_PORT"] = "20021" os.environ["PROJECT_CONFIGS_DIR"] = str(TEST_DIR.parents[1] / "project_configs") @@ -71,6 +68,23 @@ def run_containers() -> subprocess.CompletedProcess[bytes]: ) +class MockFTPSUploader(FTPSUploader): + """Mock FTPSUploader for testing.""" + + def __init__(self) -> None: + """Initialise the mock uploader with hardcoded values for FTPS config.""" + self.host = "localhost" + self.user = "pixl" + self.password = "longpassword" # noqa: S105 Hardcoding password + self.port = 20021 + + +@pytest.fixture() +def ftps_uploader() -> MockFTPSUploader: + """Return a MockFTPSUploader object.""" + return MockFTPSUploader() + + @pytest.fixture() def ftps_home_dir(ftps_server) -> Generator[Path, None, None]: """ From f59a11b1532aaf31dee75cc61218b6d289a87cfb Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Fri, 23 Feb 2024 22:18:28 +0000 Subject: [PATCH 027/120] Update tests with `MockFTPSUploader` class --- pixl_core/tests/test_upload.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/pixl_core/tests/test_upload.py b/pixl_core/tests/test_upload.py index 3e89cc93c..59a06e447 100644 --- a/pixl_core/tests/test_upload.py +++ b/pixl_core/tests/test_upload.py @@ -20,13 +20,12 @@ import pytest from core.db.models import Image from core.db.queries import get_project_slug_from_hashid, update_exported_at -from core.upload import FTPSUploader - -FTPS_UPLOADER = FTPSUploader("test") @pytest.mark.usefixtures("ftps_server") -def test_upload_dicom_image(test_zip_content, not_yet_exported_dicom_image, ftps_home_dir) -> None: +def test_upload_dicom_image( + test_zip_content, not_yet_exported_dicom_image, ftps_uploader, ftps_home_dir +) -> None: """Tests that DICOM image can be uploaded to the correct location""" # ARRANGE # Get the pseudo identifier from the test image @@ -35,14 +34,16 @@ def test_upload_dicom_image(test_zip_content, not_yet_exported_dicom_image, ftps expected_output_file = ftps_home_dir / project_slug / (pseudo_anon_id + ".zip") # ACT - FTPS_UPLOADER.upload_dicom_image(test_zip_content, pseudo_anon_id) + ftps_uploader.upload_dicom_image(test_zip_content, pseudo_anon_id) # ASSERT assert expected_output_file.exists() @pytest.mark.usefixtures("ftps_server") -def test_upload_dicom_image_throws(test_zip_content, already_exported_dicom_image) -> None: +def test_upload_dicom_image_throws( + test_zip_content, already_exported_dicom_image, ftps_uploader +) -> None: """Tests that exception thrown if DICOM image already exported""" # ARRANGE # Get the pseudo identifier from the test image @@ -50,7 +51,7 @@ def test_upload_dicom_image_throws(test_zip_content, already_exported_dicom_imag # ASSERT with pytest.raises(RuntimeError, match="Image already exported"): - FTPS_UPLOADER.upload_dicom_image(test_zip_content, pseudo_anon_id) + ftps_uploader.upload_dicom_image(test_zip_content, pseudo_anon_id) def test_update_exported_and_save(rows_in_session) -> None: @@ -70,7 +71,7 @@ def test_update_exported_and_save(rows_in_session) -> None: @pytest.mark.usefixtures("ftps_server") -def test_upload_parquet(parquet_export, ftps_home_dir) -> None: +def test_upload_parquet(parquet_export, ftps_home_dir, ftps_uploader) -> None: """Tests that parquet files are uploaded to the correct location""" # ARRANGE @@ -80,7 +81,7 @@ def test_upload_parquet(parquet_export, ftps_home_dir) -> None: parquet_export.export_radiology(pd.DataFrame(list("dummy"), columns=["D"])) # ACT - FTPS_UPLOADER.upload_parquet_files(parquet_export) + ftps_uploader.upload_parquet_files(parquet_export) # ASSERT expected_public_parquet_dir = ( ftps_home_dir / parquet_export.project_slug / parquet_export.extract_time_slug / "parquet" @@ -97,8 +98,8 @@ def test_upload_parquet(parquet_export, ftps_home_dir) -> None: @pytest.mark.usefixtures("ftps_server") -def test_no_export_to_upload(parquet_export) -> None: +def test_no_export_to_upload(parquet_export, ftps_uploader) -> None: """If there is nothing in the export directly, an exception is thrown""" parquet_export.public_output.mkdir(parents=True, exist_ok=True) with pytest.raises(FileNotFoundError): - FTPS_UPLOADER.upload_parquet_files(parquet_export) + ftps_uploader.upload_parquet_files(parquet_export) From 331d600a7be01ceb9ad511dffb7591d7498e364b Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Fri, 23 Feb 2024 22:19:46 +0000 Subject: [PATCH 028/120] Partially added docs to describe project configuration --- README.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/README.md b/README.md index 2b4573e9d..884b6ea1e 100644 --- a/README.md +++ b/README.md @@ -188,6 +188,38 @@ For the deidentification of the EHR extracts, we rely on an instance running the [CogStack API](https://cogstack.org/) with a `/redact` endpoint. The URL of this instance should be set in `.env` as `COGSTACK_REDACT_URL`. +### 3. Configure a new project + + +- Copy the `template_config.yaml` file to a new file in the `projects/config` directory +- The project slug should match the project name in the `extract_summary.json` log file +- The filename of the project config should be ``.yaml +- Project config should be created in `/projects/config/` **in a new _git_ branch** +- Open PR in [PIXL](https://github.com/UCLH-Foundry/PIXL) to merge the new project config into `main` + +#### The config YAML file + +The configuration file defines: + +- Project name: the `` name of the Project +- The DICOM dataset modalities to retain (e.g. `["DX", "CR"]` for X-Ray studies) +- The anonymisation operations to be applied to the DICOM tags, by providing a file path to one or multiple YAML files +- The endpoints used to upload the anonymised DICOM data and the public and radiology + [parquet files](./docs/file_types/parquet_files.md). We currently support the following endpoints: + + - `"none"`: no upload + - `"ftps"`: a secure FTP server (for both _DICOM_ and _parquet_ files) + Requires the `FTPS_*` environment variables to be set in `.env` + - `"azure"`: a secure Azure Blob Storage account (for both _DICOM_ and _parquet_ files) + Requires the `AZURE_*` environment variables to be set in `.env` + - `"dicomweb"`: a DICOMweb server (for _DICOM_ files only) + Requires the `DICOMWEB_*` environment variables to be set in `.env` + +#### Set up secrets in Azure keyvault + + + + ## Run ### Start From 697840f478cdbbc8c6caf2c69bba3fdbaca3f966 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Mon, 26 Feb 2024 10:54:20 +0100 Subject: [PATCH 029/120] Small code reordering --- pixl_core/src/core/_secrets.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/pixl_core/src/core/_secrets.py b/pixl_core/src/core/_secrets.py index f67dd8cd6..38e38bf5c 100644 --- a/pixl_core/src/core/_secrets.py +++ b/pixl_core/src/core/_secrets.py @@ -48,17 +48,6 @@ def _connect_to_keyvault(self) -> SecretClient: credentials = DefaultAzureCredential() return SecretClient(vault_url=key_vault_uri, credential=credentials) - def _check_envvars(self) -> None: - """ - Check if the required environment variables are set. - These need to be set system-wide, as the Azure SDK picks them up from the environment. - :raises OSError: if any of the environment variables are not set - """ - _check_system_envvar("AZURE_CLIENT_ID") - _check_system_envvar("AZURE_CLIENT_SECRET") - _check_system_envvar("AZURE_TENANT_ID") - _check_system_envvar("AZURE_KEY_VAULT_NAME") - def fetch_secret(self, secret_name: str) -> str: """ Fetch a secret from the Azure Key Vault instance specified in the environment variables. @@ -71,6 +60,17 @@ def fetch_secret(self, secret_name: str) -> str: return str(secret) + def _check_envvars(self) -> None: + """ + Check if the required environment variables are set. + These need to be set system-wide, as the Azure SDK picks them up from the environment. + :raises OSError: if any of the environment variables are not set + """ + _check_system_envvar("AZURE_CLIENT_ID") + _check_system_envvar("AZURE_CLIENT_SECRET") + _check_system_envvar("AZURE_TENANT_ID") + _check_system_envvar("AZURE_KEY_VAULT_NAME") + def _check_system_envvar(var_name: str) -> None: """Check if an environment variable is set system-wide""" From 6564127d575f2a5bf8400959ecb0087f7d0d33d4 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Mon, 26 Feb 2024 11:40:20 +0100 Subject: [PATCH 030/120] Add `upload` method to `ParquetExports` class using the uploader strategy --- pixl_core/src/core/exports.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pixl_core/src/core/exports.py b/pixl_core/src/core/exports.py index 20bcaf0b4..21b726514 100644 --- a/pixl_core/src/core/exports.py +++ b/pixl_core/src/core/exports.py @@ -20,6 +20,9 @@ import slugify +from core.project_config import load_project_config +from core.upload import FTPSUploader + if TYPE_CHECKING: import datetime import pathlib @@ -45,6 +48,7 @@ def __init__( self.export_dir = export_dir self.project_slug, self.extract_time_slug = self._get_slugs(project_name, extract_datetime) project_base = self.export_dir / self.project_slug + self.project_config = load_project_config(self.project_slug) self.current_extract_base = project_base / "all_extracts" / self.extract_time_slug self.public_output = self.current_extract_base / "omop" / "public" @@ -108,3 +112,12 @@ def export_radiology(self, export_df: pd.DataFrame) -> pathlib.Path: def _mkdir(directory: pathlib.Path) -> pathlib.Path: directory.mkdir(parents=True, exist_ok=True) return directory + + def upload(self) -> None: + """Upload the latest extract to the DSH.""" + destination = self.project_config.destination.parquet + if destination == "ftps": + FTPSUploader(self.project_slug).upload_parquet_files(self) + else: + msg = f"Destination {destination} for parquet files not supported" + raise ValueError(msg) From 5a8a5402b10c48ac3a0c9168ad8b26d9c94e8dc3 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Mon, 26 Feb 2024 11:40:45 +0100 Subject: [PATCH 031/120] Update EHR API to use the new uploader strategy --- pixl_ehr/src/pixl_ehr/main.py | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/pixl_ehr/src/pixl_ehr/main.py b/pixl_ehr/src/pixl_ehr/main.py index 2ca76289f..521ebb488 100644 --- a/pixl_ehr/src/pixl_ehr/main.py +++ b/pixl_ehr/src/pixl_ehr/main.py @@ -27,14 +27,11 @@ from azure.storage.blob import BlobServiceClient from core.exports import ParquetExport from core.patient_queue import PixlConsumer -from core.project_config import load_project_config from core.rest_api.router import router, state -from core.upload import upload_parquet_files from decouple import config from fastapi import FastAPI, HTTPException from fastapi.responses import JSONResponse from pydantic import BaseModel -from slugify import slugify from ._databases import PIXLDatabase from ._processing import process_message @@ -94,20 +91,17 @@ def export_patient_data(export_params: ExportRadiologyData) -> None: logger.info("Exporting Patient Data for '%s'", export_params.project_name) export_radiology_as_parquet(export_params) - project_slug = slugify(export_params.project_name) - project_config = load_project_config(project_slug) # Upload Parquet files to the appropriate endpoint + parquet_export = ParquetExport( + export_params.project_name, export_params.extract_datetime, export_params.output_dir + ) - if project_config.destination.parquet == "ftps": - upload_parquet_files( - ParquetExport( - export_params.project_name, export_params.extract_datetime, export_params.output_dir - ) - ) - else: - msg = f"Destination {project_config.destination.parquet} for parquet files unavailable" - logger.error(msg) - raise HTTPException(status_code=400, detail=msg) + try: + parquet_export.upload() + except ValueError as e: + msg = "Destination for parquet files unavailable" + logger.exception(msg) + raise HTTPException(status_code=400, detail=msg) from e def export_radiology_as_parquet(export_params: ExportRadiologyData) -> None: From 633ad54063eac6b449f85dbbfa904e45440fc4b6 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Mon, 26 Feb 2024 11:50:42 +0100 Subject: [PATCH 032/120] Load project config only when necessary --- pixl_core/src/core/exports.py | 4 ++-- pixl_core/src/core/upload.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pixl_core/src/core/exports.py b/pixl_core/src/core/exports.py index 21b726514..75de2af8b 100644 --- a/pixl_core/src/core/exports.py +++ b/pixl_core/src/core/exports.py @@ -48,7 +48,6 @@ def __init__( self.export_dir = export_dir self.project_slug, self.extract_time_slug = self._get_slugs(project_name, extract_datetime) project_base = self.export_dir / self.project_slug - self.project_config = load_project_config(self.project_slug) self.current_extract_base = project_base / "all_extracts" / self.extract_time_slug self.public_output = self.current_extract_base / "omop" / "public" @@ -115,7 +114,8 @@ def _mkdir(directory: pathlib.Path) -> pathlib.Path: def upload(self) -> None: """Upload the latest extract to the DSH.""" - destination = self.project_config.destination.parquet + project_config = load_project_config(self.project_slug) + destination = project_config.destination.parquet if destination == "ftps": FTPSUploader(self.project_slug).upload_parquet_files(self) else: diff --git a/pixl_core/src/core/upload.py b/pixl_core/src/core/upload.py index 6c23f63ba..f1f5a1ebf 100644 --- a/pixl_core/src/core/upload.py +++ b/pixl_core/src/core/upload.py @@ -40,7 +40,7 @@ class Uploader(ABC): - """Upload strategy interface for DICOM datasets.""" + """Upload strategy interface.""" @abstractmethod def __init__(self, project: str) -> None: From d1208dba054a727466538d7e4b42031043617661 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Mon, 26 Feb 2024 12:19:17 +0100 Subject: [PATCH 033/120] Move `parquet_export` fixture out of conftest Fixture is only used in `test_upload` anyway, and it avoids an issue where importing the `ParquetEport` class requires the `PROJECT_CONFIGS_DIR` envvar to be set, but is only set after the imports in the conftest. --- pixl_core/tests/conftest.py | 11 ----------- pixl_core/tests/test_upload.py | 20 +++++++++++++++++++- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/pixl_core/tests/conftest.py b/pixl_core/tests/conftest.py index 26ae5275b..65a2a581a 100644 --- a/pixl_core/tests/conftest.py +++ b/pixl_core/tests/conftest.py @@ -22,7 +22,6 @@ import pytest from core.db.models import Base, Extract, Image -from core.exports import ParquetExport from core.upload import FTPSUploader from sqlalchemy import Engine, create_engine from sqlalchemy.orm import Session, sessionmaker @@ -197,13 +196,3 @@ def already_exported_dicom_image(rows_in_session) -> Image: def export_dir(tmp_path_factory: pytest.TempPathFactory) -> pathlib.Path: """Tmp dir to for tests to extract to.""" return tmp_path_factory.mktemp("export_base") / "exports" - - -@pytest.fixture() -def parquet_export(export_dir) -> ParquetExport: - """Return a ParquetExport object.""" - return ParquetExport( - project_name="i-am-a-project", - extract_datetime=datetime.datetime.now(tz=datetime.timezone.utc), - export_dir=export_dir, - ) diff --git a/pixl_core/tests/test_upload.py b/pixl_core/tests/test_upload.py index 59a06e447..ce8948d76 100644 --- a/pixl_core/tests/test_upload.py +++ b/pixl_core/tests/test_upload.py @@ -20,6 +20,7 @@ import pytest from core.db.models import Image from core.db.queries import get_project_slug_from_hashid, update_exported_at +from core.exports import ParquetExport @pytest.mark.usefixtures("ftps_server") @@ -70,6 +71,23 @@ def test_update_exported_and_save(rows_in_session) -> None: assert actual_export_time == expected_export_time +@pytest.fixture() +def parquet_export(export_dir) -> ParquetExport: + """ + Return a ParquetExport object. + + This fixture is deliberately not definied in conftest, because it imports the ParquetExport + class, which in turn loads the PixlConfig class, which in turn requres the PROJECT_CONFIGS_DIR + environment to be set. This environment variable is set in conftest, so the import needs to + happen after that. + """ + return ParquetExport( + project_name="i-am-a-project", + extract_datetime=datetime.datetime.now(tz=datetime.timezone.utc), + export_dir=export_dir, + ) + + @pytest.mark.usefixtures("ftps_server") def test_upload_parquet(parquet_export, ftps_home_dir, ftps_uploader) -> None: """Tests that parquet files are uploaded to the correct location""" @@ -81,7 +99,7 @@ def test_upload_parquet(parquet_export, ftps_home_dir, ftps_uploader) -> None: parquet_export.export_radiology(pd.DataFrame(list("dummy"), columns=["D"])) # ACT - ftps_uploader.upload_parquet_files(parquet_export) + parquet_export.upload() # ASSERT expected_public_parquet_dir = ( ftps_home_dir / parquet_export.project_slug / parquet_export.extract_time_slug / "parquet" From e8930d234ba377047cc6577f20ee8932a3b2ab81 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Mon, 26 Feb 2024 12:19:58 +0100 Subject: [PATCH 034/120] Update test instruction in EHR API --- pixl_ehr/README.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/pixl_ehr/README.md b/pixl_ehr/README.md index 8eb65c4a4..1dccbd1a9 100644 --- a/pixl_ehr/README.md +++ b/pixl_ehr/README.md @@ -34,13 +34,7 @@ pip install -e ../pixl_core/ -e . ```bash pip install -e ../pixl_core/ -e .[test] -pytest -m "not processing" -``` - -and the processing tests with - -```bash -./tests/run-processing-tests.sh +pytest ``` To test the availability of a CogStack instance, we mock up a *FastAPI* server which simply takes in From 5302276e8a857eb8cb25f04ded92b340f2abbb6c Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Mon, 26 Feb 2024 12:49:50 +0100 Subject: [PATCH 035/120] Use uploader strategy in orthanc-anonn for DICOM uploads --- orthanc/orthanc-anon/plugin/pixl.py | 36 +++++++++++++---------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/orthanc/orthanc-anon/plugin/pixl.py b/orthanc/orthanc-anon/plugin/pixl.py index 27ce84dcd..4eacb3831 100644 --- a/orthanc/orthanc-anon/plugin/pixl.py +++ b/orthanc/orthanc-anon/plugin/pixl.py @@ -143,12 +143,27 @@ def Send(resourceId: str) -> None: # send to destination if project_config.destination.dicom == "ftps": - SendViaFTPS(resourceId) + msg = f"Sending {resourceId} via FTPS" + logger.debug(msg) + + zip_content = _get_study_zip_archive(resourceId) + upload.FTPSUploader(slug).upload_dicom_image(zip_content, hashed_patient_id) else: msg = f"Invalid destination: {project_config.destination.dicom}" raise ValueError(msg) +def _get_study_zip_archive(resourceId: str) -> BytesIO: + # Download zip archive of the DICOM resource + query = f"{ORTHANC_URL}/studies/{resourceId}/archive" + fail_msg = "Could not download archive of resource '%s'" + response_study = _query(resourceId, query, fail_msg) + + # get the zip content + logger.debug("Downloaded data for resource %s", resourceId) + return BytesIO(response_study.content) + + def SendViaStow(resourceId): """ Makes a POST API call to upload the resource to a dicom-web server @@ -178,25 +193,6 @@ def SendViaStow(resourceId): orthanc.LogError("Failed to send via STOW") -def SendViaFTPS(resourceId: str) -> None: - """ - Makes a POST API call to upload the resource to an FTPS server - using orthanc credentials as authorisation - """ - msg = f"Sending {resourceId} via FTPS" - logger.debug(msg) - # Download zip archive of the DICOM resource - query = f"{ORTHANC_URL}/studies/{resourceId}/archive" - fail_msg = "Could not download archive of resource '%s'" - response_study = _query(resourceId, query, fail_msg) - - # get the zip content - zip_content = response_study.content - logger.debug("Downloaded data for resource %s", resourceId) - - upload.upload_dicom_image(BytesIO(zip_content), _get_patient_id(resourceId)) - - def _get_patient_id(resourceId: str) -> str: """ Queries the Orthanc instance to get the PatientID for a given resource. From e1559790669abe6dd03917118c1e3c2dea59fc5d Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Mon, 26 Feb 2024 13:51:28 +0100 Subject: [PATCH 036/120] Update required environment variables - Remove the `FTP_` variables (except `FTP_PORT`), as these are now retrievd from the Azure Keyvault - Add Azure Keyvault variables to docker-compose --- .env.sample | 14 +++----------- docker-compose.yml | 13 ++++++++----- test/.env | 7 +------ 3 files changed, 12 insertions(+), 22 deletions(-) diff --git a/.env.sample b/.env.sample index a143733ac..70128dbd4 100644 --- a/.env.sample +++ b/.env.sample @@ -26,6 +26,7 @@ PIXL_EHR_API_PORT= RABBITMQ_PORT= RABBITMQ_ADMIN_PORT= PIXL_IMAGING_API_PORT= +FTP_PORT= # Hasher API HASHER_API_AZ_CLIENT_ID= @@ -68,10 +69,6 @@ AZ_DICOM_ENDPOINT_CLIENT_SECRET= AZ_DICOM_ENDPOINT_TENANT_ID= # EHR extraction API -PIXL_EHR_API_AZ_SUBSCRIPTION_ID= -PIXL_EHR_API_AZ_CLIENT_ID= -PIXL_EHR_API_AZ_CLIENT_SECRET= -PIXL_EHR_API_AZ_TENANT_ID= PIXL_EHR_API_AZ_STORAGE_ACCOUNT_NAME= PIXL_EHR_API_AZ_STORAGE_CONTAINER_NAME= PIXL_EHR_COGSTACK_REDACT_URL= @@ -84,11 +81,6 @@ RABBITMQ_PASSWORD= PIXL_DICOM_TRANSFER_TIMEOUT=240 PIXL_QUERY_TIMEOUT=10 -# FTP server -FTP_HOST= -FTP_USER_NAME= -FTP_USER_PASSWORD= -FTP_PORT= - # Project configs directory -PROJECT_CONFIGS_DIR= \ No newline at end of file +PROJECT_CONFIGS_DIR= + diff --git a/docker-compose.yml b/docker-compose.yml index de6300be2..2289b892f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -61,6 +61,12 @@ x-ftp-host: &ftp-host FTP_USER_PASSWORD: ${FTP_USER_PASSWORD} FTP_PORT: ${FTP_PORT} +x-azure-keyvault: &azure-keyvault + AZURE_CLIENT_ID: ${EXPORT_AZ_CLIENT_ID} + AZURE_CLIENT_SECRET: ${EXPORT_AZ_CLIENT_PASSWORD} + AZURE_TENANT_ID: ${EXPORT_AZ_TENANT_ID} + AZURE_KEY_VAULT_NAME: ${EXPORT_AZ_KEY_VAULT_NAME} + x-logs-volume: &logs-volume type: volume source: logs @@ -116,7 +122,7 @@ services: <<: *build-args-common command: /run/secrets environment: - <<: [*proxy-common, *pixl-common-env, *ftp-host] + <<: [*proxy-common, *pixl-common-env, *ftp-host, *azure-keyvault] ORTHANC_NAME: "PIXL: Anon" ORTHANC_USERNAME: ${ORTHANC_ANON_USERNAME} ORTHANC_PASSWORD: ${ORTHANC_ANON_PASSWORD} @@ -231,10 +237,7 @@ services: args: <<: *build-args-common environment: - <<: [*pixl-db, *emap-db, *proxy-common, *pixl-common-env, *pixl-rabbit-mq, *ftp-host] - AZURE_CLIENT_ID: ${PIXL_EHR_API_AZ_CLIENT_ID} - AZURE_CLIENT_SECRET: ${PIXL_EHR_API_AZ_CLIENT_SECRET} - AZURE_TENANT_ID: ${PIXL_EHR_API_AZ_TENANT_ID} + <<: [*pixl-db, *emap-db, *proxy-common, *pixl-common-env, *pixl-rabbit-mq, *ftp-host, *azure-keyvault] AZ_STORAGE_ACCOUNT_NAME: ${PIXL_EHR_API_AZ_STORAGE_ACCOUNT_NAME} AZ_STORAGE_CONTAINER_NAME: ${PIXL_EHR_API_AZ_STORAGE_CONTAINER_NAME} COGSTACK_REDACT_URL: ${PIXL_EHR_COGSTACK_REDACT_URL} diff --git a/test/.env b/test/.env index b3aaa097b..6f58e70a4 100644 --- a/test/.env +++ b/test/.env @@ -62,10 +62,5 @@ PIXL_EHR_COGSTACK_REDACT_URL=http://cogstack-api:8000/redact RABBITMQ_USERNAME=rabbitmq_username RABBITMQ_PASSWORD=rabbitmq_password -# FTP server -FTP_HOST=ftp-server -FTP_USER_NAME=ftp_username -FTP_USER_PASSWORD=longpassword - # Project configs directory -PROJECT_CONFIGS_DIR=/project_configs \ No newline at end of file +PROJECT_CONFIGS_DIR=/project_configs From 0596205e5f86a3eedfab7e4223c3c794cdab455c Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Mon, 26 Feb 2024 13:52:32 +0100 Subject: [PATCH 037/120] Format docker-compose --- docker-compose.yml | 580 +++++++++++++++++++++++---------------------- 1 file changed, 302 insertions(+), 278 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 2289b892f..3a35207f2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,311 +20,335 @@ x-http-proxy: &http-proxy ${HTTP_PROXY} x-https-proxy: &https-proxy ${HTTPS_PROXY} x-no-proxy: &no-proxy localhost,0.0.0.0,127.0.0.1,uclvlddpragae07,hasher-api,orthanc-raw x-proxy-common: &proxy-common - HTTP_PROXY: *http-proxy - http_proxy: *http-proxy - HTTPS_PROXY: *https-proxy - https_proxy: *https-proxy - NO_PROXY: *no-proxy - no_proxy: *no-proxy + HTTP_PROXY: *http-proxy + http_proxy: *http-proxy + HTTPS_PROXY: *https-proxy + https_proxy: *https-proxy + NO_PROXY: *no-proxy + no_proxy: *no-proxy x-build-args-common: &build-args-common - <<: [*proxy-common] + <<: [*proxy-common] x-pixl-common-env: &pixl-common-env - ENV: ${ENV} - DEBUG: ${DEBUG} + ENV: ${ENV} + DEBUG: ${DEBUG} x-pixl-rabbit-mq: &pixl-rabbit-mq - RABBITMQ_HOST: "queue" # Name of the queue service - RABBITMQ_PORT: "5672" - RABBITMQ_USERNAME: ${RABBITMQ_USERNAME} - RABBITMQ_PASSWORD: ${RABBITMQ_PASSWORD} + RABBITMQ_HOST: "queue" # Name of the queue service + RABBITMQ_PORT: "5672" + RABBITMQ_USERNAME: ${RABBITMQ_USERNAME} + RABBITMQ_PASSWORD: ${RABBITMQ_PASSWORD} x-emap-db: &emap-db - EMAP_UDS_HOST: ${EMAP_UDS_HOST} - EMAP_UDS_PORT: ${EMAP_UDS_PORT} - EMAP_UDS_NAME: ${EMAP_UDS_NAME} - EMAP_UDS_USER: ${EMAP_UDS_USER} - EMAP_UDS_PASSWORD: ${EMAP_UDS_PASSWORD} - EMAP_UDS_SCHEMA_NAME: ${EMAP_UDS_SCHEMA_NAME} + EMAP_UDS_HOST: ${EMAP_UDS_HOST} + EMAP_UDS_PORT: ${EMAP_UDS_PORT} + EMAP_UDS_NAME: ${EMAP_UDS_NAME} + EMAP_UDS_USER: ${EMAP_UDS_USER} + EMAP_UDS_PASSWORD: ${EMAP_UDS_PASSWORD} + EMAP_UDS_SCHEMA_NAME: ${EMAP_UDS_SCHEMA_NAME} x-pixl-db: &pixl-db - PIXL_DB_HOST: ${PIXL_DB_HOST} - PIXL_DB_PORT: ${PIXL_DB_PORT} - PIXL_DB_USER: ${PIXL_DB_USER} - PIXL_DB_PASSWORD: ${PIXL_DB_PASSWORD} - PIXL_DB_NAME: ${PIXL_DB_NAME} + PIXL_DB_HOST: ${PIXL_DB_HOST} + PIXL_DB_PORT: ${PIXL_DB_PORT} + PIXL_DB_USER: ${PIXL_DB_USER} + PIXL_DB_PASSWORD: ${PIXL_DB_PASSWORD} + PIXL_DB_NAME: ${PIXL_DB_NAME} x-ftp-host: &ftp-host - FTP_HOST: ${FTP_HOST} - FTP_USER_NAME: ${FTP_USER_NAME} - FTP_USER_PASSWORD: ${FTP_USER_PASSWORD} - FTP_PORT: ${FTP_PORT} + FTP_HOST: ${FTP_HOST} + FTP_USER_NAME: ${FTP_USER_NAME} + FTP_USER_PASSWORD: ${FTP_USER_PASSWORD} + FTP_PORT: ${FTP_PORT} x-azure-keyvault: &azure-keyvault - AZURE_CLIENT_ID: ${EXPORT_AZ_CLIENT_ID} - AZURE_CLIENT_SECRET: ${EXPORT_AZ_CLIENT_PASSWORD} - AZURE_TENANT_ID: ${EXPORT_AZ_TENANT_ID} - AZURE_KEY_VAULT_NAME: ${EXPORT_AZ_KEY_VAULT_NAME} + AZURE_CLIENT_ID: ${EXPORT_AZ_CLIENT_ID} + AZURE_CLIENT_SECRET: ${EXPORT_AZ_CLIENT_PASSWORD} + AZURE_TENANT_ID: ${EXPORT_AZ_TENANT_ID} + AZURE_KEY_VAULT_NAME: ${EXPORT_AZ_KEY_VAULT_NAME} x-logs-volume: &logs-volume - type: volume - source: logs - target: /logs + type: volume + source: logs + target: /logs volumes: - logs: - orthanc-anon-data: - orthanc-raw-data: - postgres-data: - exports: + logs: + orthanc-anon-data: + orthanc-raw-data: + postgres-data: + exports: networks: - pixl-net: + pixl-net: ################################################################################ # Services services: + hasher-api: + build: + context: . + dockerfile: ./docker/hasher-api/Dockerfile + args: + <<: *build-args-common + environment: + <<: [*proxy-common, *pixl-common-env] + AZURE_CLIENT_ID: ${HASHER_API_AZ_CLIENT_ID} + AZURE_CLIENT_SECRET: ${HASHER_API_AZ_CLIENT_PASSWORD} + AZURE_TENANT_ID: ${HASHER_API_AZ_TENANT_ID} + AZURE_KEY_VAULT_NAME: ${HASHER_API_AZ_KEY_VAULT_NAME} + AZURE_KEY_VAULT_SECRET_NAME: ${HASHER_API_AZ_KEY_VAULT_SECRET_NAME} + env_file: + - ./docker/common.env + ports: + - "${HASHER_API_PORT}:8000" + volumes: + - *logs-volume + networks: + - pixl-net + healthcheck: + test: ["CMD", "curl", "-f", "http://hasher-api:8000/heart-beat"] + interval: 10s + timeout: 30s + retries: 5 + restart: "no" - hasher-api: - build: - context: . - dockerfile: ./docker/hasher-api/Dockerfile - args: - <<: *build-args-common - environment: - <<: [*proxy-common, *pixl-common-env] - AZURE_CLIENT_ID: ${HASHER_API_AZ_CLIENT_ID} - AZURE_CLIENT_SECRET: ${HASHER_API_AZ_CLIENT_PASSWORD} - AZURE_TENANT_ID: ${HASHER_API_AZ_TENANT_ID} - AZURE_KEY_VAULT_NAME: ${HASHER_API_AZ_KEY_VAULT_NAME} - AZURE_KEY_VAULT_SECRET_NAME: ${HASHER_API_AZ_KEY_VAULT_SECRET_NAME} - env_file: - - ./docker/common.env - ports: - - "${HASHER_API_PORT}:8000" - volumes: - - *logs-volume - networks: - - pixl-net - healthcheck: - test: [ "CMD", "curl", "-f", "http://hasher-api:8000/heart-beat" ] - interval: 10s - timeout: 30s - retries: 5 - restart: "no" + orthanc-anon: + build: + context: . + dockerfile: ./docker/orthanc-anon/Dockerfile + args: + <<: *build-args-common + command: /run/secrets + environment: + <<: [*proxy-common, *pixl-common-env, *ftp-host, *azure-keyvault] + ORTHANC_NAME: "PIXL: Anon" + ORTHANC_USERNAME: ${ORTHANC_ANON_USERNAME} + ORTHANC_PASSWORD: ${ORTHANC_ANON_PASSWORD} + ORTHANC_ANON_AE_TITLE: ${ORTHANC_ANON_AE_TITLE} + ORTHANC_AUTOROUTE_ANON_TO_ENDPOINT: ${ORTHANC_AUTOROUTE_ANON_TO_ENDPOINT} + ORTHANC_RAW_AE_TITLE: ${ORTHANC_RAW_AE_TITLE} + ORTHANC_RAW_DICOM_PORT: "4242" + ORTHANC_RAW_HOSTNAME: "orthanc-raw" + PIXL_DB_HOST: ${PIXL_DB_HOST} + PIXL_DB_PORT: ${PIXL_DB_PORT} + PIXL_DB_NAME: ${PIXL_DB_NAME} + PIXL_DB_USER: ${PIXL_DB_USER} + PIXL_DB_PASSWORD: ${PIXL_DB_PASSWORD} + DICOM_WEB_PLUGIN_ENABLED: ${ENABLE_DICOM_WEB} + HASHER_API_AZ_NAME: "hasher-api" + HASHER_API_PORT: 8000 + HTTP_TIMEOUT: ${ORTHANC_ANON_HTTP_TIMEOUT} + AZ_DICOM_ENDPOINT_NAME: ${AZ_DICOM_ENDPOINT_NAME} + AZ_DICOM_ENDPOINT_URL: ${AZ_DICOM_ENDPOINT_URL} + AZ_DICOM_ENDPOINT_TOKEN: ${AZ_DICOM_ENDPOINT_TOKEN} + AZ_DICOM_ENDPOINT_CLIENT_ID: ${AZ_DICOM_ENDPOINT_CLIENT_ID} + AZ_DICOM_ENDPOINT_CLIENT_SECRET: ${AZ_DICOM_ENDPOINT_CLIENT_SECRET} + AZ_DICOM_ENDPOINT_TENANT_ID: ${AZ_DICOM_ENDPOINT_TENANT_ID} + AZ_DICOM_TOKEN_REFRESH_SECS: "600" + TIME_OFFSET: "${STUDY_TIME_OFFSET}" + SALT_VALUE: ${SALT_VALUE}" + PROJECT_CONFIGS_DIR: ${PROJECT_CONFIGS_DIR:-/project_configs} + ports: + - "${ORTHANC_ANON_DICOM_PORT}:4242" + - "${ORTHANC_ANON_WEB_PORT}:8042" + volumes: + - type: volume + source: orthanc-anon-data + target: /var/lib/orthanc/db + - ${PWD}/orthanc/orthanc-anon/config:/run/secrets:ro + - ${PWD}/project_configs:${PROJECT_CONFIGS_DIR:-/project_configs}:ro + networks: + - pixl-net + depends_on: + postgres: + condition: service_healthy + healthcheck: + test: + [ + "CMD", + "curl", + "-f", + "-u", + "${ORTHANC_ANON_USERNAME}:${ORTHANC_ANON_PASSWORD}", + "http://orthanc-anon:8042/heart-beat", + ] + interval: 10s + timeout: 30s + retries: 5 + restart: "no" - orthanc-anon: - build: - context: . - dockerfile: ./docker/orthanc-anon/Dockerfile - args: - <<: *build-args-common - command: /run/secrets - environment: - <<: [*proxy-common, *pixl-common-env, *ftp-host, *azure-keyvault] - ORTHANC_NAME: "PIXL: Anon" - ORTHANC_USERNAME: ${ORTHANC_ANON_USERNAME} - ORTHANC_PASSWORD: ${ORTHANC_ANON_PASSWORD} - ORTHANC_ANON_AE_TITLE: ${ORTHANC_ANON_AE_TITLE} - ORTHANC_AUTOROUTE_ANON_TO_ENDPOINT: ${ORTHANC_AUTOROUTE_ANON_TO_ENDPOINT} - ORTHANC_RAW_AE_TITLE: ${ORTHANC_RAW_AE_TITLE} - ORTHANC_RAW_DICOM_PORT: "4242" - ORTHANC_RAW_HOSTNAME: "orthanc-raw" - PIXL_DB_HOST: ${PIXL_DB_HOST} - PIXL_DB_PORT: ${PIXL_DB_PORT} - PIXL_DB_NAME: ${PIXL_DB_NAME} - PIXL_DB_USER: ${PIXL_DB_USER} - PIXL_DB_PASSWORD: ${PIXL_DB_PASSWORD} - DICOM_WEB_PLUGIN_ENABLED: ${ENABLE_DICOM_WEB} - HASHER_API_AZ_NAME: "hasher-api" - HASHER_API_PORT: 8000 - HTTP_TIMEOUT: ${ORTHANC_ANON_HTTP_TIMEOUT} - AZ_DICOM_ENDPOINT_NAME: ${AZ_DICOM_ENDPOINT_NAME} - AZ_DICOM_ENDPOINT_URL: ${AZ_DICOM_ENDPOINT_URL} - AZ_DICOM_ENDPOINT_TOKEN: ${AZ_DICOM_ENDPOINT_TOKEN} - AZ_DICOM_ENDPOINT_CLIENT_ID: ${AZ_DICOM_ENDPOINT_CLIENT_ID} - AZ_DICOM_ENDPOINT_CLIENT_SECRET: ${AZ_DICOM_ENDPOINT_CLIENT_SECRET} - AZ_DICOM_ENDPOINT_TENANT_ID: ${AZ_DICOM_ENDPOINT_TENANT_ID} - AZ_DICOM_TOKEN_REFRESH_SECS: "600" - TIME_OFFSET: "${STUDY_TIME_OFFSET}" - SALT_VALUE: ${SALT_VALUE}" - PROJECT_CONFIGS_DIR: ${PROJECT_CONFIGS_DIR:-/project_configs} - ports: - - "${ORTHANC_ANON_DICOM_PORT}:4242" - - "${ORTHANC_ANON_WEB_PORT}:8042" - volumes: - - type: volume - source: orthanc-anon-data - target: /var/lib/orthanc/db - - ${PWD}/orthanc/orthanc-anon/config:/run/secrets:ro - - ${PWD}/project_configs:${PROJECT_CONFIGS_DIR:-/project_configs}:ro - networks: - - pixl-net - depends_on: - postgres: - condition: service_healthy - healthcheck: - test: [ "CMD", "curl", "-f", "-u" , "${ORTHANC_ANON_USERNAME}:${ORTHANC_ANON_PASSWORD}", "http://orthanc-anon:8042/heart-beat" ] - interval: 10s - timeout: 30s - retries: 5 - restart: "no" + orthanc-raw: + build: + context: . + dockerfile: ./docker/orthanc-raw/Dockerfile + args: + <<: *build-args-common + ORTHANC_RAW_MAXIMUM_STORAGE_SIZE: ${ORTHANC_RAW_MAXIMUM_STORAGE_SIZE} + command: /run/secrets + environment: + <<: [*pixl-db, *proxy-common, *pixl-common-env] + ORTHANC_NAME: "PIXL: Raw" + ORTHANC_USERNAME: ${ORTHANC_RAW_USERNAME} + ORTHANC_PASSWORD: ${ORTHANC_RAW_PASSWORD} + ORTHANC_RAW_AE_TITLE: ${ORTHANC_RAW_AE_TITLE} + ORTHANC_AUTOROUTE_RAW_TO_ANON: ${ORTHANC_AUTOROUTE_RAW_TO_ANON} + ORTHANC_RAW_MAXIMUM_STORAGE_SIZE: ${ORTHANC_RAW_MAXIMUM_STORAGE_SIZE} + VNAQR_AE_TITLE: ${VNAQR_AE_TITLE} + VNAQR_DICOM_PORT: ${VNAQR_DICOM_PORT} + VNAQR_IP_ADDR: ${VNAQR_IP_ADDR} + ORTHANC_ANON_AE_TITLE: ${ORTHANC_ANON_AE_TITLE} + ORTHANC_ANON_DICOM_PORT: "4242" + ORTHANC_ANON_HOSTNAME: "orthanc-anon" + ports: + - "${ORTHANC_RAW_DICOM_PORT}:4242" + - "${ORTHANC_RAW_WEB_PORT}:8042" + volumes: + - type: volume + source: orthanc-raw-data + target: /var/lib/orthanc/db + networks: + - pixl-net + depends_on: + postgres: + condition: service_healthy + orthanc-anon: + condition: service_started + healthcheck: + test: + [ + "CMD", + "curl", + "-f", + "-u", + "${ORTHANC_RAW_USERNAME}:${ORTHANC_RAW_PASSWORD}", + "http://orthanc-raw:8042/heart-beat", + ] + interval: 10s + timeout: 30s + retries: 5 + restart: "no" - orthanc-raw: - build: - context: . - dockerfile: ./docker/orthanc-raw/Dockerfile - args: - <<: *build-args-common - ORTHANC_RAW_MAXIMUM_STORAGE_SIZE: ${ORTHANC_RAW_MAXIMUM_STORAGE_SIZE} - command: /run/secrets - environment: - <<: [*pixl-db, *proxy-common, *pixl-common-env] - ORTHANC_NAME: "PIXL: Raw" - ORTHANC_USERNAME: ${ORTHANC_RAW_USERNAME} - ORTHANC_PASSWORD: ${ORTHANC_RAW_PASSWORD} - ORTHANC_RAW_AE_TITLE: ${ORTHANC_RAW_AE_TITLE} - ORTHANC_AUTOROUTE_RAW_TO_ANON: ${ORTHANC_AUTOROUTE_RAW_TO_ANON} - ORTHANC_RAW_MAXIMUM_STORAGE_SIZE: ${ORTHANC_RAW_MAXIMUM_STORAGE_SIZE} - VNAQR_AE_TITLE : ${VNAQR_AE_TITLE} - VNAQR_DICOM_PORT: ${VNAQR_DICOM_PORT} - VNAQR_IP_ADDR: ${VNAQR_IP_ADDR} - ORTHANC_ANON_AE_TITLE: ${ORTHANC_ANON_AE_TITLE} - ORTHANC_ANON_DICOM_PORT: "4242" - ORTHANC_ANON_HOSTNAME: "orthanc-anon" - ports: - - "${ORTHANC_RAW_DICOM_PORT}:4242" - - "${ORTHANC_RAW_WEB_PORT}:8042" - volumes: - - type: volume - source: orthanc-raw-data - target: /var/lib/orthanc/db - networks: - - pixl-net - depends_on: - postgres: - condition: service_healthy - orthanc-anon: - condition: service_started - healthcheck: - test: [ "CMD", "curl", "-f", "-u" , "${ORTHANC_RAW_USERNAME}:${ORTHANC_RAW_PASSWORD}", "http://orthanc-raw:8042/heart-beat" ] - interval: 10s - timeout: 30s - retries: 5 - restart: "no" + queue: + image: rabbitmq:3.12.9-management + environment: + RABBITMQ_DEFAULT_USER: ${RABBITMQ_USERNAME} + RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD} + healthcheck: + test: rabbitmq-diagnostics -q check_running + interval: 30s + timeout: 30s + retries: 3 + ports: + - "${RABBITMQ_PORT}:5672" + - "${RABBITMQ_ADMIN_PORT}:15672" + networks: + - pixl-net - queue: - image: rabbitmq:3.12.9-management - environment: - RABBITMQ_DEFAULT_USER: ${RABBITMQ_USERNAME} - RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD} - healthcheck: - test: rabbitmq-diagnostics -q check_running - interval: 30s - timeout: 30s - retries: 3 - ports: - - "${RABBITMQ_PORT}:5672" - - "${RABBITMQ_ADMIN_PORT}:15672" - networks: - - pixl-net + ehr-api: + build: + context: . + dockerfile: ./docker/ehr-api/Dockerfile + args: + <<: *build-args-common + environment: + <<: + [ + *pixl-db, + *emap-db, + *proxy-common, + *pixl-common-env, + *pixl-rabbit-mq, + *ftp-host, + *azure-keyvault, + ] + AZ_STORAGE_ACCOUNT_NAME: ${PIXL_EHR_API_AZ_STORAGE_ACCOUNT_NAME} + AZ_STORAGE_CONTAINER_NAME: ${PIXL_EHR_API_AZ_STORAGE_CONTAINER_NAME} + COGSTACK_REDACT_URL: ${PIXL_EHR_COGSTACK_REDACT_URL} + PROJECT_CONFIGS_DIR: ${PROJECT_CONFIGS_DIR:-/project_configs} + env_file: + - ./docker/common.env + depends_on: + queue: + condition: service_healthy + postgres: + condition: service_healthy + hasher-api: + condition: service_healthy + ports: + - "${PIXL_EHR_API_PORT}:8000" + healthcheck: + interval: 10s + timeout: 30s + retries: 5 + networks: + - pixl-net + volumes: + - ${PWD}/exports:/run/exports + - ${PWD}/project_configs:${PROJECT_CONFIGS_DIR:-/project_configs}:ro - ehr-api: - build: - context: . - dockerfile: ./docker/ehr-api/Dockerfile - args: - <<: *build-args-common - environment: - <<: [*pixl-db, *emap-db, *proxy-common, *pixl-common-env, *pixl-rabbit-mq, *ftp-host, *azure-keyvault] - AZ_STORAGE_ACCOUNT_NAME: ${PIXL_EHR_API_AZ_STORAGE_ACCOUNT_NAME} - AZ_STORAGE_CONTAINER_NAME: ${PIXL_EHR_API_AZ_STORAGE_CONTAINER_NAME} - COGSTACK_REDACT_URL: ${PIXL_EHR_COGSTACK_REDACT_URL} - PROJECT_CONFIGS_DIR: ${PROJECT_CONFIGS_DIR:-/project_configs} - env_file: - - ./docker/common.env - depends_on: - queue: - condition: service_healthy - postgres: - condition: service_healthy - hasher-api: - condition: service_healthy - ports: - - "${PIXL_EHR_API_PORT}:8000" - healthcheck: - interval: 10s - timeout: 30s - retries: 5 - networks: - - pixl-net - volumes: - - ${PWD}/exports:/run/exports - - ${PWD}/project_configs:${PROJECT_CONFIGS_DIR:-/project_configs}:ro + imaging-api: + build: + context: . + dockerfile: ./docker/imaging-api/Dockerfile + args: + <<: *build-args-common + depends_on: + queue: + condition: service_healthy + orthanc-raw: + condition: service_healthy + healthcheck: + test: curl -f http://0.0.0.0:8000/heart-beat + interval: 10s + timeout: 30s + retries: 5 + networks: + - pixl-net + environment: + <<: [*pixl-rabbit-mq, *proxy-common] + ORTHANC_RAW_USERNAME: ${ORTHANC_RAW_USERNAME} + ORTHANC_RAW_PASSWORD: ${ORTHANC_RAW_PASSWORD} + ORTHANC_RAW_AE_TITLE: ${ORTHANC_RAW_AE_TITLE} + VNAQR_MODALITY: ${VNAQR_MODALITY} + SKIP_ALEMBIC: ${SKIP_ALEMBIC} + PIXL_DB_HOST: ${PIXL_DB_HOST} + PIXL_DB_PORT: ${PIXL_DB_PORT} + PIXL_DB_NAME: ${PIXL_DB_NAME} + PIXL_DB_USER: ${PIXL_DB_USER} + PIXL_DB_PASSWORD: ${PIXL_DB_PASSWORD} + PIXL_DICOM_TRANSFER_TIMEOUT: ${PIXL_DICOM_TRANSFER_TIMEOUT} + PIXL_QUERY_TIMEOUT: ${PIXL_QUERY_TIMEOUT} + ports: + - "${PIXL_IMAGING_API_PORT}:8000" - imaging-api: - build: - context: . - dockerfile: ./docker/imaging-api/Dockerfile - args: - <<: *build-args-common - depends_on: - queue: - condition: service_healthy - orthanc-raw: - condition: service_healthy - healthcheck: - test: curl -f http://0.0.0.0:8000/heart-beat - interval: 10s - timeout: 30s - retries: 5 - networks: - - pixl-net - environment: - <<: [*pixl-rabbit-mq, *proxy-common] - ORTHANC_RAW_USERNAME: ${ORTHANC_RAW_USERNAME} - ORTHANC_RAW_PASSWORD: ${ORTHANC_RAW_PASSWORD} - ORTHANC_RAW_AE_TITLE: ${ORTHANC_RAW_AE_TITLE} - VNAQR_MODALITY: ${VNAQR_MODALITY} - SKIP_ALEMBIC: ${SKIP_ALEMBIC} - PIXL_DB_HOST: ${PIXL_DB_HOST} - PIXL_DB_PORT: ${PIXL_DB_PORT} - PIXL_DB_NAME: ${PIXL_DB_NAME} - PIXL_DB_USER: ${PIXL_DB_USER} - PIXL_DB_PASSWORD: ${PIXL_DB_PASSWORD} - PIXL_DICOM_TRANSFER_TIMEOUT: ${PIXL_DICOM_TRANSFER_TIMEOUT} - PIXL_QUERY_TIMEOUT: ${PIXL_QUERY_TIMEOUT} - ports: - - "${PIXL_IMAGING_API_PORT}:8000" - -################################################################################ -# Data Stores - postgres: - build: - context: . - dockerfile: ./docker/postgres/Dockerfile - args: - <<: *build-args-common - environment: - POSTGRES_USER: ${PIXL_DB_USER} - POSTGRES_PASSWORD: ${PIXL_DB_PASSWORD} - POSTGRES_DB: ${PIXL_DB_NAME} - PGTZ: Europe/London - env_file: - - ./docker/common.env - command: postgres -c 'config_file=/etc/postgresql/postgresql.conf' - volumes: - - type: volume - source: postgres-data - target: /var/lib/postgresql/data - ports: - - "${POSTGRES_PORT}:5432" - healthcheck: - test: [ "CMD", "pg_isready", "-U", "${PIXL_DB_USER}" ] - interval: 10s - timeout: 30s - retries: 5 - restart: always - networks: - - pixl-net + ################################################################################ + # Data Stores + postgres: + build: + context: . + dockerfile: ./docker/postgres/Dockerfile + args: + <<: *build-args-common + environment: + POSTGRES_USER: ${PIXL_DB_USER} + POSTGRES_PASSWORD: ${PIXL_DB_PASSWORD} + POSTGRES_DB: ${PIXL_DB_NAME} + PGTZ: Europe/London + env_file: + - ./docker/common.env + command: postgres -c 'config_file=/etc/postgresql/postgresql.conf' + volumes: + - type: volume + source: postgres-data + target: /var/lib/postgresql/data + ports: + - "${POSTGRES_PORT}:5432" + healthcheck: + test: ["CMD", "pg_isready", "-U", "${PIXL_DB_USER}"] + interval: 10s + timeout: 30s + retries: 5 + restart: always + networks: + - pixl-net From 51ab9d9c5068165ce4de86c457b1ae13b7390021 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Mon, 26 Feb 2024 14:00:38 +0100 Subject: [PATCH 038/120] Load `.secrets.env` in system tests Contains the Azure Keyvault credentials --- test/run-system-test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/run-system-test.sh b/test/run-system-test.sh index b3da0a22b..f2b106b0c 100755 --- a/test/run-system-test.sh +++ b/test/run-system-test.sh @@ -23,7 +23,7 @@ docker compose --env-file .env -p system-test down --volumes docker compose --env-file .env -p system-test up --wait -d --build --remove-orphans # Warning: Requires to be run from the project root (cd .. && \ - docker compose --env-file test/.env -p system-test up --wait -d --build) + docker compose --env-file test/.env --env-file .secrets.env -p system-test up --wait -d --build) ./scripts/insert_test_data.sh From da46f950c1496d9f46856ec053275db985c7e155 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Mon, 26 Feb 2024 14:07:51 +0100 Subject: [PATCH 039/120] Add instructions for setting up project config --- README.md | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 884b6ea1e..60047c135 100644 --- a/README.md +++ b/README.md @@ -190,12 +190,24 @@ set in `.env` as `COGSTACK_REDACT_URL`. ### 3. Configure a new project - -- Copy the `template_config.yaml` file to a new file in the `projects/config` directory -- The project slug should match the project name in the `extract_summary.json` log file -- The filename of the project config should be ``.yaml -- Project config should be created in `/projects/config/` **in a new _git_ branch** -- Open PR in [PIXL](https://github.com/UCLH-Foundry/PIXL) to merge the new project config into `main` +To configure a new project, follow these steps: + +1. Create a new `git` branch from `main` + + ```sh + git checkout main + git pull + git switch -c + ``` + +1. Copy the `template_config.yaml` file to a new file in the `projects/config` directory and fill + in the details. +1. The filename of the project config should be ``.yaml + + >[!NOTE] + > The project slug should match the project name in the `extract_summary.json` log file! + +1. [Open a PR in PIXL](https://github.com/UCLH-Foundry/PIXL/compare) to merge the new project config into `main` #### The config YAML file @@ -206,7 +218,6 @@ The configuration file defines: - The anonymisation operations to be applied to the DICOM tags, by providing a file path to one or multiple YAML files - The endpoints used to upload the anonymised DICOM data and the public and radiology [parquet files](./docs/file_types/parquet_files.md). We currently support the following endpoints: - - `"none"`: no upload - `"ftps"`: a secure FTP server (for both _DICOM_ and _parquet_ files) Requires the `FTPS_*` environment variables to be set in `.env` @@ -215,10 +226,16 @@ The configuration file defines: - `"dicomweb"`: a DICOMweb server (for _DICOM_ files only) Requires the `DICOMWEB_*` environment variables to be set in `.env` -#### Set up secrets in Azure keyvault +#### Project secrets - +Any credentials required for uploading the project's results should be stored in an **Azure Key Vault**. +PIXL will query this key vault for the required secrets at runtime. This requires the following +environment variables to be set in `.secrets.env` so that PIXL can connect to the key vault: +- `EXPORT_AZ_CLIENT_ID` +- `EXPORT_AZ_CLIENT_PASSWORD` +- `EXPORT_AZ_TENANT_ID` +- `EXPORT_AZ_KEY_VAULT_NAME` ## Run From 2f20eb6541cb7996c61cb5fd8ffeec2c8ef692f8 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Mon, 26 Feb 2024 14:23:57 +0100 Subject: [PATCH 040/120] Update core docs --- README.md | 2 +- pixl_core/README.md | 14 +++++--------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 230c62076..a418339b7 100644 --- a/README.md +++ b/README.md @@ -226,7 +226,7 @@ The configuration file defines: - `"dicomweb"`: a DICOMweb server (for _DICOM_ files only) Requires the `DICOMWEB_*` environment variables to be set in `.env` -#### Project secrets +#### Project secrets {#project-secrets} Any credentials required for uploading the project's results should be stored in an **Azure Key Vault**. PIXL will query this key vault for the required secrets at runtime. This requires the following diff --git a/pixl_core/README.md b/pixl_core/README.md index fbc68471b..be5d22bf7 100644 --- a/pixl_core/README.md +++ b/pixl_core/README.md @@ -91,16 +91,12 @@ for convenience `latest` is a symlink to the most recent extract. ## Uploading to an FTPS server The `core.upload` module implements functionality to upload DICOM images and parquet files to an -**FTPS server**. This requires the following environment variables to be set: +several destinations. This requires the following environment variables to be set: -- `FTP_HOST`: URL to the FTPS server -- `FTP_PORT`: port on which the FTPS server is listening -- `FTP_USER_NAME`: name of user with access to the FTPS server -- `FTP_USER_PASSWORD`: password for the authorised user - -We provide mock values for these for the unit tests (see -[`./tests/conftest.py`](./tests/conftest.py)). When running in production, these should be defined -in the `.env` file (see [the example](../.env.sample)). +The `Uploader` abstract class provides a consistent interface for uploading files. Child classes +such as the `FTPSUploader` implement the actual upload functionality. The credentials required for +uploading are queried from an **Azure Keyvault** instance (implemented in `_secrets.py`), for which +the setup instructions are in the [top-level README](../README.md#project-secrets) When an extract is ready to be published to the DSH, the PIXL pipeline will upload the **Public** and **Radiology** [_parquet_ files](../docs/data/parquet_files.md) to the `` directory From 06841cad38c96140cc883fac5e51a11321d4155d Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Mon, 26 Feb 2024 16:09:57 +0100 Subject: [PATCH 041/120] Set PROJECT_CONFIGS_DIR envvar for cli tests --- cli/tests/conftest.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cli/tests/conftest.py b/cli/tests/conftest.py index f8d0f69d8..543d01a5c 100644 --- a/cli/tests/conftest.py +++ b/cli/tests/conftest.py @@ -14,6 +14,7 @@ """CLI testing fixtures.""" from __future__ import annotations +import os import pathlib import pytest @@ -21,6 +22,8 @@ from sqlalchemy import Engine, create_engine from sqlalchemy.orm import Session, sessionmaker +os.environ["PROJECT_CONFIGS_DIR"] = str(pathlib.Path(__file__).parents[2] / "project_configs") + @pytest.fixture(autouse=True) def export_dir(tmp_path_factory: pytest.TempPathFactory) -> pathlib.Path: From ea0aa8b745b274400c6ff47c53246deb67086972 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Mon, 26 Feb 2024 16:38:12 +0100 Subject: [PATCH 042/120] Use same config for `MockFTPSUploader` as for the `ftps_server` fixture --- pixl_core/tests/conftest.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/pixl_core/tests/conftest.py b/pixl_core/tests/conftest.py index 65a2a581a..a0fd67cbe 100644 --- a/pixl_core/tests/conftest.py +++ b/pixl_core/tests/conftest.py @@ -40,6 +40,11 @@ os.environ["RABBITMQ_PORT"] = "25672" os.environ["PROJECT_CONFIGS_DIR"] = str(TEST_DIR.parents[1] / "project_configs") +os.environ["FTP_HOST"] = "localhost" +os.environ["FTP_USER_NAME"] = "pixl" +os.environ["FTP_PASSWORD"] = "longpassword" # noqa: S105 Hardcoding password +os.environ["FTP_PORT"] = "20021" + @pytest.fixture(scope="package") def run_containers() -> subprocess.CompletedProcess[bytes]: @@ -72,10 +77,10 @@ class MockFTPSUploader(FTPSUploader): def __init__(self) -> None: """Initialise the mock uploader with hardcoded values for FTPS config.""" - self.host = "localhost" - self.user = "pixl" - self.password = "longpassword" # noqa: S105 Hardcoding password - self.port = 20021 + self.host = os.environ["FTP_HOST"] + self.user = os.environ["FTP_USER_NAME"] + self.password = os.environ["FTP_PASSWORD"] + self.port = int(os.environ["FTP_PORT"]) @pytest.fixture() From df89d7731f2cf1ffdaabb035c330f20141e740a1 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Mon, 26 Feb 2024 16:48:09 +0100 Subject: [PATCH 043/120] Fix core tests --- pixl_core/tests/test_upload.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pixl_core/tests/test_upload.py b/pixl_core/tests/test_upload.py index ce8948d76..827d23c2e 100644 --- a/pixl_core/tests/test_upload.py +++ b/pixl_core/tests/test_upload.py @@ -83,7 +83,7 @@ def parquet_export(export_dir) -> ParquetExport: """ return ParquetExport( project_name="i-am-a-project", - extract_datetime=datetime.datetime.now(tz=datetime.timezone.utc), + extract_datetime=datetime.now(tz=timezone.utc), export_dir=export_dir, ) @@ -99,7 +99,8 @@ def test_upload_parquet(parquet_export, ftps_home_dir, ftps_uploader) -> None: parquet_export.export_radiology(pd.DataFrame(list("dummy"), columns=["D"])) # ACT - parquet_export.upload() + ftps_uploader.upload_parquet_files(parquet_export) + # ASSERT expected_public_parquet_dir = ( ftps_home_dir / parquet_export.project_slug / parquet_export.extract_time_slug / "parquet" From 54d3cee7ebda02d41d29ac50aa7bffa052d7d40d Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Mon, 26 Feb 2024 18:04:24 +0100 Subject: [PATCH 044/120] Format docker-compose --- docker-compose.yml | 575 +++++++++++++++++++++++---------------------- 1 file changed, 295 insertions(+), 280 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 65420a398..dcd4e48f5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,311 +20,326 @@ x-http-proxy: &http-proxy ${HTTP_PROXY} x-https-proxy: &https-proxy ${HTTPS_PROXY} x-no-proxy: &no-proxy localhost,0.0.0.0,127.0.0.1,uclvlddpragae07,hasher-api,orthanc-raw x-proxy-common: &proxy-common - HTTP_PROXY: *http-proxy - http_proxy: *http-proxy - HTTPS_PROXY: *https-proxy - https_proxy: *https-proxy - NO_PROXY: *no-proxy - no_proxy: *no-proxy + HTTP_PROXY: *http-proxy + http_proxy: *http-proxy + HTTPS_PROXY: *https-proxy + https_proxy: *https-proxy + NO_PROXY: *no-proxy + no_proxy: *no-proxy x-build-args-common: &build-args-common - <<: [*proxy-common] + <<: [*proxy-common] x-pixl-common-env: &pixl-common-env - ENV: ${ENV} - DEBUG: ${DEBUG} + ENV: ${ENV} + DEBUG: ${DEBUG} x-pixl-rabbit-mq: &pixl-rabbit-mq - RABBITMQ_HOST: "queue" # Name of the queue service - RABBITMQ_PORT: "5672" - RABBITMQ_USERNAME: ${RABBITMQ_USERNAME} - RABBITMQ_PASSWORD: ${RABBITMQ_PASSWORD} + RABBITMQ_HOST: "queue" # Name of the queue service + RABBITMQ_PORT: "5672" + RABBITMQ_USERNAME: ${RABBITMQ_USERNAME} + RABBITMQ_PASSWORD: ${RABBITMQ_PASSWORD} x-emap-db: &emap-db - EMAP_UDS_HOST: ${EMAP_UDS_HOST} - EMAP_UDS_PORT: ${EMAP_UDS_PORT} - EMAP_UDS_NAME: ${EMAP_UDS_NAME} - EMAP_UDS_USER: ${EMAP_UDS_USER} - EMAP_UDS_PASSWORD: ${EMAP_UDS_PASSWORD} - EMAP_UDS_SCHEMA_NAME: ${EMAP_UDS_SCHEMA_NAME} + EMAP_UDS_HOST: ${EMAP_UDS_HOST} + EMAP_UDS_PORT: ${EMAP_UDS_PORT} + EMAP_UDS_NAME: ${EMAP_UDS_NAME} + EMAP_UDS_USER: ${EMAP_UDS_USER} + EMAP_UDS_PASSWORD: ${EMAP_UDS_PASSWORD} + EMAP_UDS_SCHEMA_NAME: ${EMAP_UDS_SCHEMA_NAME} x-pixl-db: &pixl-db - PIXL_DB_HOST: ${PIXL_DB_HOST} - PIXL_DB_PORT: ${PIXL_DB_PORT} - PIXL_DB_USER: ${PIXL_DB_USER} - PIXL_DB_PASSWORD: ${PIXL_DB_PASSWORD} - PIXL_DB_NAME: ${PIXL_DB_NAME} + PIXL_DB_HOST: ${PIXL_DB_HOST} + PIXL_DB_PORT: ${PIXL_DB_PORT} + PIXL_DB_USER: ${PIXL_DB_USER} + PIXL_DB_PASSWORD: ${PIXL_DB_PASSWORD} + PIXL_DB_NAME: ${PIXL_DB_NAME} x-ftp-host: &ftp-host - FTP_HOST: ${FTP_HOST} - FTP_USER_NAME: ${FTP_USER_NAME} - FTP_USER_PASSWORD: ${FTP_USER_PASSWORD} - FTP_PORT: ${FTP_PORT} + FTP_HOST: ${FTP_HOST} + FTP_USER_NAME: ${FTP_USER_NAME} + FTP_USER_PASSWORD: ${FTP_USER_PASSWORD} + FTP_PORT: ${FTP_PORT} x-logs-volume: &logs-volume - type: volume - source: logs - target: /logs + type: volume + source: logs + target: /logs volumes: - logs: - orthanc-anon-data: - orthanc-raw-data: - postgres-data: - exports: + logs: + orthanc-anon-data: + orthanc-raw-data: + postgres-data: + exports: networks: - pixl-net: + pixl-net: ################################################################################ # Services services: + hasher-api: + build: + context: . + dockerfile: ./docker/hasher-api/Dockerfile + args: + <<: *build-args-common + environment: + <<: [*proxy-common, *pixl-common-env] + AZURE_CLIENT_ID: ${HASHER_API_AZ_CLIENT_ID} + AZURE_CLIENT_SECRET: ${HASHER_API_AZ_CLIENT_PASSWORD} + AZURE_TENANT_ID: ${HASHER_API_AZ_TENANT_ID} + AZURE_KEY_VAULT_NAME: ${HASHER_API_AZ_KEY_VAULT_NAME} + AZURE_KEY_VAULT_SECRET_NAME: ${HASHER_API_AZ_KEY_VAULT_SECRET_NAME} + env_file: + - ./docker/common.env + ports: + - "${HASHER_API_PORT}:8000" + volumes: + - *logs-volume + networks: + - pixl-net + healthcheck: + test: ["CMD", "curl", "-f", "http://hasher-api:8000/heart-beat"] + interval: 10s + timeout: 30s + retries: 5 + restart: "no" - hasher-api: - build: - context: . - dockerfile: ./docker/hasher-api/Dockerfile - args: - <<: *build-args-common - environment: - <<: [*proxy-common, *pixl-common-env] - AZURE_CLIENT_ID: ${HASHER_API_AZ_CLIENT_ID} - AZURE_CLIENT_SECRET: ${HASHER_API_AZ_CLIENT_PASSWORD} - AZURE_TENANT_ID: ${HASHER_API_AZ_TENANT_ID} - AZURE_KEY_VAULT_NAME: ${HASHER_API_AZ_KEY_VAULT_NAME} - AZURE_KEY_VAULT_SECRET_NAME: ${HASHER_API_AZ_KEY_VAULT_SECRET_NAME} - env_file: - - ./docker/common.env - ports: - - "${HASHER_API_PORT}:8000" - volumes: - - *logs-volume - networks: - - pixl-net - healthcheck: - test: [ "CMD", "curl", "-f", "http://hasher-api:8000/heart-beat" ] - interval: 10s - timeout: 30s - retries: 5 - restart: "no" + orthanc-anon: + build: + context: . + dockerfile: ./docker/orthanc-anon/Dockerfile + args: + <<: *build-args-common + command: /run/secrets + environment: + <<: [*proxy-common, *pixl-common-env, *ftp-host] + ORTHANC_NAME: "PIXL: Anon" + ORTHANC_USERNAME: ${ORTHANC_ANON_USERNAME} + ORTHANC_PASSWORD: ${ORTHANC_ANON_PASSWORD} + ORTHANC_ANON_AE_TITLE: ${ORTHANC_ANON_AE_TITLE} + ORTHANC_AUTOROUTE_ANON_TO_ENDPOINT: ${ORTHANC_AUTOROUTE_ANON_TO_ENDPOINT} + ORTHANC_RAW_AE_TITLE: ${ORTHANC_RAW_AE_TITLE} + ORTHANC_RAW_DICOM_PORT: "4242" + ORTHANC_RAW_HOSTNAME: "orthanc-raw" + PIXL_DB_HOST: ${PIXL_DB_HOST} + PIXL_DB_PORT: ${PIXL_DB_PORT} + PIXL_DB_NAME: ${PIXL_DB_NAME} + PIXL_DB_USER: ${PIXL_DB_USER} + PIXL_DB_PASSWORD: ${PIXL_DB_PASSWORD} + DICOM_WEB_PLUGIN_ENABLED: ${ENABLE_DICOM_WEB} + HASHER_API_AZ_NAME: "hasher-api" + HASHER_API_PORT: 8000 + HTTP_TIMEOUT: ${ORTHANC_ANON_HTTP_TIMEOUT} + AZ_DICOM_ENDPOINT_NAME: ${AZ_DICOM_ENDPOINT_NAME} + AZ_DICOM_ENDPOINT_URL: ${AZ_DICOM_ENDPOINT_URL} + AZ_DICOM_ENDPOINT_TOKEN: ${AZ_DICOM_ENDPOINT_TOKEN} + AZ_DICOM_ENDPOINT_CLIENT_ID: ${AZ_DICOM_ENDPOINT_CLIENT_ID} + AZ_DICOM_ENDPOINT_CLIENT_SECRET: ${AZ_DICOM_ENDPOINT_CLIENT_SECRET} + AZ_DICOM_ENDPOINT_TENANT_ID: ${AZ_DICOM_ENDPOINT_TENANT_ID} + AZ_DICOM_TOKEN_REFRESH_SECS: "600" + TIME_OFFSET: "${STUDY_TIME_OFFSET}" + SALT_VALUE: ${SALT_VALUE}" + ports: + - "${ORTHANC_ANON_DICOM_PORT}:4242" + - "${ORTHANC_ANON_WEB_PORT}:8042" + volumes: + - type: volume + source: orthanc-anon-data + target: /var/lib/orthanc/db + - ${PWD}/orthanc/orthanc-anon/config:/run/secrets:ro + networks: + - pixl-net + # needed for same reason as ehr-api + extra_hosts: + - "host.docker.internal:host-gateway" + depends_on: + postgres: + condition: service_healthy + healthcheck: + test: + [ + "CMD", + "curl", + "-f", + "-u", + "${ORTHANC_ANON_USERNAME}:${ORTHANC_ANON_PASSWORD}", + "http://orthanc-anon:8042/heart-beat", + ] + interval: 10s + timeout: 30s + retries: 5 + restart: "no" - orthanc-anon: - build: - context: . - dockerfile: ./docker/orthanc-anon/Dockerfile - args: - <<: *build-args-common - command: /run/secrets - environment: - <<: [*proxy-common, *pixl-common-env, *ftp-host] - ORTHANC_NAME: "PIXL: Anon" - ORTHANC_USERNAME: ${ORTHANC_ANON_USERNAME} - ORTHANC_PASSWORD: ${ORTHANC_ANON_PASSWORD} - ORTHANC_ANON_AE_TITLE: ${ORTHANC_ANON_AE_TITLE} - ORTHANC_AUTOROUTE_ANON_TO_ENDPOINT: ${ORTHANC_AUTOROUTE_ANON_TO_ENDPOINT} - ORTHANC_RAW_AE_TITLE: ${ORTHANC_RAW_AE_TITLE} - ORTHANC_RAW_DICOM_PORT: "4242" - ORTHANC_RAW_HOSTNAME: "orthanc-raw" - PIXL_DB_HOST: ${PIXL_DB_HOST} - PIXL_DB_PORT: ${PIXL_DB_PORT} - PIXL_DB_NAME: ${PIXL_DB_NAME} - PIXL_DB_USER: ${PIXL_DB_USER} - PIXL_DB_PASSWORD: ${PIXL_DB_PASSWORD} - DICOM_WEB_PLUGIN_ENABLED: ${ENABLE_DICOM_WEB} - HASHER_API_AZ_NAME: "hasher-api" - HASHER_API_PORT: 8000 - HTTP_TIMEOUT: ${ORTHANC_ANON_HTTP_TIMEOUT} - AZ_DICOM_ENDPOINT_NAME: ${AZ_DICOM_ENDPOINT_NAME} - AZ_DICOM_ENDPOINT_URL: ${AZ_DICOM_ENDPOINT_URL} - AZ_DICOM_ENDPOINT_TOKEN: ${AZ_DICOM_ENDPOINT_TOKEN} - AZ_DICOM_ENDPOINT_CLIENT_ID: ${AZ_DICOM_ENDPOINT_CLIENT_ID} - AZ_DICOM_ENDPOINT_CLIENT_SECRET: ${AZ_DICOM_ENDPOINT_CLIENT_SECRET} - AZ_DICOM_ENDPOINT_TENANT_ID: ${AZ_DICOM_ENDPOINT_TENANT_ID} - AZ_DICOM_TOKEN_REFRESH_SECS: "600" - TIME_OFFSET: "${STUDY_TIME_OFFSET}" - SALT_VALUE: ${SALT_VALUE}" - ports: - - "${ORTHANC_ANON_DICOM_PORT}:4242" - - "${ORTHANC_ANON_WEB_PORT}:8042" - volumes: - - type: volume - source: orthanc-anon-data - target: /var/lib/orthanc/db - - ${PWD}/orthanc/orthanc-anon/config:/run/secrets:ro - networks: - - pixl-net - # needed for same reason as ehr-api - extra_hosts: - - "host.docker.internal:host-gateway" - depends_on: - postgres: - condition: service_healthy - healthcheck: - test: [ "CMD", "curl", "-f", "-u" , "${ORTHANC_ANON_USERNAME}:${ORTHANC_ANON_PASSWORD}", "http://orthanc-anon:8042/heart-beat" ] - interval: 10s - timeout: 30s - retries: 5 - restart: "no" + orthanc-raw: + build: + context: . + dockerfile: ./docker/orthanc-raw/Dockerfile + args: + <<: *build-args-common + ORTHANC_RAW_MAXIMUM_STORAGE_SIZE: ${ORTHANC_RAW_MAXIMUM_STORAGE_SIZE} + command: /run/secrets + environment: + <<: [*pixl-db, *proxy-common, *pixl-common-env] + ORTHANC_NAME: "PIXL: Raw" + ORTHANC_USERNAME: ${ORTHANC_RAW_USERNAME} + ORTHANC_PASSWORD: ${ORTHANC_RAW_PASSWORD} + ORTHANC_RAW_AE_TITLE: ${ORTHANC_RAW_AE_TITLE} + ORTHANC_AUTOROUTE_RAW_TO_ANON: ${ORTHANC_AUTOROUTE_RAW_TO_ANON} + ORTHANC_RAW_MAXIMUM_STORAGE_SIZE: ${ORTHANC_RAW_MAXIMUM_STORAGE_SIZE} + VNAQR_AE_TITLE: ${VNAQR_AE_TITLE} + VNAQR_DICOM_PORT: ${VNAQR_DICOM_PORT} + VNAQR_IP_ADDR: ${VNAQR_IP_ADDR} + ORTHANC_ANON_AE_TITLE: ${ORTHANC_ANON_AE_TITLE} + ORTHANC_ANON_DICOM_PORT: "4242" + ORTHANC_ANON_HOSTNAME: "orthanc-anon" + ports: + - "${ORTHANC_RAW_DICOM_PORT}:4242" + - "${ORTHANC_RAW_WEB_PORT}:8042" + volumes: + - type: volume + source: orthanc-raw-data + target: /var/lib/orthanc/db + networks: + - pixl-net + depends_on: + postgres: + condition: service_healthy + orthanc-anon: + condition: service_started + healthcheck: + test: + [ + "CMD", + "curl", + "-f", + "-u", + "${ORTHANC_RAW_USERNAME}:${ORTHANC_RAW_PASSWORD}", + "http://orthanc-raw:8042/heart-beat", + ] + interval: 10s + timeout: 30s + retries: 5 + restart: "no" - orthanc-raw: - build: - context: . - dockerfile: ./docker/orthanc-raw/Dockerfile - args: - <<: *build-args-common - ORTHANC_RAW_MAXIMUM_STORAGE_SIZE: ${ORTHANC_RAW_MAXIMUM_STORAGE_SIZE} - command: /run/secrets - environment: - <<: [*pixl-db, *proxy-common, *pixl-common-env] - ORTHANC_NAME: "PIXL: Raw" - ORTHANC_USERNAME: ${ORTHANC_RAW_USERNAME} - ORTHANC_PASSWORD: ${ORTHANC_RAW_PASSWORD} - ORTHANC_RAW_AE_TITLE: ${ORTHANC_RAW_AE_TITLE} - ORTHANC_AUTOROUTE_RAW_TO_ANON: ${ORTHANC_AUTOROUTE_RAW_TO_ANON} - ORTHANC_RAW_MAXIMUM_STORAGE_SIZE: ${ORTHANC_RAW_MAXIMUM_STORAGE_SIZE} - VNAQR_AE_TITLE : ${VNAQR_AE_TITLE} - VNAQR_DICOM_PORT: ${VNAQR_DICOM_PORT} - VNAQR_IP_ADDR: ${VNAQR_IP_ADDR} - ORTHANC_ANON_AE_TITLE: ${ORTHANC_ANON_AE_TITLE} - ORTHANC_ANON_DICOM_PORT: "4242" - ORTHANC_ANON_HOSTNAME: "orthanc-anon" - ports: - - "${ORTHANC_RAW_DICOM_PORT}:4242" - - "${ORTHANC_RAW_WEB_PORT}:8042" - volumes: - - type: volume - source: orthanc-raw-data - target: /var/lib/orthanc/db - networks: - - pixl-net - depends_on: - postgres: - condition: service_healthy - orthanc-anon: - condition: service_started - healthcheck: - test: [ "CMD", "curl", "-f", "-u" , "${ORTHANC_RAW_USERNAME}:${ORTHANC_RAW_PASSWORD}", "http://orthanc-raw:8042/heart-beat" ] - interval: 10s - timeout: 30s - retries: 5 - restart: "no" + queue: + image: rabbitmq:3.12.9-management + environment: + RABBITMQ_DEFAULT_USER: ${RABBITMQ_USERNAME} + RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD} + healthcheck: + test: rabbitmq-diagnostics -q check_running + interval: 30s + timeout: 30s + retries: 3 + ports: + - "${RABBITMQ_PORT}:5672" + - "${RABBITMQ_ADMIN_PORT}:15672" + networks: + - pixl-net - queue: - image: rabbitmq:3.12.9-management - environment: - RABBITMQ_DEFAULT_USER: ${RABBITMQ_USERNAME} - RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD} - healthcheck: - test: rabbitmq-diagnostics -q check_running - interval: 30s - timeout: 30s - retries: 3 - ports: - - "${RABBITMQ_PORT}:5672" - - "${RABBITMQ_ADMIN_PORT}:15672" - networks: - - pixl-net + ehr-api: + build: + context: . + dockerfile: ./docker/ehr-api/Dockerfile + args: + <<: *build-args-common + environment: + <<: [*pixl-db, *emap-db, *proxy-common, *pixl-common-env, *pixl-rabbit-mq, *ftp-host] + AZURE_CLIENT_ID: ${PIXL_EHR_API_AZ_CLIENT_ID} + AZURE_CLIENT_SECRET: ${PIXL_EHR_API_AZ_CLIENT_SECRET} + AZURE_TENANT_ID: ${PIXL_EHR_API_AZ_TENANT_ID} + AZ_STORAGE_ACCOUNT_NAME: ${PIXL_EHR_API_AZ_STORAGE_ACCOUNT_NAME} + AZ_STORAGE_CONTAINER_NAME: ${PIXL_EHR_API_AZ_STORAGE_CONTAINER_NAME} + COGSTACK_REDACT_URL: ${PIXL_EHR_COGSTACK_REDACT_URL} + env_file: + - ./docker/common.env + depends_on: + queue: + condition: service_healthy + postgres: + condition: service_healthy + hasher-api: + condition: service_healthy + ports: + - "${PIXL_EHR_API_PORT}:8000" + healthcheck: + interval: 10s + timeout: 30s + retries: 5 + networks: + - pixl-net + # needed for testing under GHA (linux), so this container + # can reach the test FTP server running on the docker host + extra_hosts: + - "host.docker.internal:host-gateway" + volumes: + - ${PWD}/exports:/run/exports - ehr-api: - build: - context: . - dockerfile: ./docker/ehr-api/Dockerfile - args: - <<: *build-args-common - environment: - <<: [*pixl-db, *emap-db, *proxy-common, *pixl-common-env, *pixl-rabbit-mq, *ftp-host] - AZURE_CLIENT_ID: ${PIXL_EHR_API_AZ_CLIENT_ID} - AZURE_CLIENT_SECRET: ${PIXL_EHR_API_AZ_CLIENT_SECRET} - AZURE_TENANT_ID: ${PIXL_EHR_API_AZ_TENANT_ID} - AZ_STORAGE_ACCOUNT_NAME: ${PIXL_EHR_API_AZ_STORAGE_ACCOUNT_NAME} - AZ_STORAGE_CONTAINER_NAME: ${PIXL_EHR_API_AZ_STORAGE_CONTAINER_NAME} - COGSTACK_REDACT_URL: ${PIXL_EHR_COGSTACK_REDACT_URL} - env_file: - - ./docker/common.env - depends_on: - queue: - condition: service_healthy - postgres: - condition: service_healthy - hasher-api: - condition: service_healthy - ports: - - "${PIXL_EHR_API_PORT}:8000" - healthcheck: - interval: 10s - timeout: 30s - retries: 5 - networks: - - pixl-net - # needed for testing under GHA (linux), so this container - # can reach the test FTP server running on the docker host - extra_hosts: - - "host.docker.internal:host-gateway" - volumes: - - ${PWD}/exports:/run/exports + imaging-api: + build: + context: . + dockerfile: ./docker/imaging-api/Dockerfile + args: + <<: *build-args-common + depends_on: + queue: + condition: service_healthy + orthanc-raw: + condition: service_healthy + healthcheck: + test: curl -f http://0.0.0.0:8000/heart-beat + interval: 10s + timeout: 30s + retries: 5 + networks: + - pixl-net + environment: + <<: [*pixl-rabbit-mq, *proxy-common] + ORTHANC_RAW_USERNAME: ${ORTHANC_RAW_USERNAME} + ORTHANC_RAW_PASSWORD: ${ORTHANC_RAW_PASSWORD} + ORTHANC_RAW_AE_TITLE: ${ORTHANC_RAW_AE_TITLE} + VNAQR_MODALITY: ${VNAQR_MODALITY} + SKIP_ALEMBIC: ${SKIP_ALEMBIC} + PIXL_DB_HOST: ${PIXL_DB_HOST} + PIXL_DB_PORT: ${PIXL_DB_PORT} + PIXL_DB_NAME: ${PIXL_DB_NAME} + PIXL_DB_USER: ${PIXL_DB_USER} + PIXL_DB_PASSWORD: ${PIXL_DB_PASSWORD} + PIXL_DICOM_TRANSFER_TIMEOUT: ${PIXL_DICOM_TRANSFER_TIMEOUT} + PIXL_QUERY_TIMEOUT: ${PIXL_QUERY_TIMEOUT} + ports: + - "${PIXL_IMAGING_API_PORT}:8000" - imaging-api: - build: - context: . - dockerfile: ./docker/imaging-api/Dockerfile - args: - <<: *build-args-common - depends_on: - queue: - condition: service_healthy - orthanc-raw: - condition: service_healthy - healthcheck: - test: curl -f http://0.0.0.0:8000/heart-beat - interval: 10s - timeout: 30s - retries: 5 - networks: - - pixl-net - environment: - <<: [*pixl-rabbit-mq, *proxy-common] - ORTHANC_RAW_USERNAME: ${ORTHANC_RAW_USERNAME} - ORTHANC_RAW_PASSWORD: ${ORTHANC_RAW_PASSWORD} - ORTHANC_RAW_AE_TITLE: ${ORTHANC_RAW_AE_TITLE} - VNAQR_MODALITY: ${VNAQR_MODALITY} - SKIP_ALEMBIC: ${SKIP_ALEMBIC} - PIXL_DB_HOST: ${PIXL_DB_HOST} - PIXL_DB_PORT: ${PIXL_DB_PORT} - PIXL_DB_NAME: ${PIXL_DB_NAME} - PIXL_DB_USER: ${PIXL_DB_USER} - PIXL_DB_PASSWORD: ${PIXL_DB_PASSWORD} - PIXL_DICOM_TRANSFER_TIMEOUT: ${PIXL_DICOM_TRANSFER_TIMEOUT} - PIXL_QUERY_TIMEOUT: ${PIXL_QUERY_TIMEOUT} - ports: - - "${PIXL_IMAGING_API_PORT}:8000" - -################################################################################ -# Data Stores - postgres: - build: - context: . - dockerfile: ./docker/postgres/Dockerfile - args: - <<: *build-args-common - environment: - POSTGRES_USER: ${PIXL_DB_USER} - POSTGRES_PASSWORD: ${PIXL_DB_PASSWORD} - POSTGRES_DB: ${PIXL_DB_NAME} - PGTZ: Europe/London - env_file: - - ./docker/common.env - command: postgres -c 'config_file=/etc/postgresql/postgresql.conf' - volumes: - - type: volume - source: postgres-data - target: /var/lib/postgresql/data - ports: - - "${POSTGRES_PORT}:5432" - healthcheck: - test: [ "CMD", "pg_isready", "-U", "${PIXL_DB_USER}" ] - interval: 10s - timeout: 30s - retries: 5 - restart: always - networks: - - pixl-net + ################################################################################ + # Data Stores + postgres: + build: + context: . + dockerfile: ./docker/postgres/Dockerfile + args: + <<: *build-args-common + environment: + POSTGRES_USER: ${PIXL_DB_USER} + POSTGRES_PASSWORD: ${PIXL_DB_PASSWORD} + POSTGRES_DB: ${PIXL_DB_NAME} + PGTZ: Europe/London + env_file: + - ./docker/common.env + command: postgres -c 'config_file=/etc/postgresql/postgresql.conf' + volumes: + - type: volume + source: postgres-data + target: /var/lib/postgresql/data + ports: + - "${POSTGRES_PORT}:5432" + healthcheck: + test: ["CMD", "pg_isready", "-U", "${PIXL_DB_USER}"] + interval: 10s + timeout: 30s + retries: 5 + restart: always + networks: + - pixl-net From 3244b570802dd4a7b95d4630624c1219102171ec Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Mon, 26 Feb 2024 18:23:33 +0100 Subject: [PATCH 045/120] Update cli docs Running tests no longer requires the extra script. --- cli/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cli/README.md b/cli/README.md index 3e6d5b57a..64b2259cf 100644 --- a/cli/README.md +++ b/cli/README.md @@ -114,9 +114,9 @@ pip install -e ../pixl_core/ -e .[test] ### Running tests The CLI tests require a running instance of the `rabbitmq` service, for which we provide a -`docker-compose` [file](./tests/docker-compose.yml). Spinning up the service and running `pytest` -can be done by running +`docker-compose` [file](./tests/docker-compose.yml). The service is automatically started by the +`run_containers` _pytest_ fixture. So to run the tests, simply run ```bash -./tests/run-tests.sh +pytest ``` From 52388cb904f457669628a7d1086a5262e4e4fcc8 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Mon, 26 Feb 2024 19:40:57 +0100 Subject: [PATCH 046/120] Fix: keyvault name envvar --- pixl_core/src/core/_secrets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pixl_core/src/core/_secrets.py b/pixl_core/src/core/_secrets.py index 38e38bf5c..7dff41234 100644 --- a/pixl_core/src/core/_secrets.py +++ b/pixl_core/src/core/_secrets.py @@ -39,7 +39,7 @@ def __init__(self) -> None: - AZURE_KEY_VAULT_NAME """ self._check_envvars() - self.kv_name = config("EXPORT_AZ_KEY_VAULT_NAME") + self.kv_name = config("AZURE_KEY_VAULT_NAME") self.client = self._connect_to_keyvault() def _connect_to_keyvault(self) -> SecretClient: From 155d60ab27429e1c3173c60963f66c70495b8483 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Mon, 26 Feb 2024 19:41:41 +0100 Subject: [PATCH 047/120] Load local `.env` if it exists before setting `PROJECT_CONFIGS_DIR` --- pixl_core/src/core/project_config.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pixl_core/src/core/project_config.py b/pixl_core/src/core/project_config.py index 1fa68c8e2..378f7b2a0 100644 --- a/pixl_core/src/core/project_config.py +++ b/pixl_core/src/core/project_config.py @@ -21,9 +21,12 @@ from typing import Any import yaml -from decouple import config +from decouple import Config, RepositoryEmpty, RepositoryEnv from pydantic import BaseModel, validator +# Make sure local .env file is loaded if it exists +env_file = Path.cwd() / ".env" +config = Config(RepositoryEnv(env_file)) if env_file.exists() else Config(RepositoryEmpty()) PROJECT_CONFIGS_DIR = Path(config("PROJECT_CONFIGS_DIR")) logger = logging.getLogger(__name__) From ad758b6a30277e99a3f6fb32f2d47bf4355f5555 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Mon, 26 Feb 2024 19:41:54 +0100 Subject: [PATCH 048/120] Improve logging --- pixl_core/src/core/exports.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/pixl_core/src/core/exports.py b/pixl_core/src/core/exports.py index d74e48a6c..624e22a32 100644 --- a/pixl_core/src/core/exports.py +++ b/pixl_core/src/core/exports.py @@ -30,7 +30,7 @@ import pandas as pd -logger = logging.getLogger(__file__) +logger = logging.getLogger(__name__) class ParquetExport: @@ -83,7 +83,7 @@ def copy_to_exports(self, input_omop_dir: pathlib.Path) -> str: """ public_input = input_omop_dir / "public" - logging.info("Copying public parquet files from %s to %s", public_input, self.public_output) + logger.info("Copying public parquet files from %s to %s", public_input, self.public_output) # Make directory for exports if they don't exist ParquetExport._mkdir(self.public_output) @@ -100,7 +100,7 @@ def export_radiology(self, export_df: pd.DataFrame) -> pathlib.Path: """Export radiology reports to parquet file""" self._mkdir(self.radiology_output) parquet_file = self.radiology_output / "radiology.parquet" - logging.info("Exporting radiology to %s", self.radiology_output) + logger.info("Exporting radiology to %s", self.radiology_output) export_df.to_parquet(parquet_file) # We are not responsible for making the "latest" symlink, see `copy_to_exports`. # This avoids the confusion caused by EHR API (which calls export_radiology) having a @@ -118,8 +118,15 @@ def upload(self) -> None: """Upload the latest extract to the DSH.""" project_config = load_project_config(self.project_slug) destination = project_config.destination.parquet + if destination == "ftps": - FTPSUploader(self.project_slug).upload_parquet_files(self) + ftps_uploader = FTPSUploader(self.project_slug) + msg = ( + f"Uploading parquet files for project {self.project_slug} to FTPS to host" + f"{ftps_uploader.host} via port {ftps_uploader.port}" + ) + logger.warning(msg) + ftps_uploader.upload_parquet_files(self) else: msg = f"Destination {destination} for parquet files not supported" raise ValueError(msg) From cb0c2f319ebd28da904e4dfa2040509a35a7f266 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Mon, 26 Feb 2024 20:02:25 +0100 Subject: [PATCH 049/120] Move project configs in top-level `projects/` directory --- cli/tests/conftest.py | 2 +- docker-compose.yml | 8 ++++---- pixl_core/tests/conftest.py | 2 +- pixl_dcmd/tests/conftest.py | 2 +- pixl_dcmd/tests/test_main.py | 4 ++-- pixl_ehr/tests/conftest.py | 2 +- .../test-extract-uclh-omop-cdm-tag-operations.yaml | 0 ...tube-project-ngt-only-full-dataset-tag-operations.yaml | 0 .../configs}/test-extract-uclh-omop-cdm.yaml | 0 ...lh-nasogastric-tube-project-ngt-only-full-dataset.yaml | 0 test/.env | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) rename {project_configs => projects/configs}/tag-operations/test-extract-uclh-omop-cdm-tag-operations.yaml (100%) rename {project_configs => projects/configs}/tag-operations/uclh-nasogastric-tube-project-ngt-only-full-dataset-tag-operations.yaml (100%) rename {project_configs => projects/configs}/test-extract-uclh-omop-cdm.yaml (100%) rename {project_configs => projects/configs}/uclh-nasogastric-tube-project-ngt-only-full-dataset.yaml (100%) diff --git a/cli/tests/conftest.py b/cli/tests/conftest.py index 6b187abd6..947807e10 100644 --- a/cli/tests/conftest.py +++ b/cli/tests/conftest.py @@ -22,7 +22,7 @@ from sqlalchemy import Engine, create_engine from sqlalchemy.orm import Session, sessionmaker -os.environ["PROJECT_CONFIGS_DIR"] = str(pathlib.Path(__file__).parents[2] / "project_configs") +os.environ["PROJECT_CONFIGS_DIR"] = str(pathlib.Path(__file__).parents[2] / "projects/configs") # Set the necessary environment variables os.environ["PIXL_EHR_API_HOST"] = "localhost" diff --git a/docker-compose.yml b/docker-compose.yml index a542b13cd..64f64d1ff 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -148,7 +148,7 @@ services: AZ_DICOM_TOKEN_REFRESH_SECS: "600" TIME_OFFSET: "${STUDY_TIME_OFFSET}" SALT_VALUE: ${SALT_VALUE}" - PROJECT_CONFIGS_DIR: ${PROJECT_CONFIGS_DIR:-/project_configs} + PROJECT_CONFIGS_DIR: ${PROJECT_CONFIGS_DIR:-/projects/configs} ports: - "${ORTHANC_ANON_DICOM_PORT}:4242" - "${ORTHANC_ANON_WEB_PORT}:8042" @@ -157,7 +157,7 @@ services: source: orthanc-anon-data target: /var/lib/orthanc/db - ${PWD}/orthanc/orthanc-anon/config:/run/secrets:ro - - ${PWD}/project_configs:${PROJECT_CONFIGS_DIR:-/project_configs}:ro + - ${PWD}/projects/configs:${PROJECT_CONFIGS_DIR:-/projects/configs}:ro networks: - pixl-net # needed for same reason as ehr-api @@ -268,7 +268,7 @@ services: AZ_STORAGE_ACCOUNT_NAME: ${PIXL_EHR_API_AZ_STORAGE_ACCOUNT_NAME} AZ_STORAGE_CONTAINER_NAME: ${PIXL_EHR_API_AZ_STORAGE_CONTAINER_NAME} COGSTACK_REDACT_URL: ${PIXL_EHR_COGSTACK_REDACT_URL} - PROJECT_CONFIGS_DIR: ${PROJECT_CONFIGS_DIR:-/project_configs} + PROJECT_CONFIGS_DIR: ${PROJECT_CONFIGS_DIR:-/projects/configs} env_file: - ./docker/common.env depends_on: @@ -292,7 +292,7 @@ services: - "host.docker.internal:host-gateway" volumes: - ${PWD}/exports:/run/exports - - ${PWD}/project_configs:${PROJECT_CONFIGS_DIR:-/project_configs}:ro + - ${PWD}/projects/configs:${PROJECT_CONFIGS_DIR:-/projects/configs}:ro imaging-api: build: diff --git a/pixl_core/tests/conftest.py b/pixl_core/tests/conftest.py index f876ce991..3a73ec8b1 100644 --- a/pixl_core/tests/conftest.py +++ b/pixl_core/tests/conftest.py @@ -41,7 +41,7 @@ os.environ["RABBITMQ_PASSWORD"] = "guest" # noqa: S105 Hardcoding password os.environ["RABBITMQ_HOST"] = "localhost" os.environ["RABBITMQ_PORT"] = "25672" -os.environ["PROJECT_CONFIGS_DIR"] = str(TEST_DIR.parents[1] / "project_configs") +os.environ["PROJECT_CONFIGS_DIR"] = str(TEST_DIR.parents[1] / "projects/configs") os.environ["FTP_HOST"] = "localhost" os.environ["FTP_USER_NAME"] = "pixl" diff --git a/pixl_dcmd/tests/conftest.py b/pixl_dcmd/tests/conftest.py index d52e7429e..baa1a4b0d 100644 --- a/pixl_dcmd/tests/conftest.py +++ b/pixl_dcmd/tests/conftest.py @@ -29,7 +29,7 @@ os.environ["HASHER_API_PORT"] = "test_hash_API_port" os.environ["TIME_OFFSET"] = "5" os.environ["PROJECT_CONFIGS_DIR"] = str( - pathlib.Path(__file__).parents[2] / "project_configs" + pathlib.Path(__file__).parents[2] / "projects/configs" ) STUDY_DATE = datetime.date.fromisoformat("2023-01-01") diff --git a/pixl_dcmd/tests/test_main.py b/pixl_dcmd/tests/test_main.py index 329ce599d..d03f3e7f2 100644 --- a/pixl_dcmd/tests/test_main.py +++ b/pixl_dcmd/tests/test_main.py @@ -37,7 +37,7 @@ def tag_scheme() -> dict: """Read the tag scheme from orthanc raw.""" tag_file = ( pathlib.Path(__file__).parents[2] - / "project_configs/tag-operations/test-extract-uclh-omop-cdm-tag-operations.yaml" + / "projects/configs/tag-operations/test-extract-uclh-omop-cdm-tag-operations.yaml" ) return yaml.safe_load(tag_file.read_text()) @@ -140,6 +140,6 @@ def test_can_nifti_convert_post_anonymisation( def test_merge_tag_schemes_single_file(): tag_ops_file = ( pathlib.Path(__file__).parents[2] - / "project_configs/tag-operations/test-extract-uclh-omop-cdm-tag-operations.yaml" + / "projects/configs/tag-operations/test-extract-uclh-omop-cdm-tag-operations.yaml" ) merge_tag_schemes([tag_ops_file]) diff --git a/pixl_ehr/tests/conftest.py b/pixl_ehr/tests/conftest.py index 6cc6e5185..8bce49bc2 100644 --- a/pixl_ehr/tests/conftest.py +++ b/pixl_ehr/tests/conftest.py @@ -39,7 +39,7 @@ os.environ["EMAP_UDS_PASSWORD"] = "postgres" # noqa: S105 os.environ["EMAP_UDS_SCHEMA_NAME"] = "star" os.environ["COGSTACK_REDACT_URL"] = "test" -os.environ["PROJECT_CONFIGS_DIR"] = str(Path(__file__).parents[2] / "project_configs") +os.environ["PROJECT_CONFIGS_DIR"] = str(Path(__file__).parents[2] / "projects/configs") TEST_DIR = Path(__file__).parent diff --git a/project_configs/tag-operations/test-extract-uclh-omop-cdm-tag-operations.yaml b/projects/configs/tag-operations/test-extract-uclh-omop-cdm-tag-operations.yaml similarity index 100% rename from project_configs/tag-operations/test-extract-uclh-omop-cdm-tag-operations.yaml rename to projects/configs/tag-operations/test-extract-uclh-omop-cdm-tag-operations.yaml diff --git a/project_configs/tag-operations/uclh-nasogastric-tube-project-ngt-only-full-dataset-tag-operations.yaml b/projects/configs/tag-operations/uclh-nasogastric-tube-project-ngt-only-full-dataset-tag-operations.yaml similarity index 100% rename from project_configs/tag-operations/uclh-nasogastric-tube-project-ngt-only-full-dataset-tag-operations.yaml rename to projects/configs/tag-operations/uclh-nasogastric-tube-project-ngt-only-full-dataset-tag-operations.yaml diff --git a/project_configs/test-extract-uclh-omop-cdm.yaml b/projects/configs/test-extract-uclh-omop-cdm.yaml similarity index 100% rename from project_configs/test-extract-uclh-omop-cdm.yaml rename to projects/configs/test-extract-uclh-omop-cdm.yaml diff --git a/project_configs/uclh-nasogastric-tube-project-ngt-only-full-dataset.yaml b/projects/configs/uclh-nasogastric-tube-project-ngt-only-full-dataset.yaml similarity index 100% rename from project_configs/uclh-nasogastric-tube-project-ngt-only-full-dataset.yaml rename to projects/configs/uclh-nasogastric-tube-project-ngt-only-full-dataset.yaml diff --git a/test/.env b/test/.env index 7c66eeb76..f8cb3851d 100644 --- a/test/.env +++ b/test/.env @@ -73,7 +73,7 @@ RABBITMQ_USERNAME=rabbitmq_username RABBITMQ_PASSWORD=rabbitmq_password # Project configs directory -PROJECT_CONFIGS_DIR=/project_configs +PROJECT_CONFIGS_DIR=/projects/configs # FTP server FTP_HOST=host.docker.internal From 0de653a539f6b04879d89b0064e863db3f384d9f Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Mon, 26 Feb 2024 20:07:27 +0100 Subject: [PATCH 050/120] Move exports dir in top-level projects --- docker-compose.yml | 2 +- pixl_ehr/src/pixl_ehr/main.py | 2 +- {exports => projects/exports}/.gitkeep | 0 test/conftest.py | 2 +- test/test_radiology_parquet.py | 2 +- 5 files changed, 4 insertions(+), 4 deletions(-) rename {exports => projects/exports}/.gitkeep (100%) diff --git a/docker-compose.yml b/docker-compose.yml index 64f64d1ff..ce6eff987 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -291,7 +291,7 @@ services: extra_hosts: - "host.docker.internal:host-gateway" volumes: - - ${PWD}/exports:/run/exports + - ${PWD}/projects/exports:/run/projects/exports - ${PWD}/projects/configs:${PROJECT_CONFIGS_DIR:-/projects/configs}:ro imaging-api: diff --git a/pixl_ehr/src/pixl_ehr/main.py b/pixl_ehr/src/pixl_ehr/main.py index 521ebb488..2f2213b6f 100644 --- a/pixl_ehr/src/pixl_ehr/main.py +++ b/pixl_ehr/src/pixl_ehr/main.py @@ -66,7 +66,7 @@ async def startup_event() -> None: # Export root dir from inside the EHR container. # For the view from outside, see pixl_cli/_io.py: HOST_EXPORT_ROOT_DIR -EHR_EXPORT_ROOT_DIR = Path("/run/exports") +EHR_EXPORT_ROOT_DIR = Path("/run/projects/exports") class ExportRadiologyData(BaseModel): diff --git a/exports/.gitkeep b/projects/exports/.gitkeep similarity index 100% rename from exports/.gitkeep rename to projects/exports/.gitkeep diff --git a/test/conftest.py b/test/conftest.py index e1309391f..262292737 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -49,7 +49,7 @@ def _setup_pixl_cli(ftps_server) -> None: "system-test-ehr-api-1", "rm", "-r", - "/run/exports/test-extract-uclh-omop-cdm/", + "/run/projects/exports/test-extract-uclh-omop-cdm/", ], TEST_DIR, ) diff --git a/test/test_radiology_parquet.py b/test/test_radiology_parquet.py index 84f722269..a304b7344 100644 --- a/test/test_radiology_parquet.py +++ b/test/test_radiology_parquet.py @@ -26,7 +26,7 @@ def test_radiology_parquet(host_export_root_dir: Path): """ From: scripts/test_radiology_parquet.py \ - ../exports/test-extract-uclh-omop-cdm/latest/radiology/radiology.parquet + ../projects/exports/test-extract-uclh-omop-cdm/latest/radiology/radiology.parquet Test contents of radiology report parquet file in the export location """ expected_radiology_parquet_file = ( From 1f580282cfcf3a5ef1bc6964e72efc6e7bb3d054 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Mon, 26 Feb 2024 20:24:30 +0100 Subject: [PATCH 051/120] Allow setting an alias for Azure KV secret fetching --- orthanc/orthanc-anon/plugin/pixl.py | 2 +- pixl_core/src/core/exports.py | 2 +- pixl_core/src/core/project_config.py | 3 ++- pixl_core/src/core/upload.py | 19 ++++++++++++------- .../configs/test-extract-uclh-omop-cdm.yaml | 1 + 5 files changed, 17 insertions(+), 10 deletions(-) diff --git a/orthanc/orthanc-anon/plugin/pixl.py b/orthanc/orthanc-anon/plugin/pixl.py index b10fd7701..f66b6b3c8 100644 --- a/orthanc/orthanc-anon/plugin/pixl.py +++ b/orthanc/orthanc-anon/plugin/pixl.py @@ -150,7 +150,7 @@ def Send(resourceId: str) -> None: logger.debug(msg) zip_content = _get_study_zip_archive(resourceId) - upload.FTPSUploader(slug).upload_dicom_image(zip_content, hashed_patient_id) + upload.FTPSUploader(project_config).upload_dicom_image(zip_content, hashed_patient_id) else: msg = f"Invalid destination: {project_config.destination.dicom}" raise ValueError(msg) diff --git a/pixl_core/src/core/exports.py b/pixl_core/src/core/exports.py index 624e22a32..4d9428177 100644 --- a/pixl_core/src/core/exports.py +++ b/pixl_core/src/core/exports.py @@ -120,7 +120,7 @@ def upload(self) -> None: destination = project_config.destination.parquet if destination == "ftps": - ftps_uploader = FTPSUploader(self.project_slug) + ftps_uploader = FTPSUploader(project_config) msg = ( f"Uploading parquet files for project {self.project_slug} to FTPS to host" f"{ftps_uploader.host} via port {ftps_uploader.port}" diff --git a/pixl_core/src/core/project_config.py b/pixl_core/src/core/project_config.py index 378f7b2a0..b7f515f27 100644 --- a/pixl_core/src/core/project_config.py +++ b/pixl_core/src/core/project_config.py @@ -18,7 +18,7 @@ import logging from enum import Enum from pathlib import Path -from typing import Any +from typing import Any, Optional import yaml from decouple import Config, RepositoryEmpty, RepositoryEnv @@ -55,6 +55,7 @@ def _load_project_config(filename: Path) -> PixlConfig | Any: class _Project(BaseModel): name: str + azure_kv_alias: Optional[str] modalities: list[str] diff --git a/pixl_core/src/core/upload.py b/pixl_core/src/core/upload.py index f1f5a1ebf..100b572a7 100644 --- a/pixl_core/src/core/upload.py +++ b/pixl_core/src/core/upload.py @@ -26,6 +26,7 @@ if TYPE_CHECKING: from core.exports import ParquetExport + from core.project_config import PixlConfig from core._secrets import AzureKeyVault @@ -43,7 +44,7 @@ class Uploader(ABC): """Upload strategy interface.""" @abstractmethod - def __init__(self, project: str) -> None: + def __init__(self, project_config: PixlConfig) -> None: """ Initialise the uploader for a specific project with the destination configuration and an AzureKeyvault instance. The keyvault is used to fetch the secrets required to connect to @@ -52,22 +53,26 @@ def __init__(self, project: str) -> None: :param project: The project name for which the uploader is being initialised. Used to fetch the correct secrets from the keyvault. """ - self.project = project + self.project_config = project_config self.keyvault = AzureKeyVault() class FTPSUploader(Uploader): """Upload strategy for an FTPS server.""" - def __init__(self, project: str) -> None: + def __init__(self, project_config: PixlConfig) -> None: """Initialise the uploader with the destination configuration.""" - Uploader.__init__(self, project) + Uploader.__init__(self, project_config) self._set_config() def _set_config(self) -> None: - self.host = self.keyvault.fetch_secret(f"{self.project}--ftp--host") - self.user = self.keyvault.fetch_secret(f"{self.project}--ftp--username") - self.password = self.keyvault.fetch_secret(f"{self.project}--ftp--password") + # Use the Azure KV alias as prefix if it exists, otherwise use the project name + az_prefix = self.project_config.project.azure_kv_alias + az_prefix = az_prefix if az_prefix else self.project_config.project.name + + self.host = self.keyvault.fetch_secret(f"{az_prefix}--ftp--host") + self.user = self.keyvault.fetch_secret(f"{az_prefix}--ftp--username") + self.password = self.keyvault.fetch_secret(f"{az_prefix}--ftp--password") self.port = config("FTP_PORT", default=21, cast=int) def upload_dicom_image(self, zip_content: BinaryIO, pseudo_anon_id: str) -> None: diff --git a/projects/configs/test-extract-uclh-omop-cdm.yaml b/projects/configs/test-extract-uclh-omop-cdm.yaml index eebdea178..3d3773e71 100644 --- a/projects/configs/test-extract-uclh-omop-cdm.yaml +++ b/projects/configs/test-extract-uclh-omop-cdm.yaml @@ -14,6 +14,7 @@ project: name: "test-extract-uclh-omop-cdm" + azure_kv_alias: "test" modalities: ["DX", "CR"] tag_operation_files: ["test-extract-uclh-omop-cdm-tag-operations.yaml"] From 93a13dc990398316916badee822074dd71cc40ea Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Mon, 26 Feb 2024 21:46:04 +0100 Subject: [PATCH 052/120] Remove debugging message --- pixl_core/src/core/exports.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pixl_core/src/core/exports.py b/pixl_core/src/core/exports.py index 4d9428177..11c9f27b4 100644 --- a/pixl_core/src/core/exports.py +++ b/pixl_core/src/core/exports.py @@ -121,10 +121,7 @@ def upload(self) -> None: if destination == "ftps": ftps_uploader = FTPSUploader(project_config) - msg = ( - f"Uploading parquet files for project {self.project_slug} to FTPS to host" - f"{ftps_uploader.host} via port {ftps_uploader.port}" - ) + msg = f"Uploading parquet files for project {self.project_slug} via FTPS" logger.warning(msg) ftps_uploader.upload_parquet_files(self) else: From 20617f7784ab5b0b2bab7cddc667c001de0e106e Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Mon, 26 Feb 2024 22:05:28 +0100 Subject: [PATCH 053/120] Set Azure Keyvault credentials as secrets in CI --- .github/workflows/main.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 880127f7f..6c196804d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -223,6 +223,18 @@ jobs: pip install -e cli/[test] pip install -e pytest-pixl/ + - name: Create .secrets.env + env: + EXPORT_AZ_CLIENT_ID: ${{ secrets.EXPORT_AZ_CLIENT_ID }} + EXPORT_AZ_CLIENT_PASSWORD: ${{ secrets.EXPORT_AZ_CLIENT_PASSWORD }} + EXPORT_AZ_TENANT_ID: ${{ secrets.EXPORT_AZ_TENANT_ID }} + EXPORT_AZ_KEY_VAULT_NAME: ${{ secrets.EXPORT_AZ_KEY_VAULT_NAME }} + run: | + echo EXPORT_AZ_CLIENT_ID=$EXPORT_AZ_CLIENT_ID >> .secrets.env + echo EXPORT_AZ_CLIENT_PASSWORD=$EXPORT_AZ_CLIENT_PASSWORD >> .secrets.env + echo EXPORT_AZ_TENANT_ID=$EXPORT_AZ_TENANT_ID >> .secrets.env + echo EXPORT_AZ_KEY_VAULT_NAME=$EXPORT_AZ_KEY_VAULT_NAME >> .secrets.env + - name: Build test services working-directory: test run: | From 1dc640c64f84d5d8ae3a01efc4a35c42884c09dc Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Mon, 26 Feb 2024 22:21:22 +0100 Subject: [PATCH 054/120] Add Azure keyvault setup instructions Copied from the Hasher API docs --- README.md | 96 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 91 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index fcf6cd6e0..a61edd306 100644 --- a/README.md +++ b/README.md @@ -228,14 +228,100 @@ The configuration file defines: #### Project secrets {#project-secrets} -Any credentials required for uploading the project's results should be stored in an **Azure Key Vault**. +Any credentials required for uploading the project's results should be stored in an **Azure Key Vault** +(set up instructions below). PIXL will query this key vault for the required secrets at runtime. This requires the following environment variables to be set in `.secrets.env` so that PIXL can connect to the key vault: -- `EXPORT_AZ_CLIENT_ID` -- `EXPORT_AZ_CLIENT_PASSWORD` -- `EXPORT_AZ_TENANT_ID` -- `EXPORT_AZ_KEY_VAULT_NAME` +- `EXPORT_AZ_CLIENT_ID`: the service principal's client ID, mapped to `AZURE_CLIENT ID` in `docker-compose` +- `EXPORT_AZ_CLIENT_PASSWORD`: the password, mapped to `AZURE_CLIENT_SECRET` in `docker-compose` +- `EXPORT_AZ_TENANT_ID`: ID of the service principal's tenant. Also called its 'directory' ID. Mapped to `AZURE_TENANT_ID` in `docker-compose` +- `EXPORT_AZ_KEY_VAULT_NAME` the name of the key vault, used to connect to the correct key vault + +

Azure Keyvault setup + +## Azure Keyvault setup + +_This is done for the \_UCLH_DIF_ `dev` tenancy, will need to be done once in the _UCLHAZ_ `prod` +tenancy when ready to deploy to production.\_ + +This Key Vault and secret must persist any infrastructure changes so should be separate from disposable +infrastructure services. ServicePrincipal is required to connect to the Key Vault. + +The application uses the ServicePrincipal and password to authenticates with Azure via environment +variables. See [here](https://learn.microsoft.com/en-us/python/api/azure-identity/azure.identity.environmentcredential?view=azure-python) +for more info. + +The Key Vault and ServicePrincipal have already been created for the `dev` environment and details +are stored in the `pixl-secrets` note in the shared PIXL folder on _LastPass_. + +The process for doing so using the `az` CLI tool is described below. +This process must be repeated for `staging` & `prod` environments. + +### Step 1 + +Create the Azure Key Vault in an appropriate resource group: + +```bash +az keyvault create --resource-group --name --location "UKSouth" +``` + +### Step 2 + +Create Service Principal & grant access as per + +```bash +az ad sp create-for-rbac -n hasher-api --skip-assignment +``` + +This will produce the following output + +```json +{ + "appId": "", + "displayName": "", + "name": "http://", + "password": "", + "tenant": "" +} +``` + +### Step 3 + +Assign correct permissions to the newly created ServicePrincipal + +```bash +az keyvault set-policy --name --spn --secret-permissions backup delete get list set +``` + +### Step 4 + +Create a secret and store in the Key Vault + +Use Python to create a secret: + +```python +import secrets +secrets.token_urlsafe(32) +``` + +copy the secret and paste as below + +```bash +az keyvault secret set --vault-name "" --name "" --value "" +``` + +### Step 5 + +Save credentials in `.secrets.env` and a LastPass `PIXL Keyvault secrets` note. + +``` +EXPORT_AZ_CLIENT_ID= +EXPORT_AZ_CLIENT_PASSWORD= +EXPORT_AZ_TENANT_ID= +EXPORT_AZ_KEY_VAULT_NAME= +``` +
## Run From ecaf450a55df9190fd3bdbc620248d9ca7a59b6f Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Tue, 27 Feb 2024 11:42:08 +0100 Subject: [PATCH 055/120] Add sample `.secrets.env` --- .secrets.env.sample | 6 ++++++ README.md | 8 ++++++++ 2 files changed, 14 insertions(+) create mode 100644 .secrets.env.sample diff --git a/.secrets.env.sample b/.secrets.env.sample new file mode 100644 index 000000000..ab3955c96 --- /dev/null +++ b/.secrets.env.sample @@ -0,0 +1,6 @@ +# Azure key vault +EXPORT_AZ_CLIENT_ID= +EXPORT_AZ_CLIENT_PASSWORD= +EXPORT_AZ_TENANT_ID= +EXPORT_AZ_KEY_VAULT_NAME= + diff --git a/README.md b/README.md index a61edd306..dea383e3c 100644 --- a/README.md +++ b/README.md @@ -238,6 +238,14 @@ environment variables to be set in `.secrets.env` so that PIXL can connect to th - `EXPORT_AZ_TENANT_ID`: ID of the service principal's tenant. Also called its 'directory' ID. Mapped to `AZURE_TENANT_ID` in `docker-compose` - `EXPORT_AZ_KEY_VAULT_NAME` the name of the key vault, used to connect to the correct key vault +Create the `.secrets.env` file in the _PIXL_ directory by copying the sample: + +```bash +cp .secrets.env.sample .secrets.env +``` + +and fill in the missing values. +
Azure Keyvault setup ## Azure Keyvault setup From 72c21ea399fd3129405012dd3d35295e33c756ce Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Tue, 27 Feb 2024 12:02:30 +0100 Subject: [PATCH 056/120] Report which secret is missing --- pixl_core/src/core/_secrets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pixl_core/src/core/_secrets.py b/pixl_core/src/core/_secrets.py index 7dff41234..a8271bcf8 100644 --- a/pixl_core/src/core/_secrets.py +++ b/pixl_core/src/core/_secrets.py @@ -55,7 +55,7 @@ def fetch_secret(self, secret_name: str) -> str: """ secret = self.client.get_secret(secret_name).value if secret is None: - msg = "Azure Key Vault secret is None" + msg = f"Azure Key Vault secret {secret_name} is None" raise ValueError(msg) return str(secret) From 7c973b077bd5451badcb91f74ea5d88c863e39de Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Tue, 27 Feb 2024 12:04:44 +0100 Subject: [PATCH 057/120] Set default value for `PROJECT_CONFIGS_DIR` in sample `.env` --- .env.sample | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.sample b/.env.sample index aebb6ef52..ab1d3dae0 100644 --- a/.env.sample +++ b/.env.sample @@ -94,4 +94,4 @@ PIXL_DICOM_TRANSFER_TIMEOUT=240 PIXL_QUERY_TIMEOUT=10 # Project configs directory -PROJECT_CONFIGS_DIR= +PROJECT_CONFIGS_DIR=projects/configs From e99d59c8c246168cb83c5b30966c522927b4f15a Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Tue, 27 Feb 2024 12:06:22 +0100 Subject: [PATCH 058/120] Update destination options in README --- README.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index dea383e3c..1e624cbe2 100644 --- a/README.md +++ b/README.md @@ -220,11 +220,10 @@ The configuration file defines: [parquet files](./docs/file_types/parquet_files.md). We currently support the following endpoints: - `"none"`: no upload - `"ftps"`: a secure FTP server (for both _DICOM_ and _parquet_ files) - Requires the `FTPS_*` environment variables to be set in `.env` - - `"azure"`: a secure Azure Blob Storage account (for both _DICOM_ and _parquet_ files) - Requires the `AZURE_*` environment variables to be set in `.env` - - `"dicomweb"`: a DICOMweb server (for _DICOM_ files only) - Requires the `DICOMWEB_*` environment variables to be set in `.env` + + + + #### Project secrets {#project-secrets} From e1fd3945e7bc79ee626fa488ade2962784b72d6b Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Tue, 27 Feb 2024 12:09:19 +0100 Subject: [PATCH 059/120] Update Azure KV setup instructions --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1e624cbe2..e522b889a 100644 --- a/README.md +++ b/README.md @@ -278,7 +278,7 @@ az keyvault create --resource-group --name " --name "" -- ### Step 5 -Save credentials in `.secrets.env` and a LastPass `PIXL Keyvault secrets` note. +Save credentials in `.secrets.env` and a LastPass `PIXL Keyvault secrets` note. ``` EXPORT_AZ_CLIENT_ID= From eee4caadeeb714bec6877037db657a6e64a7c954 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Tue, 27 Feb 2024 12:15:17 +0100 Subject: [PATCH 060/120] Raise wanring when destination not supported instead of error --- pixl_core/src/core/exports.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pixl_core/src/core/exports.py b/pixl_core/src/core/exports.py index 11c9f27b4..5f703963f 100644 --- a/pixl_core/src/core/exports.py +++ b/pixl_core/src/core/exports.py @@ -122,8 +122,8 @@ def upload(self) -> None: if destination == "ftps": ftps_uploader = FTPSUploader(project_config) msg = f"Uploading parquet files for project {self.project_slug} via FTPS" - logger.warning(msg) + logger.info(msg) ftps_uploader.upload_parquet_files(self) else: - msg = f"Destination {destination} for parquet files not supported" - raise ValueError(msg) + msg = f"Destination {destination} for parquet files not supported. Skipping upload." + logger.warning(msg) From 878e22d5ed7c0a8fd6dcf3d87ea8f69fefddf44d Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Tue, 27 Feb 2024 12:24:56 +0100 Subject: [PATCH 061/120] Don't mention unsupported alternatives --- template_config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/template_config.yaml b/template_config.yaml index 752c4a749..1909a4115 100644 --- a/template_config.yaml +++ b/template_config.yaml @@ -19,5 +19,5 @@ project: tag_operation_files: ["base-tag-operations.yaml"] # DICOM tag anonymisation operations, can specify multiple files destination: - dicom: "ftps" # alternatives: "none", "dicomweb", "azure" - parquet: "ftps" # alternatives: "none", "azure" + dicom: "ftps" # alternatives: "none" + parquet: "ftps" # alternatives: "none" From 7815259c5443a0057a22d74dd93d81b169bfe5d8 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Tue, 27 Feb 2024 12:28:16 +0100 Subject: [PATCH 062/120] No need for line continuation --- test/run-system-test.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/run-system-test.sh b/test/run-system-test.sh index 5e9514976..008204a9d 100755 --- a/test/run-system-test.sh +++ b/test/run-system-test.sh @@ -23,8 +23,10 @@ setup() { # Note: cannot run as single docker compose command due to different build contexts docker compose --env-file .env -p system-test up --wait -d --build --remove-orphans # Warning: Requires to be run from the project root - (cd "${PACKAGE_DIR}" && - docker compose --env-file test/.env --env-file .secrets.env -p system-test up --wait -d --build) + ( + cd "${PACKAGE_DIR}" + docker compose --env-file test/.env --env-file .secrets.env -p system-test up --wait -d --build + ) ./scripts/insert_test_data.sh } From 3350bcc6015115f58ae09f5e2be74c203971d908 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Tue, 27 Feb 2024 12:34:22 +0100 Subject: [PATCH 063/120] Better function naming --- pixl_core/src/core/project_config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pixl_core/src/core/project_config.py b/pixl_core/src/core/project_config.py index b7f515f27..e41448bc6 100644 --- a/pixl_core/src/core/project_config.py +++ b/pixl_core/src/core/project_config.py @@ -41,10 +41,10 @@ def load_project_config(project_slug: str) -> PixlConfig | Any: logger.warning(f"Loading config for {project_slug} from {configpath}") # noqa: G004 if not configpath.exists(): raise FileNotFoundError(f"No config for {project_slug}. Please submit PR and redeploy.") # noqa: EM102, TRY003 - return _load_project_config(configpath) + return _load_and_validate(configpath) -def _load_project_config(filename: Path) -> PixlConfig | Any: +def _load_and_validate(filename: Path) -> PixlConfig | Any: """ Load configuration from a yaml file. :param filename: Path to the yaml file From 2865a913b68a518a270611bdba246b5a2d5d6325 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Tue, 27 Feb 2024 12:48:07 +0100 Subject: [PATCH 064/120] Get FTP port from Azure Keyvault as well --- pixl_core/src/core/upload.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pixl_core/src/core/upload.py b/pixl_core/src/core/upload.py index 100b572a7..f91bb83b6 100644 --- a/pixl_core/src/core/upload.py +++ b/pixl_core/src/core/upload.py @@ -22,8 +22,6 @@ from pathlib import Path from typing import TYPE_CHECKING, BinaryIO -from decouple import config - if TYPE_CHECKING: from core.exports import ParquetExport from core.project_config import PixlConfig @@ -73,7 +71,7 @@ def _set_config(self) -> None: self.host = self.keyvault.fetch_secret(f"{az_prefix}--ftp--host") self.user = self.keyvault.fetch_secret(f"{az_prefix}--ftp--username") self.password = self.keyvault.fetch_secret(f"{az_prefix}--ftp--password") - self.port = config("FTP_PORT", default=21, cast=int) + self.port = self.keyvault.fetch_secret(f"{az_prefix}--ftp--port") def upload_dicom_image(self, zip_content: BinaryIO, pseudo_anon_id: str) -> None: """Upload a DICOM image to the FTPS server.""" From 9e251750ce68f62bee2813ecda7c9794ef9b69d6 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Tue, 27 Feb 2024 12:53:53 +0100 Subject: [PATCH 065/120] Remove more references to `FTP_` environment variables --- docker-compose.yml | 9 +-------- pytest-pixl/tests/conftest.py | 6 ------ 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index ce6eff987..bd200a856 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -55,12 +55,6 @@ x-pixl-db: &pixl-db PIXL_DB_PASSWORD: ${PIXL_DB_PASSWORD} PIXL_DB_NAME: ${PIXL_DB_NAME} -x-ftp-host: &ftp-host - FTP_HOST: ${FTP_HOST} - FTP_USER_NAME: ${FTP_USER_NAME} - FTP_USER_PASSWORD: ${FTP_USER_PASSWORD} - FTP_PORT: ${FTP_PORT} - x-azure-keyvault: &azure-keyvault AZURE_CLIENT_ID: ${EXPORT_AZ_CLIENT_ID} AZURE_CLIENT_SECRET: ${EXPORT_AZ_CLIENT_PASSWORD} @@ -121,7 +115,7 @@ services: <<: *build-args-common command: /run/secrets environment: - <<: [*proxy-common, *pixl-common-env, *ftp-host, *azure-keyvault] + <<: [*proxy-common, *pixl-common-env, *azure-keyvault] ORTHANC_NAME: "PIXL: Anon" ORTHANC_USERNAME: ${ORTHANC_ANON_USERNAME} ORTHANC_PASSWORD: ${ORTHANC_ANON_PASSWORD} @@ -262,7 +256,6 @@ services: *proxy-common, *pixl-common-env, *pixl-rabbit-mq, - *ftp-host, *azure-keyvault, ] AZ_STORAGE_ACCOUNT_NAME: ${PIXL_EHR_API_AZ_STORAGE_ACCOUNT_NAME} diff --git a/pytest-pixl/tests/conftest.py b/pytest-pixl/tests/conftest.py index b06da963d..ffc8739ea 100644 --- a/pytest-pixl/tests/conftest.py +++ b/pytest-pixl/tests/conftest.py @@ -11,7 +11,6 @@ # 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. -import os from pathlib import Path # Avoid running samples for fixture tests directly with pytest @@ -20,8 +19,3 @@ pytest_plugins = ["pytester"] TEST_DIR = Path(__file__).parent - -os.environ["FTP_HOST"] = "localhost" -os.environ["FTP_USER_NAME"] = "pixl_user" -os.environ["FTP_USER_PASSWORD"] = "longpassword" # noqa: S105 Hardcoding password -os.environ["FTP_PORT"] = "20021" From 60314f0177b57809b907c0f7f6b8c296900993bc Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Tue, 27 Feb 2024 13:05:05 +0100 Subject: [PATCH 066/120] Fix formatting --- test/run-system-test.sh | 40 +++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/test/run-system-test.sh b/test/run-system-test.sh index 008204a9d..f623e0d74 100755 --- a/test/run-system-test.sh +++ b/test/run-system-test.sh @@ -18,22 +18,24 @@ PACKAGE_DIR="${BIN_DIR%/*}" cd "${PACKAGE_DIR}/test" setup() { - docker compose --env-file .env -p system-test down --volumes - # - # Note: cannot run as single docker compose command due to different build contexts - docker compose --env-file .env -p system-test up --wait -d --build --remove-orphans - # Warning: Requires to be run from the project root - ( - cd "${PACKAGE_DIR}" - docker compose --env-file test/.env --env-file .secrets.env -p system-test up --wait -d --build - ) + docker compose --env-file .env -p system-test down --volumes + # + # Note: cannot run as single docker compose command due to different build contexts + docker compose --env-file .env -p system-test up --wait -d --build --remove-orphans + # Warning: Requires to be run from the project root + ( + cd "${PACKAGE_DIR}" + docker compose --env-file test/.env --env-file .secrets.env -p system-test up --wait -d --build + ) - ./scripts/insert_test_data.sh + ./scripts/insert_test_data.sh } teardown() { - (cd "${PACKAGE_DIR}" && - docker compose -f docker-compose.yml -f test/docker-compose.yml -p system-test down --volumes) + ( + cd "${PACKAGE_DIR}" + docker compose -f docker-compose.yml -f test/docker-compose.yml -p system-test down --volumes + ) } # Allow user to perform just setup so that pytest may be run repeatedly without @@ -41,13 +43,13 @@ teardown() { # for clearing up anything it creates (export temp dir?) subcmd=${1:-""} if [ "$subcmd" = "setup" ]; then - setup + setup elif [ "$subcmd" = "teardown" ]; then - teardown + teardown else - setup - pytest --verbose --log-cli-level INFO - echo FINISHED PYTEST COMMAND - teardown - echo SYSTEM TEST SUCCESSFUL + setup + pytest --verbose --log-cli-level INFO + echo FINISHED PYTEST COMMAND + teardown + echo SYSTEM TEST SUCCESSFUL fi From 8edbe552b4041fde78fcacb7a419899a9f7a151a Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Tue, 27 Feb 2024 13:29:28 +0100 Subject: [PATCH 067/120] Forgot another renaming instance --- pixl_core/tests/test_project_config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pixl_core/tests/test_project_config.py b/pixl_core/tests/test_project_config.py index 66448763e..f1ea2aa8c 100644 --- a/pixl_core/tests/test_project_config.py +++ b/pixl_core/tests/test_project_config.py @@ -16,7 +16,7 @@ from pathlib import Path import pytest -from core.project_config import PixlConfig, _load_project_config +from core.project_config import PixlConfig, _load_and_validate from decouple import config from pydantic import ValidationError @@ -26,7 +26,7 @@ def test_config_from_file(): """Test whether config file is correctly parsed and validated.""" - project_config = _load_project_config(TEST_CONFIG) + project_config = _load_and_validate(TEST_CONFIG) assert project_config.project.name == "test-extract-uclh-omop-cdm" assert project_config.project.modalities == ["DX", "CR"] From 6a555e29e7e8e103d5857d2925b5e2726022c7fc Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Tue, 27 Feb 2024 13:51:33 +0100 Subject: [PATCH 068/120] Remove more instances of `FTP_*` env variables --- test/.env | 5 ----- 1 file changed, 5 deletions(-) diff --git a/test/.env b/test/.env index f8cb3851d..8782e52e3 100644 --- a/test/.env +++ b/test/.env @@ -74,8 +74,3 @@ RABBITMQ_PASSWORD=rabbitmq_password # Project configs directory PROJECT_CONFIGS_DIR=/projects/configs - -# FTP server -FTP_HOST=host.docker.internal -FTP_USER_NAME=pixl_user -FTP_USER_PASSWORD=longpassword From 56fad9dfe866e11e2474d3dde095ae93acd24cdc Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Wed, 28 Feb 2024 11:11:09 +0000 Subject: [PATCH 069/120] No more FTP dummy service --- test/README.md | 25 ------------------- .../ftp-server/mounts/data/.gitkeep | 0 2 files changed, 25 deletions(-) delete mode 100644 test/dummy-services/ftp-server/mounts/data/.gitkeep diff --git a/test/README.md b/test/README.md index 0e3ad87e7..344cfd47f 100644 --- a/test/README.md +++ b/test/README.md @@ -49,31 +49,6 @@ Run the following to teardown: `./dummy-services` contains a Python script and Dockerfile to mock the CogStack service, used for de-identification of the radiology reports in the EHR API. -#### FTP server - -We spin up a test FTP server from the Docker container defined in `./dummy-services/ftp-server/`. -The Docker container inherits from -[`delfer/alpine-ftp-server`](https://github.com/delfer/docker-alpine-ftp-server) and uses `vsftpd` -as the FTP client. The Docker container requires the following environment variables to be defined: - -- `ADDRESS`: external address to which clients can connect for passive ports -- `USERS`: space and `|` separated list of usernames, passwords, home directories and groups: - - format `name1|password1|[folder1][|uid1][|gid1] name2|password2|[folder2][|uid2][|gid2]` - - the values in `[]` are optional -- `TLS_KEY`: keyfile for the TLS certificate -- `TLS_CERT`: TLS certificate - -> [!warning] The `ADDRESS` should match the `FTP_HOST` environment variable defined in `.env`, -> otherwise FTP commands such as `STOR` or `dir` run from other Docker containers in the network -> (such as `orthanc-anon`) will fail. _Note: connecting and logging into the FTP server might still -> work, as the address name is only checked for protected operations such as listing and transfering -> files._ - -**Volume**: to check succesful uploads, we mount a local data directory to `/home/${FTP_USERNAME}/` - -**SSL certifcates**: the SSL certificate files are defined in `test/dummy-services/ftp-server/ssl` -and are copied into `/etc/ssl/private` when building the Docker container. - ### Resources - `./resources/` provides 2 mock DICOM images used to populate the mock VNA diff --git a/test/dummy-services/ftp-server/mounts/data/.gitkeep b/test/dummy-services/ftp-server/mounts/data/.gitkeep deleted file mode 100644 index e69de29bb..000000000 From 313cab9e669ca63a59a491acd0aa851afd45468e Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Wed, 28 Feb 2024 11:15:28 +0000 Subject: [PATCH 070/120] Refer to pytest-pixl plugin in test docs --- test/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/README.md b/test/README.md index 344cfd47f..5e7ac522a 100644 --- a/test/README.md +++ b/test/README.md @@ -34,6 +34,12 @@ Run the following to teardown: ./run-system-test.sh teardown ``` +## The `pytest-pixl` plugin + +We provide a [`pytest` plugin](../pytest-pixl/README.md) with shared functionality for PIXL system +and unit tests. This includes an `ftp_server` fixture to spin up a lightweight FTP server, +to mock the FTP server used by the Data Safe Haven. + ## File organisation ### Docker compose From 5ccd23aa53275080dcd0d6508d1672f5331c06d6 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Wed, 28 Feb 2024 14:40:59 +0000 Subject: [PATCH 071/120] Get FTP port from Keyvault as well --- pixl_core/src/core/upload.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pixl_core/src/core/upload.py b/pixl_core/src/core/upload.py index f91bb83b6..9a316c11f 100644 --- a/pixl_core/src/core/upload.py +++ b/pixl_core/src/core/upload.py @@ -71,7 +71,7 @@ def _set_config(self) -> None: self.host = self.keyvault.fetch_secret(f"{az_prefix}--ftp--host") self.user = self.keyvault.fetch_secret(f"{az_prefix}--ftp--username") self.password = self.keyvault.fetch_secret(f"{az_prefix}--ftp--password") - self.port = self.keyvault.fetch_secret(f"{az_prefix}--ftp--port") + self.port = int(self.keyvault.fetch_secret(f"{az_prefix}--ftp--port")) def upload_dicom_image(self, zip_content: BinaryIO, pseudo_anon_id: str) -> None: """Upload a DICOM image to the FTPS server.""" From 72553f4781ed4f1e43a9cbb8473576bdd45441b6 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Wed, 28 Feb 2024 14:41:35 +0000 Subject: [PATCH 072/120] Allow 'none' destination for uploading --- pixl_core/src/core/exports.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pixl_core/src/core/exports.py b/pixl_core/src/core/exports.py index 5f703963f..aed413df5 100644 --- a/pixl_core/src/core/exports.py +++ b/pixl_core/src/core/exports.py @@ -119,11 +119,16 @@ def upload(self) -> None: project_config = load_project_config(self.project_slug) destination = project_config.destination.parquet + if destination == "none": + msg = ( + f"Destination for parquet files for project {self.project_slug} is 'none'." + "Skipping upload." + ) + logger.info(msg) if destination == "ftps": - ftps_uploader = FTPSUploader(project_config) msg = f"Uploading parquet files for project {self.project_slug} via FTPS" logger.info(msg) - ftps_uploader.upload_parquet_files(self) + FTPSUploader(project_config).upload_parquet_files(self) else: msg = f"Destination {destination} for parquet files not supported. Skipping upload." logger.warning(msg) From 8fbb330f330779d0fd54a44f183cb82b128382b1 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Wed, 28 Feb 2024 14:41:56 +0000 Subject: [PATCH 073/120] This string expansion isn't working --- pixl_core/src/core/_upload_ftps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pixl_core/src/core/_upload_ftps.py b/pixl_core/src/core/_upload_ftps.py index 37426eab2..f5d9bf5f9 100644 --- a/pixl_core/src/core/_upload_ftps.py +++ b/pixl_core/src/core/_upload_ftps.py @@ -62,7 +62,7 @@ def _connect_to_ftp(ftp_host: str, ftp_port: int, ftp_user: str, ftp_password: s ftp.login(ftp_user, ftp_password) ftp.prot_p() except ftplib.all_errors as ftp_error: - error_msg = "Failed to connect to FTPS server: '%s'" + error_msg = "Failed to connect to FTPS server" raise ConnectionError(error_msg, ftp_error) from ftp_error return ftp From 795060d58b80a226ffbaa84892185a6c6fa13104 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Wed, 28 Feb 2024 14:42:22 +0000 Subject: [PATCH 074/120] Exports are now under `projects/exports` --- test/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/conftest.py b/test/conftest.py index 262292737..5f21bcc5d 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -25,7 +25,7 @@ @pytest.fixture() def host_export_root_dir(): """Intermediate export dir as seen from the host""" - return Path(__file__).parents[1] / "exports" + return Path(__file__).parents[1] / "projects" / "exports" TEST_DIR = Path(__file__).parent From 52a44ef253aff771371766d23e93d35684aa935a Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Wed, 28 Feb 2024 14:42:44 +0000 Subject: [PATCH 075/120] Docker mounts need absolute paths --- docker-compose.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index bd200a856..29c56a964 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -142,7 +142,7 @@ services: AZ_DICOM_TOKEN_REFRESH_SECS: "600" TIME_OFFSET: "${STUDY_TIME_OFFSET}" SALT_VALUE: ${SALT_VALUE}" - PROJECT_CONFIGS_DIR: ${PROJECT_CONFIGS_DIR:-/projects/configs} + PROJECT_CONFIGS_DIR: /${PROJECT_CONFIGS_DIR:-/projects/configs} ports: - "${ORTHANC_ANON_DICOM_PORT}:4242" - "${ORTHANC_ANON_WEB_PORT}:8042" @@ -151,7 +151,7 @@ services: source: orthanc-anon-data target: /var/lib/orthanc/db - ${PWD}/orthanc/orthanc-anon/config:/run/secrets:ro - - ${PWD}/projects/configs:${PROJECT_CONFIGS_DIR:-/projects/configs}:ro + - ${PWD}/projects/configs:/${PROJECT_CONFIGS_DIR:-/projects/configs}:ro networks: - pixl-net # needed for same reason as ehr-api @@ -261,7 +261,7 @@ services: AZ_STORAGE_ACCOUNT_NAME: ${PIXL_EHR_API_AZ_STORAGE_ACCOUNT_NAME} AZ_STORAGE_CONTAINER_NAME: ${PIXL_EHR_API_AZ_STORAGE_CONTAINER_NAME} COGSTACK_REDACT_URL: ${PIXL_EHR_COGSTACK_REDACT_URL} - PROJECT_CONFIGS_DIR: ${PROJECT_CONFIGS_DIR:-/projects/configs} + PROJECT_CONFIGS_DIR: /${PROJECT_CONFIGS_DIR:-/projects/configs} env_file: - ./docker/common.env depends_on: @@ -285,7 +285,7 @@ services: - "host.docker.internal:host-gateway" volumes: - ${PWD}/projects/exports:/run/projects/exports - - ${PWD}/projects/configs:${PROJECT_CONFIGS_DIR:-/projects/configs}:ro + - ${PWD}/projects/configs:/${PROJECT_CONFIGS_DIR:-/projects/configs}:ro imaging-api: build: From 19537bf2857d6c72e969e99344e79ebbeadcf979 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Wed, 28 Feb 2024 14:52:59 +0000 Subject: [PATCH 076/120] Seems that we still need the `FTP_*` environment variables I suspect because they are still used by the pytest plugin ftpserver fixture. --- test/.env | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/.env b/test/.env index 8782e52e3..9d14a1790 100644 --- a/test/.env +++ b/test/.env @@ -72,5 +72,10 @@ RABBITMQ_HOST=localhost RABBITMQ_USERNAME=rabbitmq_username RABBITMQ_PASSWORD=rabbitmq_password +# FTP server +FTP_HOST=host.docker.internal +FTP_USER_NAME=pixl_user +FTP_USER_PASSWORD=longpassword + # Project configs directory -PROJECT_CONFIGS_DIR=/projects/configs +PROJECT_CONFIGS_DIR=projects/configs From 9410466191447f7a4d08a55d4daf3656d0294496 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Wed, 28 Feb 2024 15:14:34 +0000 Subject: [PATCH 077/120] Update exports dir in gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index ea9a25403..3034017ff 100644 --- a/.gitignore +++ b/.gitignore @@ -147,6 +147,6 @@ dmypy.json .vscode # project specific files -/exports/ +/projects/exports/ **/test_producer.csv /test/dummy-services/ftp-server/mounts/data/* From 48d19637180052f806f1fe515144815e0d9bc489 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Wed, 28 Feb 2024 15:25:28 +0000 Subject: [PATCH 078/120] Mostly for debugging but probably a useful check to avoid surprises --- pixl_core/src/core/exports.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pixl_core/src/core/exports.py b/pixl_core/src/core/exports.py index aed413df5..29871d93c 100644 --- a/pixl_core/src/core/exports.py +++ b/pixl_core/src/core/exports.py @@ -46,6 +46,11 @@ def __init__( different view of the filesystem than the docker containers do. """ self.export_dir = export_dir + # Make sure the base export direcotry exsists + if not self.export_dir.exists(): + msg = f"Export directory {self.export_dir} does not exist" + raise FileNotFoundError(msg) + self.project_slug, self.extract_time_slug = self._get_slugs(project_name, extract_datetime) project_base = self.export_dir / self.project_slug @@ -85,7 +90,7 @@ def copy_to_exports(self, input_omop_dir: pathlib.Path) -> str: logger.info("Copying public parquet files from %s to %s", public_input, self.public_output) - # Make directory for exports if they don't exist + # Make directory for project exports ParquetExport._mkdir(self.public_output) # Copy extract files, overwriting if it exists From 893ec5624fae19cbef148601d82d4698173c394e Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Wed, 28 Feb 2024 15:25:45 +0000 Subject: [PATCH 079/120] Think I finally found the bug --- cli/src/pixl_cli/_io.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/src/pixl_cli/_io.py b/cli/src/pixl_cli/_io.py index 8d928692c..1450a294f 100644 --- a/cli/src/pixl_cli/_io.py +++ b/cli/src/pixl_cli/_io.py @@ -27,7 +27,7 @@ # The export root dir from the point of view of the docker host (which is where the CLI runs) # For the view from inside, see pixl_ehr/main.py: EHR_EXPORT_ROOT_DIR -HOST_EXPORT_ROOT_DIR = Path(__file__).parents[3] / "exports" +HOST_EXPORT_ROOT_DIR = Path(__file__).parents[3] / "projects" / "exports" def messages_from_state_file(filepath: Path) -> list[Message]: From de3dbe0c1b64738a4202319fa4b71010e2951af3 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Wed, 28 Feb 2024 15:25:57 +0000 Subject: [PATCH 080/120] Update docs --- docs/file_types/parquet_files.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/file_types/parquet_files.md b/docs/file_types/parquet_files.md index 0502202fe..1288bb003 100644 --- a/docs/file_types/parquet_files.md +++ b/docs/file_types/parquet_files.md @@ -42,5 +42,5 @@ Various _parquet_ files are provided throughout the repo to enable unit and syst `extract_summary.json` file to mimic the input received from OMOP-ES for the system tests During the system test, a `radiology.parquet` file is generated and temporarily stored in -`exports/test-extract-uclh-omop-cdm/latest/radiology/radiology.parquet` to check the successful +`projects/exports/test-extract-uclh-omop-cdm/latest/radiology/radiology.parquet` to check the successful de-identification before the DSH upload. This file is then deleted after the test. From 26f77a3a0df79bc738aef38b69197d87746aa0c1 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Wed, 28 Feb 2024 15:33:45 +0000 Subject: [PATCH 081/120] Make sure exports dir exists in unit tests --- cli/tests/conftest.py | 4 +++- pixl_core/tests/conftest.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/cli/tests/conftest.py b/cli/tests/conftest.py index 947807e10..356cb7386 100644 --- a/cli/tests/conftest.py +++ b/cli/tests/conftest.py @@ -48,7 +48,9 @@ @pytest.fixture(autouse=True) def export_dir(tmp_path_factory: pytest.TempPathFactory) -> pathlib.Path: """Tmp dir to for tests to extract to.""" - return tmp_path_factory.mktemp("export_base") / "exports" + export_dir = tmp_path_factory.mktemp("export_base") / "exports" + export_dir.mkdir() + return export_dir @pytest.fixture() diff --git a/pixl_core/tests/conftest.py b/pixl_core/tests/conftest.py index 3a73ec8b1..5a409f5fe 100644 --- a/pixl_core/tests/conftest.py +++ b/pixl_core/tests/conftest.py @@ -203,4 +203,6 @@ def already_exported_dicom_image(rows_in_session) -> Image: @pytest.fixture(autouse=True) def export_dir(tmp_path_factory: pytest.TempPathFactory) -> pathlib.Path: """Tmp dir to for tests to extract to.""" - return tmp_path_factory.mktemp("export_base") / "exports" + export_dir = tmp_path_factory.mktemp("export_base") / "exports" + export_dir.mkdir() + return export_dir From 0e19bfec3aa68d2bb2f7420937eec5c3f0e118e2 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Wed, 28 Feb 2024 17:02:00 +0000 Subject: [PATCH 082/120] Add check for public parquet exports --- test/test_radiology_parquet.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/test_radiology_parquet.py b/test/test_radiology_parquet.py index a304b7344..79d73b436 100644 --- a/test/test_radiology_parquet.py +++ b/test/test_radiology_parquet.py @@ -17,10 +17,23 @@ import pandas as pd import pytest +from conftest import RESOURCES_DIR logger = logging.getLogger(__name__) +@pytest.mark.usefixtures("_setup_pixl_cli") +def test_public_parquet(host_export_root_dir: Path): + """Tests whether the public parquet files have been exported to the right place""" + expected_public_dir = ( + host_export_root_dir / "test-extract-uclh-omop-cdm" / "latest" / "omop" / "public" + ) + expected_files = sorted([x.stem for x in (RESOURCES_DIR / "omop" / "public").glob("*.parquet")]) + + assert expected_public_dir.exists() + assert expected_files == sorted([x.stem for x in expected_public_dir.glob("*.parquet")]) + + @pytest.mark.usefixtures("_extract_radiology_reports") def test_radiology_parquet(host_export_root_dir: Path): """ From fbd7b5bcab8730411727da4d1c9af2c7ccaa82da Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Wed, 28 Feb 2024 17:03:10 +0000 Subject: [PATCH 083/120] Rename system test checks for exports --- test/{test_radiology_parquet.py => test_parquet_exports.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/{test_radiology_parquet.py => test_parquet_exports.py} (100%) diff --git a/test/test_radiology_parquet.py b/test/test_parquet_exports.py similarity index 100% rename from test/test_radiology_parquet.py rename to test/test_parquet_exports.py From b8b4f2a64bbb460f7101c0cc4690eff82ba3b6af Mon Sep 17 00:00:00 2001 From: peshence Date: Thu, 29 Feb 2024 16:26:09 +0000 Subject: [PATCH 084/120] refactor[config]: remove tag-operations from filenames --- README.md | 2 +- pixl_core/tests/test_project_config.py | 4 ++-- pixl_dcmd/tests/test_main.py | 4 ++-- ...dm-tag-operations.yaml => test-extract-uclh-omop-cdm.yaml} | 0 ... uclh-nasogastric-tube-project-ngt-only-full-dataset.yaml} | 0 projects/configs/test-extract-uclh-omop-cdm.yaml | 2 +- .../uclh-nasogastric-tube-project-ngt-only-full-dataset.yaml | 2 +- test/README.md | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) rename projects/configs/tag-operations/{test-extract-uclh-omop-cdm-tag-operations.yaml => test-extract-uclh-omop-cdm.yaml} (100%) rename projects/configs/tag-operations/{uclh-nasogastric-tube-project-ngt-only-full-dataset-tag-operations.yaml => uclh-nasogastric-tube-project-ngt-only-full-dataset.yaml} (100%) diff --git a/README.md b/README.md index e522b889a..f3623653f 100644 --- a/README.md +++ b/README.md @@ -225,7 +225,7 @@ The configuration file defines: -#### Project secrets {#project-secrets} +#### Project secrets Any credentials required for uploading the project's results should be stored in an **Azure Key Vault** (set up instructions below). diff --git a/pixl_core/tests/test_project_config.py b/pixl_core/tests/test_project_config.py index f1ea2aa8c..21df06081 100644 --- a/pixl_core/tests/test_project_config.py +++ b/pixl_core/tests/test_project_config.py @@ -31,7 +31,7 @@ def test_config_from_file(): assert project_config.project.name == "test-extract-uclh-omop-cdm" assert project_config.project.modalities == ["DX", "CR"] assert project_config.tag_operation_files == [ - PROJECT_CONFIGS_DIR / "tag-operations" / "test-extract-uclh-omop-cdm-tag-operations.yaml" + PROJECT_CONFIGS_DIR / "tag-operations" / "test-extract-uclh-omop-cdm.yaml" ] assert project_config.destination.dicom == "ftps" assert project_config.destination.parquet == "ftps" @@ -39,7 +39,7 @@ def test_config_from_file(): BASE_YAML_DATA = { "project": {"name": "myproject", "modalities": ["DX", "CR"]}, - "tag_operations": ["test-extract-uclh-omop-cdm-tag-operations.yaml"], + "tag_operations": ["test-extract-uclh-omop-cdm.yaml"], "destination": {"dicom": "ftps", "parquet": "ftps"}, } diff --git a/pixl_dcmd/tests/test_main.py b/pixl_dcmd/tests/test_main.py index d03f3e7f2..d2624c484 100644 --- a/pixl_dcmd/tests/test_main.py +++ b/pixl_dcmd/tests/test_main.py @@ -37,7 +37,7 @@ def tag_scheme() -> dict: """Read the tag scheme from orthanc raw.""" tag_file = ( pathlib.Path(__file__).parents[2] - / "projects/configs/tag-operations/test-extract-uclh-omop-cdm-tag-operations.yaml" + / "projects/configs/tag-operations/test-extract-uclh-omop-cdm.yaml" ) return yaml.safe_load(tag_file.read_text()) @@ -140,6 +140,6 @@ def test_can_nifti_convert_post_anonymisation( def test_merge_tag_schemes_single_file(): tag_ops_file = ( pathlib.Path(__file__).parents[2] - / "projects/configs/tag-operations/test-extract-uclh-omop-cdm-tag-operations.yaml" + / "projects/configs/tag-operations/test-extract-uclh-omop-cdm.yaml" ) merge_tag_schemes([tag_ops_file]) diff --git a/projects/configs/tag-operations/test-extract-uclh-omop-cdm-tag-operations.yaml b/projects/configs/tag-operations/test-extract-uclh-omop-cdm.yaml similarity index 100% rename from projects/configs/tag-operations/test-extract-uclh-omop-cdm-tag-operations.yaml rename to projects/configs/tag-operations/test-extract-uclh-omop-cdm.yaml diff --git a/projects/configs/tag-operations/uclh-nasogastric-tube-project-ngt-only-full-dataset-tag-operations.yaml b/projects/configs/tag-operations/uclh-nasogastric-tube-project-ngt-only-full-dataset.yaml similarity index 100% rename from projects/configs/tag-operations/uclh-nasogastric-tube-project-ngt-only-full-dataset-tag-operations.yaml rename to projects/configs/tag-operations/uclh-nasogastric-tube-project-ngt-only-full-dataset.yaml diff --git a/projects/configs/test-extract-uclh-omop-cdm.yaml b/projects/configs/test-extract-uclh-omop-cdm.yaml index 3d3773e71..818008cf0 100644 --- a/projects/configs/test-extract-uclh-omop-cdm.yaml +++ b/projects/configs/test-extract-uclh-omop-cdm.yaml @@ -17,7 +17,7 @@ project: azure_kv_alias: "test" modalities: ["DX", "CR"] -tag_operation_files: ["test-extract-uclh-omop-cdm-tag-operations.yaml"] +tag_operation_files: ["test-extract-uclh-omop-cdm.yaml"] destination: dicom: "ftps" diff --git a/projects/configs/uclh-nasogastric-tube-project-ngt-only-full-dataset.yaml b/projects/configs/uclh-nasogastric-tube-project-ngt-only-full-dataset.yaml index eb5fab111..0eb56b19b 100644 --- a/projects/configs/uclh-nasogastric-tube-project-ngt-only-full-dataset.yaml +++ b/projects/configs/uclh-nasogastric-tube-project-ngt-only-full-dataset.yaml @@ -17,7 +17,7 @@ project: modalities: ["DX", "CR"] tag_operation_files: - ["uclh-nasogastric-tube-project-ngt-only-full-dataset-tag-operations.yaml"] + ["uclh-nasogastric-tube-project-ngt-only-full-dataset.yaml"] destination: dicom: "ftps" diff --git a/test/README.md b/test/README.md index 5e7ac522a..ac6733066 100644 --- a/test/README.md +++ b/test/README.md @@ -12,7 +12,7 @@ consumers started. **Then** a row in the "anon" EMAP data instance of the PIXL postgres instance exists and the DICOM study exists in the "anon" PIXL Orthanc instance. -You can run the system test with: +After setting up your [.secrets.env](../README.md#project-secrets)), you can run the system test with: ```bash ./run-system-test.sh From bc13dabb883bb60c1946a440c41f76d9ee84317c Mon Sep 17 00:00:00 2001 From: Peter Tsrunchev Date: Thu, 29 Feb 2024 16:32:04 +0000 Subject: [PATCH 085/120] Update README.md typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f3623653f..77efc8746 100644 --- a/README.md +++ b/README.md @@ -255,7 +255,7 @@ tenancy when ready to deploy to production.\_ This Key Vault and secret must persist any infrastructure changes so should be separate from disposable infrastructure services. ServicePrincipal is required to connect to the Key Vault. -The application uses the ServicePrincipal and password to authenticates with Azure via environment +The application uses the ServicePrincipal and password to authenticate with Azure via environment variables. See [here](https://learn.microsoft.com/en-us/python/api/azure-identity/azure.identity.environmentcredential?view=azure-python) for more info. From bcf0a5f1d9927b02559245eb93b26de003670706 Mon Sep 17 00:00:00 2001 From: Peter Tsrunchev Date: Thu, 29 Feb 2024 16:38:25 +0000 Subject: [PATCH 086/120] Update template_config.yaml --- template_config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/template_config.yaml b/template_config.yaml index 1909a4115..f2f9c1736 100644 --- a/template_config.yaml +++ b/template_config.yaml @@ -16,7 +16,7 @@ project: name: "project-slug" modalities: ["DX", "CR"] # DICOM dataset modalities to retain -tag_operation_files: ["base-tag-operations.yaml"] # DICOM tag anonymisation operations, can specify multiple files +tag_operation_files: ["base-tag-operations.yaml"] # DICOM tag anonymisation operations, can specify multiple files in the future destination: dicom: "ftps" # alternatives: "none" From dc84e0c6b7bc46430de8e8524de32f82aeadfba5 Mon Sep 17 00:00:00 2001 From: Peter Tsrunchev Date: Thu, 29 Feb 2024 17:07:05 +0000 Subject: [PATCH 087/120] Update cli/README.md Co-authored-by: Stef Piatek --- cli/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/README.md b/cli/README.md index c4c0ad5f7..94a32491a 100644 --- a/cli/README.md +++ b/cli/README.md @@ -143,7 +143,7 @@ pip install -e ../pixl_core/ -e .[test] The CLI tests require a running instance of the `rabbitmq` service, for which we provide a `docker-compose` [file](./tests/docker-compose.yml). The service is automatically started by the -`run_containers` _pytest_ fixture. So to run the tests, simply run +`run_containers` _pytest_ fixture. So to run the tests, run ```bash pytest From dcd5b8be0e7507e14e4d3a25b5d5726289f16bf5 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Thu, 29 Feb 2024 15:21:27 +0000 Subject: [PATCH 088/120] Switch back to `pydantic.field_validator` `pydantic.validator` is deprecated --- pixl_core/pyproject.toml | 4 ++-- pixl_core/src/core/project_config.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pixl_core/pyproject.toml b/pixl_core/pyproject.toml index a0399ef26..ebedb8296 100644 --- a/pixl_core/pyproject.toml +++ b/pixl_core/pyproject.toml @@ -48,5 +48,5 @@ extend = "../ruff.toml" "./tests/**" = ["D100"] [tool.ruff.pep8-naming] -# Allow Pydantic's `@validator` decorator to trigger class method treatment. -classmethod-decorators = ["classmethod", "pydantic.validator"] +# Allow Pydantic's `@field_validator` decorator to trigger class method treatment. +classmethod-decorators = ["classmethod", "pydantic.field_validator"] diff --git a/pixl_core/src/core/project_config.py b/pixl_core/src/core/project_config.py index e41448bc6..9de1904c2 100644 --- a/pixl_core/src/core/project_config.py +++ b/pixl_core/src/core/project_config.py @@ -22,7 +22,7 @@ import yaml from decouple import Config, RepositoryEmpty, RepositoryEnv -from pydantic import BaseModel, validator +from pydantic import BaseModel, field_validator # Make sure local .env file is loaded if it exists env_file = Path.cwd() / ".env" @@ -72,7 +72,7 @@ class _Destination(BaseModel): dicom: _DestinationEnum parquet: _DestinationEnum - @validator("parquet") + @field_validator("parquet") def valid_parquet_destination(cls, v: str) -> str: if v == "dicomweb": msg = "Parquet destination cannot be dicomweb" @@ -87,7 +87,7 @@ class PixlConfig(BaseModel): tag_operation_files: list[Path] destination: _Destination - @validator("tag_operation_files") + @field_validator("tag_operation_files") def _valid_tag_operations(cls, tag_ops_files: list[str]) -> list[Path]: if not tag_ops_files or len(tag_ops_files) == 0: msg = "There should be at least 1 tag operations file" From 10f85db67c9eddc8f9dbead4e621a8495d9bba58 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Thu, 29 Feb 2024 15:21:43 +0000 Subject: [PATCH 089/120] Record pydantic version --- pixl_core/pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pixl_core/pyproject.toml b/pixl_core/pyproject.toml index ebedb8296..4a4efbea7 100644 --- a/pixl_core/pyproject.toml +++ b/pixl_core/pyproject.toml @@ -23,6 +23,7 @@ dependencies = [ "pandas==1.5.1", "pyarrow==14.0.1", "PyYAML==6.0.1", + "pydantic==2.6.3", ] [project.optional-dependencies] From d0f2ac2893b54250c5356a168c93f3e33ae19f1e Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Thu, 29 Feb 2024 15:37:37 +0000 Subject: [PATCH 090/120] Revert "Format docker-compose" This reverts commit 54d3cee7ebda02d41d29ac50aa7bffa052d7d40d. --- docker-compose.yml | 584 ++++++++++++++++++++++----------------------- 1 file changed, 280 insertions(+), 304 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 29c56a964..5e04cbfb1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,335 +20,311 @@ x-http-proxy: &http-proxy ${HTTP_PROXY} x-https-proxy: &https-proxy ${HTTPS_PROXY} x-no-proxy: &no-proxy localhost,0.0.0.0,127.0.0.1,uclvlddpragae07,hasher-api,orthanc-raw x-proxy-common: &proxy-common - HTTP_PROXY: *http-proxy - http_proxy: *http-proxy - HTTPS_PROXY: *https-proxy - https_proxy: *https-proxy - NO_PROXY: *no-proxy - no_proxy: *no-proxy + HTTP_PROXY: *http-proxy + http_proxy: *http-proxy + HTTPS_PROXY: *https-proxy + https_proxy: *https-proxy + NO_PROXY: *no-proxy + no_proxy: *no-proxy x-build-args-common: &build-args-common - <<: [*proxy-common] + <<: [*proxy-common] x-pixl-common-env: &pixl-common-env - ENV: ${ENV} - DEBUG: ${DEBUG} + ENV: ${ENV} + DEBUG: ${DEBUG} x-pixl-rabbit-mq: &pixl-rabbit-mq - RABBITMQ_HOST: "queue" # Name of the queue service - RABBITMQ_PORT: "5672" - RABBITMQ_USERNAME: ${RABBITMQ_USERNAME} - RABBITMQ_PASSWORD: ${RABBITMQ_PASSWORD} + RABBITMQ_HOST: "queue" # Name of the queue service + RABBITMQ_PORT: "5672" + RABBITMQ_USERNAME: ${RABBITMQ_USERNAME} + RABBITMQ_PASSWORD: ${RABBITMQ_PASSWORD} x-emap-db: &emap-db - EMAP_UDS_HOST: ${EMAP_UDS_HOST} - EMAP_UDS_PORT: ${EMAP_UDS_PORT} - EMAP_UDS_NAME: ${EMAP_UDS_NAME} - EMAP_UDS_USER: ${EMAP_UDS_USER} - EMAP_UDS_PASSWORD: ${EMAP_UDS_PASSWORD} - EMAP_UDS_SCHEMA_NAME: ${EMAP_UDS_SCHEMA_NAME} + EMAP_UDS_HOST: ${EMAP_UDS_HOST} + EMAP_UDS_PORT: ${EMAP_UDS_PORT} + EMAP_UDS_NAME: ${EMAP_UDS_NAME} + EMAP_UDS_USER: ${EMAP_UDS_USER} + EMAP_UDS_PASSWORD: ${EMAP_UDS_PASSWORD} + EMAP_UDS_SCHEMA_NAME: ${EMAP_UDS_SCHEMA_NAME} x-pixl-db: &pixl-db - PIXL_DB_HOST: ${PIXL_DB_HOST} - PIXL_DB_PORT: ${PIXL_DB_PORT} - PIXL_DB_USER: ${PIXL_DB_USER} - PIXL_DB_PASSWORD: ${PIXL_DB_PASSWORD} - PIXL_DB_NAME: ${PIXL_DB_NAME} + PIXL_DB_HOST: ${PIXL_DB_HOST} + PIXL_DB_PORT: ${PIXL_DB_PORT} + PIXL_DB_USER: ${PIXL_DB_USER} + PIXL_DB_PASSWORD: ${PIXL_DB_PASSWORD} + PIXL_DB_NAME: ${PIXL_DB_NAME} x-azure-keyvault: &azure-keyvault - AZURE_CLIENT_ID: ${EXPORT_AZ_CLIENT_ID} - AZURE_CLIENT_SECRET: ${EXPORT_AZ_CLIENT_PASSWORD} - AZURE_TENANT_ID: ${EXPORT_AZ_TENANT_ID} - AZURE_KEY_VAULT_NAME: ${EXPORT_AZ_KEY_VAULT_NAME} + AZURE_CLIENT_ID: ${EXPORT_AZ_CLIENT_ID} + AZURE_CLIENT_SECRET: ${EXPORT_AZ_CLIENT_PASSWORD} + AZURE_TENANT_ID: ${EXPORT_AZ_TENANT_ID} + AZURE_KEY_VAULT_NAME: ${EXPORT_AZ_KEY_VAULT_NAME} x-logs-volume: &logs-volume - type: volume - source: logs - target: /logs + type: volume + source: logs + target: /logs volumes: - logs: - orthanc-anon-data: - orthanc-raw-data: - postgres-data: - exports: + logs: + orthanc-anon-data: + orthanc-raw-data: + postgres-data: + exports: networks: - pixl-net: + pixl-net: ################################################################################ # Services services: - hasher-api: - build: - context: . - dockerfile: ./docker/hasher-api/Dockerfile - args: - <<: *build-args-common - environment: - <<: [*proxy-common, *pixl-common-env] - AZURE_CLIENT_ID: ${HASHER_API_AZ_CLIENT_ID} - AZURE_CLIENT_SECRET: ${HASHER_API_AZ_CLIENT_PASSWORD} - AZURE_TENANT_ID: ${HASHER_API_AZ_TENANT_ID} - AZURE_KEY_VAULT_NAME: ${HASHER_API_AZ_KEY_VAULT_NAME} - AZURE_KEY_VAULT_SECRET_NAME: ${HASHER_API_AZ_KEY_VAULT_SECRET_NAME} - env_file: - - ./docker/common.env - ports: - - "${HASHER_API_PORT}:8000" - volumes: - - *logs-volume - networks: - - pixl-net - healthcheck: - test: ["CMD", "curl", "-f", "http://hasher-api:8000/heart-beat"] - interval: 10s - timeout: 30s - retries: 5 - restart: "no" - orthanc-anon: - build: - context: . - dockerfile: ./docker/orthanc-anon/Dockerfile - args: - <<: *build-args-common - command: /run/secrets - environment: - <<: [*proxy-common, *pixl-common-env, *azure-keyvault] - ORTHANC_NAME: "PIXL: Anon" - ORTHANC_USERNAME: ${ORTHANC_ANON_USERNAME} - ORTHANC_PASSWORD: ${ORTHANC_ANON_PASSWORD} - ORTHANC_ANON_AE_TITLE: ${ORTHANC_ANON_AE_TITLE} - ORTHANC_AUTOROUTE_ANON_TO_ENDPOINT: ${ORTHANC_AUTOROUTE_ANON_TO_ENDPOINT} - ORTHANC_RAW_AE_TITLE: ${ORTHANC_RAW_AE_TITLE} - ORTHANC_RAW_DICOM_PORT: "4242" - ORTHANC_RAW_HOSTNAME: "orthanc-raw" - PIXL_DB_HOST: ${PIXL_DB_HOST} - PIXL_DB_PORT: ${PIXL_DB_PORT} - PIXL_DB_NAME: ${PIXL_DB_NAME} - PIXL_DB_USER: ${PIXL_DB_USER} - PIXL_DB_PASSWORD: ${PIXL_DB_PASSWORD} - DICOM_WEB_PLUGIN_ENABLED: ${ENABLE_DICOM_WEB} - HASHER_API_AZ_NAME: "hasher-api" - HASHER_API_PORT: 8000 - HTTP_TIMEOUT: ${ORTHANC_ANON_HTTP_TIMEOUT} - AZ_DICOM_ENDPOINT_NAME: ${AZ_DICOM_ENDPOINT_NAME} - AZ_DICOM_ENDPOINT_URL: ${AZ_DICOM_ENDPOINT_URL} - AZ_DICOM_ENDPOINT_TOKEN: ${AZ_DICOM_ENDPOINT_TOKEN} - AZ_DICOM_ENDPOINT_CLIENT_ID: ${AZ_DICOM_ENDPOINT_CLIENT_ID} - AZ_DICOM_ENDPOINT_CLIENT_SECRET: ${AZ_DICOM_ENDPOINT_CLIENT_SECRET} - AZ_DICOM_ENDPOINT_TENANT_ID: ${AZ_DICOM_ENDPOINT_TENANT_ID} - AZ_DICOM_TOKEN_REFRESH_SECS: "600" - TIME_OFFSET: "${STUDY_TIME_OFFSET}" - SALT_VALUE: ${SALT_VALUE}" - PROJECT_CONFIGS_DIR: /${PROJECT_CONFIGS_DIR:-/projects/configs} - ports: - - "${ORTHANC_ANON_DICOM_PORT}:4242" - - "${ORTHANC_ANON_WEB_PORT}:8042" - volumes: - - type: volume - source: orthanc-anon-data - target: /var/lib/orthanc/db - - ${PWD}/orthanc/orthanc-anon/config:/run/secrets:ro - - ${PWD}/projects/configs:/${PROJECT_CONFIGS_DIR:-/projects/configs}:ro - networks: - - pixl-net - # needed for same reason as ehr-api - extra_hosts: - - "host.docker.internal:host-gateway" - depends_on: - postgres: - condition: service_healthy - healthcheck: - test: - [ - "CMD", - "curl", - "-f", - "-u", - "${ORTHANC_ANON_USERNAME}:${ORTHANC_ANON_PASSWORD}", - "http://orthanc-anon:8042/heart-beat", - ] - interval: 10s - timeout: 30s - retries: 5 - restart: "no" + hasher-api: + build: + context: . + dockerfile: ./docker/hasher-api/Dockerfile + args: + <<: *build-args-common + environment: + <<: [*proxy-common, *pixl-common-env] + AZURE_CLIENT_ID: ${HASHER_API_AZ_CLIENT_ID} + AZURE_CLIENT_SECRET: ${HASHER_API_AZ_CLIENT_PASSWORD} + AZURE_TENANT_ID: ${HASHER_API_AZ_TENANT_ID} + AZURE_KEY_VAULT_NAME: ${HASHER_API_AZ_KEY_VAULT_NAME} + AZURE_KEY_VAULT_SECRET_NAME: ${HASHER_API_AZ_KEY_VAULT_SECRET_NAME} + env_file: + - ./docker/common.env + ports: + - "${HASHER_API_PORT}:8000" + volumes: + - *logs-volume + networks: + - pixl-net + healthcheck: + test: [ "CMD", "curl", "-f", "http://hasher-api:8000/heart-beat" ] + interval: 10s + timeout: 30s + retries: 5 + restart: "no" - orthanc-raw: - build: - context: . - dockerfile: ./docker/orthanc-raw/Dockerfile - args: - <<: *build-args-common - ORTHANC_RAW_MAXIMUM_STORAGE_SIZE: ${ORTHANC_RAW_MAXIMUM_STORAGE_SIZE} - command: /run/secrets - environment: - <<: [*pixl-db, *proxy-common, *pixl-common-env] - ORTHANC_NAME: "PIXL: Raw" - ORTHANC_USERNAME: ${ORTHANC_RAW_USERNAME} - ORTHANC_PASSWORD: ${ORTHANC_RAW_PASSWORD} - ORTHANC_RAW_AE_TITLE: ${ORTHANC_RAW_AE_TITLE} - ORTHANC_AUTOROUTE_RAW_TO_ANON: ${ORTHANC_AUTOROUTE_RAW_TO_ANON} - ORTHANC_RAW_MAXIMUM_STORAGE_SIZE: ${ORTHANC_RAW_MAXIMUM_STORAGE_SIZE} - VNAQR_AE_TITLE: ${VNAQR_AE_TITLE} - VNAQR_DICOM_PORT: ${VNAQR_DICOM_PORT} - VNAQR_IP_ADDR: ${VNAQR_IP_ADDR} - ORTHANC_ANON_AE_TITLE: ${ORTHANC_ANON_AE_TITLE} - ORTHANC_ANON_DICOM_PORT: "4242" - ORTHANC_ANON_HOSTNAME: "orthanc-anon" - ports: - - "${ORTHANC_RAW_DICOM_PORT}:4242" - - "${ORTHANC_RAW_WEB_PORT}:8042" - volumes: - - type: volume - source: orthanc-raw-data - target: /var/lib/orthanc/db - networks: - - pixl-net - depends_on: - postgres: - condition: service_healthy - orthanc-anon: - condition: service_started - healthcheck: - test: - [ - "CMD", - "curl", - "-f", - "-u", - "${ORTHANC_RAW_USERNAME}:${ORTHANC_RAW_PASSWORD}", - "http://orthanc-raw:8042/heart-beat", - ] - interval: 10s - timeout: 30s - retries: 5 - restart: "no" + orthanc-anon: + build: + context: . + dockerfile: ./docker/orthanc-anon/Dockerfile + args: + <<: *build-args-common + command: /run/secrets + environment: + <<: [*proxy-common, *pixl-common-env, *azure-keyvault] + ORTHANC_NAME: "PIXL: Anon" + ORTHANC_USERNAME: ${ORTHANC_ANON_USERNAME} + ORTHANC_PASSWORD: ${ORTHANC_ANON_PASSWORD} + ORTHANC_ANON_AE_TITLE: ${ORTHANC_ANON_AE_TITLE} + ORTHANC_AUTOROUTE_ANON_TO_ENDPOINT: ${ORTHANC_AUTOROUTE_ANON_TO_ENDPOINT} + ORTHANC_RAW_AE_TITLE: ${ORTHANC_RAW_AE_TITLE} + ORTHANC_RAW_DICOM_PORT: "4242" + ORTHANC_RAW_HOSTNAME: "orthanc-raw" + PIXL_DB_HOST: ${PIXL_DB_HOST} + PIXL_DB_PORT: ${PIXL_DB_PORT} + PIXL_DB_NAME: ${PIXL_DB_NAME} + PIXL_DB_USER: ${PIXL_DB_USER} + PIXL_DB_PASSWORD: ${PIXL_DB_PASSWORD} + DICOM_WEB_PLUGIN_ENABLED: ${ENABLE_DICOM_WEB} + HASHER_API_AZ_NAME: "hasher-api" + HASHER_API_PORT: 8000 + HTTP_TIMEOUT: ${ORTHANC_ANON_HTTP_TIMEOUT} + AZ_DICOM_ENDPOINT_NAME: ${AZ_DICOM_ENDPOINT_NAME} + AZ_DICOM_ENDPOINT_URL: ${AZ_DICOM_ENDPOINT_URL} + AZ_DICOM_ENDPOINT_TOKEN: ${AZ_DICOM_ENDPOINT_TOKEN} + AZ_DICOM_ENDPOINT_CLIENT_ID: ${AZ_DICOM_ENDPOINT_CLIENT_ID} + AZ_DICOM_ENDPOINT_CLIENT_SECRET: ${AZ_DICOM_ENDPOINT_CLIENT_SECRET} + AZ_DICOM_ENDPOINT_TENANT_ID: ${AZ_DICOM_ENDPOINT_TENANT_ID} + AZ_DICOM_TOKEN_REFRESH_SECS: "600" + TIME_OFFSET: "${STUDY_TIME_OFFSET}" + SALT_VALUE: ${SALT_VALUE}" + ports: + - "${ORTHANC_ANON_DICOM_PORT}:4242" + - "${ORTHANC_ANON_WEB_PORT}:8042" + volumes: + - type: volume + source: orthanc-anon-data + target: /var/lib/orthanc/db + - ${PWD}/orthanc/orthanc-anon/config:/run/secrets:ro + - ${PWD}/projects/configs:/${PROJECT_CONFIGS_DIR:-/projects/configs}:ro + networks: + - pixl-net + # needed for same reason as ehr-api + extra_hosts: + - "host.docker.internal:host-gateway" + depends_on: + postgres: + condition: service_healthy + healthcheck: + test: [ "CMD", "curl", "-f", "-u" , "${ORTHANC_ANON_USERNAME}:${ORTHANC_ANON_PASSWORD}", "http://orthanc-anon:8042/heart-beat" ] + interval: 10s + timeout: 30s + retries: 5 + restart: "no" - queue: - image: rabbitmq:3.12.9-management - environment: - RABBITMQ_DEFAULT_USER: ${RABBITMQ_USERNAME} - RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD} - healthcheck: - test: rabbitmq-diagnostics -q check_running - interval: 30s - timeout: 30s - retries: 3 - ports: - - "${RABBITMQ_PORT}:5672" - - "${RABBITMQ_ADMIN_PORT}:15672" - networks: - - pixl-net + orthanc-raw: + build: + context: . + dockerfile: ./docker/orthanc-raw/Dockerfile + args: + <<: *build-args-common + ORTHANC_RAW_MAXIMUM_STORAGE_SIZE: ${ORTHANC_RAW_MAXIMUM_STORAGE_SIZE} + command: /run/secrets + environment: + <<: [*pixl-db, *proxy-common, *pixl-common-env] + ORTHANC_NAME: "PIXL: Raw" + ORTHANC_USERNAME: ${ORTHANC_RAW_USERNAME} + ORTHANC_PASSWORD: ${ORTHANC_RAW_PASSWORD} + ORTHANC_RAW_AE_TITLE: ${ORTHANC_RAW_AE_TITLE} + ORTHANC_AUTOROUTE_RAW_TO_ANON: ${ORTHANC_AUTOROUTE_RAW_TO_ANON} + ORTHANC_RAW_MAXIMUM_STORAGE_SIZE: ${ORTHANC_RAW_MAXIMUM_STORAGE_SIZE} + VNAQR_AE_TITLE : ${VNAQR_AE_TITLE} + VNAQR_DICOM_PORT: ${VNAQR_DICOM_PORT} + VNAQR_IP_ADDR: ${VNAQR_IP_ADDR} + ORTHANC_ANON_AE_TITLE: ${ORTHANC_ANON_AE_TITLE} + ORTHANC_ANON_DICOM_PORT: "4242" + ORTHANC_ANON_HOSTNAME: "orthanc-anon" + ports: + - "${ORTHANC_RAW_DICOM_PORT}:4242" + - "${ORTHANC_RAW_WEB_PORT}:8042" + volumes: + - type: volume + source: orthanc-raw-data + target: /var/lib/orthanc/db + networks: + - pixl-net + depends_on: + postgres: + condition: service_healthy + orthanc-anon: + condition: service_started + healthcheck: + test: [ "CMD", "curl", "-f", "-u" , "${ORTHANC_RAW_USERNAME}:${ORTHANC_RAW_PASSWORD}", "http://orthanc-raw:8042/heart-beat" ] + interval: 10s + timeout: 30s + retries: 5 + restart: "no" - ehr-api: - build: - context: . - dockerfile: ./docker/ehr-api/Dockerfile - args: - <<: *build-args-common - environment: - <<: - [ - *pixl-db, - *emap-db, - *proxy-common, - *pixl-common-env, - *pixl-rabbit-mq, - *azure-keyvault, - ] - AZ_STORAGE_ACCOUNT_NAME: ${PIXL_EHR_API_AZ_STORAGE_ACCOUNT_NAME} - AZ_STORAGE_CONTAINER_NAME: ${PIXL_EHR_API_AZ_STORAGE_CONTAINER_NAME} - COGSTACK_REDACT_URL: ${PIXL_EHR_COGSTACK_REDACT_URL} - PROJECT_CONFIGS_DIR: /${PROJECT_CONFIGS_DIR:-/projects/configs} - env_file: - - ./docker/common.env - depends_on: - queue: - condition: service_healthy - postgres: - condition: service_healthy - hasher-api: - condition: service_healthy - ports: - - "${PIXL_EHR_API_PORT}:8000" - healthcheck: - interval: 10s - timeout: 30s - retries: 5 - networks: - - pixl-net - # needed for testing under GHA (linux), so this container - # can reach the test FTP server running on the docker host - extra_hosts: - - "host.docker.internal:host-gateway" - volumes: - - ${PWD}/projects/exports:/run/projects/exports - - ${PWD}/projects/configs:/${PROJECT_CONFIGS_DIR:-/projects/configs}:ro + queue: + image: rabbitmq:3.12.9-management + environment: + RABBITMQ_DEFAULT_USER: ${RABBITMQ_USERNAME} + RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD} + healthcheck: + test: rabbitmq-diagnostics -q check_running + interval: 30s + timeout: 30s + retries: 3 + ports: + - "${RABBITMQ_PORT}:5672" + - "${RABBITMQ_ADMIN_PORT}:15672" + networks: + - pixl-net - imaging-api: - build: - context: . - dockerfile: ./docker/imaging-api/Dockerfile - args: - <<: *build-args-common - depends_on: - queue: - condition: service_healthy - orthanc-raw: - condition: service_healthy - healthcheck: - test: curl -f http://0.0.0.0:8000/heart-beat - interval: 10s - timeout: 30s - retries: 5 - networks: - - pixl-net - environment: - <<: [*pixl-rabbit-mq, *proxy-common] - ORTHANC_RAW_USERNAME: ${ORTHANC_RAW_USERNAME} - ORTHANC_RAW_PASSWORD: ${ORTHANC_RAW_PASSWORD} - ORTHANC_RAW_AE_TITLE: ${ORTHANC_RAW_AE_TITLE} - VNAQR_MODALITY: ${VNAQR_MODALITY} - SKIP_ALEMBIC: ${SKIP_ALEMBIC} - PIXL_DB_HOST: ${PIXL_DB_HOST} - PIXL_DB_PORT: ${PIXL_DB_PORT} - PIXL_DB_NAME: ${PIXL_DB_NAME} - PIXL_DB_USER: ${PIXL_DB_USER} - PIXL_DB_PASSWORD: ${PIXL_DB_PASSWORD} - PIXL_DICOM_TRANSFER_TIMEOUT: ${PIXL_DICOM_TRANSFER_TIMEOUT} - PIXL_QUERY_TIMEOUT: ${PIXL_QUERY_TIMEOUT} - ports: - - "${PIXL_IMAGING_API_PORT}:8000" + ehr-api: + build: + context: . + dockerfile: ./docker/ehr-api/Dockerfile + args: + <<: *build-args-common + environment: + <<: [*pixl-db, *emap-db, *proxy-common, *pixl-common-env, *pixl-rabbit-mq, *azure-keyvault] + AZ_STORAGE_ACCOUNT_NAME: ${PIXL_EHR_API_AZ_STORAGE_ACCOUNT_NAME} + AZ_STORAGE_CONTAINER_NAME: ${PIXL_EHR_API_AZ_STORAGE_CONTAINER_NAME} + COGSTACK_REDACT_URL: ${PIXL_EHR_COGSTACK_REDACT_URL} + PROJECT_CONFIGS_DIR: /${PROJECT_CONFIGS_DIR:-/projects/configs} + env_file: + - ./docker/common.env + depends_on: + queue: + condition: service_healthy + postgres: + condition: service_healthy + hasher-api: + condition: service_healthy + ports: + - "${PIXL_EHR_API_PORT}:8000" + healthcheck: + interval: 10s + timeout: 30s + retries: 5 + networks: + - pixl-net + # needed for testing under GHA (linux), so this container + # can reach the test FTP server running on the docker host + extra_hosts: + - "host.docker.internal:host-gateway" + volumes: + - ${PWD}/exports:/run/exports + - ${PWD}/projects/configs:/${PROJECT_CONFIGS_DIR:-/projects/configs}:ro - ################################################################################ - # Data Stores - postgres: - build: - context: . - dockerfile: ./docker/postgres/Dockerfile - args: - <<: *build-args-common - environment: - POSTGRES_USER: ${PIXL_DB_USER} - POSTGRES_PASSWORD: ${PIXL_DB_PASSWORD} - POSTGRES_DB: ${PIXL_DB_NAME} - PGTZ: Europe/London - env_file: - - ./docker/common.env - command: postgres -c 'config_file=/etc/postgresql/postgresql.conf' - volumes: - - type: volume - source: postgres-data - target: /var/lib/postgresql/data - ports: - - "${POSTGRES_PORT}:5432" - healthcheck: - test: ["CMD", "pg_isready", "-U", "${PIXL_DB_USER}"] - interval: 10s - timeout: 30s - retries: 5 - restart: always - networks: - - pixl-net + imaging-api: + build: + context: . + dockerfile: ./docker/imaging-api/Dockerfile + args: + <<: *build-args-common + depends_on: + queue: + condition: service_healthy + orthanc-raw: + condition: service_healthy + healthcheck: + test: curl -f http://0.0.0.0:8000/heart-beat + interval: 10s + timeout: 30s + retries: 5 + networks: + - pixl-net + environment: + <<: [*pixl-rabbit-mq, *proxy-common] + ORTHANC_RAW_USERNAME: ${ORTHANC_RAW_USERNAME} + ORTHANC_RAW_PASSWORD: ${ORTHANC_RAW_PASSWORD} + ORTHANC_RAW_AE_TITLE: ${ORTHANC_RAW_AE_TITLE} + VNAQR_MODALITY: ${VNAQR_MODALITY} + SKIP_ALEMBIC: ${SKIP_ALEMBIC} + PIXL_DB_HOST: ${PIXL_DB_HOST} + PIXL_DB_PORT: ${PIXL_DB_PORT} + PIXL_DB_NAME: ${PIXL_DB_NAME} + PIXL_DB_USER: ${PIXL_DB_USER} + PIXL_DB_PASSWORD: ${PIXL_DB_PASSWORD} + PIXL_DICOM_TRANSFER_TIMEOUT: ${PIXL_DICOM_TRANSFER_TIMEOUT} + PIXL_QUERY_TIMEOUT: ${PIXL_QUERY_TIMEOUT} + ports: + - "${PIXL_IMAGING_API_PORT}:8000" + +################################################################################ +# Data Stores + postgres: + build: + context: . + dockerfile: ./docker/postgres/Dockerfile + args: + <<: *build-args-common + environment: + POSTGRES_USER: ${PIXL_DB_USER} + POSTGRES_PASSWORD: ${PIXL_DB_PASSWORD} + POSTGRES_DB: ${PIXL_DB_NAME} + PGTZ: Europe/London + env_file: + - ./docker/common.env + command: postgres -c 'config_file=/etc/postgresql/postgresql.conf' + volumes: + - type: volume + source: postgres-data + target: /var/lib/postgresql/data + ports: + - "${POSTGRES_PORT}:5432" + healthcheck: + test: [ "CMD", "pg_isready", "-U", "${PIXL_DB_USER}" ] + interval: 10s + timeout: 30s + retries: 5 + restart: always + networks: + - pixl-net From 57796233c8901bc900dcb9f571fd98091e484e56 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Thu, 29 Feb 2024 15:55:25 +0000 Subject: [PATCH 091/120] Cache secret fetching from AZ keyvault --- pixl_core/src/core/_secrets.py | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/pixl_core/src/core/_secrets.py b/pixl_core/src/core/_secrets.py index a8271bcf8..c9fe2e834 100644 --- a/pixl_core/src/core/_secrets.py +++ b/pixl_core/src/core/_secrets.py @@ -15,6 +15,7 @@ """Handles fetching of project secrets from the Azure Keyvault""" import subprocess +from functools import lru_cache from azure.identity import DefaultAzureCredential from azure.keyvault.secrets import SecretClient @@ -51,14 +52,10 @@ def _connect_to_keyvault(self) -> SecretClient: def fetch_secret(self, secret_name: str) -> str: """ Fetch a secret from the Azure Key Vault instance specified in the environment variables. + :param secret_name: the name of the secret to fetch :return: the requested secret's value """ - secret = self.client.get_secret(secret_name).value - if secret is None: - msg = f"Azure Key Vault secret {secret_name} is None" - raise ValueError(msg) - - return str(secret) + return _fetch_secret(self.client, secret_name) def _check_envvars(self) -> None: """ @@ -77,3 +74,24 @@ def _check_system_envvar(var_name: str) -> None: error_msg = f"Environment variable {var_name} not set" if not subprocess.check_output(f"echo ${var_name}", shell=True).decode().strip(): # noqa: S602 raise OSError(error_msg) + + +@lru_cache +def _fetch_secret(client: SecretClient, secret_name: str) -> str: + """ + Fetch a secret from the Azure Key Vault instance specified in the environment variables. + This method is cached to avoid unnecessary calls to the Key Vault using the LRU (least + recently used) strategy. + + This helper is intentionally defined outside the Azure Keyvault to prevent memory leaks (see + ruff rule B019). + + :param client: a SecretClient instance + :param secret_name: the name of the secret to fetch + :return: the requested secret's value + """ + secret = client.get_secret(secret_name).value + if secret is None: + msg = f"Azure Key Vault secret {secret_name} is None" + raise ValueError(msg) + return str(secret) From 438144f9677dc87d495abf04ca11cee66ec4e7e5 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Thu, 29 Feb 2024 15:59:36 +0000 Subject: [PATCH 092/120] Set secret ENV variables for the whole system-test job This should omit the need to set them explicitly in `.secrets.env` --- .github/workflows/main.yml | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6c196804d..5f27f6730 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -182,6 +182,11 @@ jobs: if: ${{ ! github.event.pull_request.draft || contains(github.event.pull_request.title, '[force-system-test]') }} runs-on: ubuntu-22.04 timeout-minutes: 30 + env: + EXPORT_AZ_CLIENT_ID: ${{ secrets.EXPORT_AZ_CLIENT_ID }} + EXPORT_AZ_CLIENT_PASSWORD: ${{ secrets.EXPORT_AZ_CLIENT_PASSWORD }} + EXPORT_AZ_TENANT_ID: ${{ secrets.EXPORT_AZ_TENANT_ID }} + EXPORT_AZ_KEY_VAULT_NAME: ${{ secrets.EXPORT_AZ_KEY_VAULT_NAME }} steps: - uses: actions/checkout@v3 - uses: docker/setup-buildx-action@v3 @@ -224,16 +229,7 @@ jobs: pip install -e pytest-pixl/ - name: Create .secrets.env - env: - EXPORT_AZ_CLIENT_ID: ${{ secrets.EXPORT_AZ_CLIENT_ID }} - EXPORT_AZ_CLIENT_PASSWORD: ${{ secrets.EXPORT_AZ_CLIENT_PASSWORD }} - EXPORT_AZ_TENANT_ID: ${{ secrets.EXPORT_AZ_TENANT_ID }} - EXPORT_AZ_KEY_VAULT_NAME: ${{ secrets.EXPORT_AZ_KEY_VAULT_NAME }} - run: | - echo EXPORT_AZ_CLIENT_ID=$EXPORT_AZ_CLIENT_ID >> .secrets.env - echo EXPORT_AZ_CLIENT_PASSWORD=$EXPORT_AZ_CLIENT_PASSWORD >> .secrets.env - echo EXPORT_AZ_TENANT_ID=$EXPORT_AZ_TENANT_ID >> .secrets.env - echo EXPORT_AZ_KEY_VAULT_NAME=$EXPORT_AZ_KEY_VAULT_NAME >> .secrets.env + run: touch .secrets.env - name: Build test services working-directory: test From 83a1b5bd4e58ef5a48c7be9a25bb47b6a32ecc46 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Thu, 29 Feb 2024 18:17:58 +0000 Subject: [PATCH 093/120] Fix Keyvault secret names in diagram --- .../diagrams/pixl-multi-project-config.drawio | 10 +++++----- .../diagrams/pixl-multi-project-config.png | Bin 128273 -> 127320 bytes 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/design/diagrams/pixl-multi-project-config.drawio b/docs/design/diagrams/pixl-multi-project-config.drawio index cde53b88c..aa6979bf3 100644 --- a/docs/design/diagrams/pixl-multi-project-config.drawio +++ b/docs/design/diagrams/pixl-multi-project-config.drawio @@ -1,6 +1,6 @@ - + - + @@ -97,13 +97,13 @@ - + - + - + diff --git a/docs/design/diagrams/pixl-multi-project-config.png b/docs/design/diagrams/pixl-multi-project-config.png index 78532c38ea1f4ed5876cf96986dc73990ceb373d..986c54d1bd199ce15dd1e8ff92e4f680a5bc7c66 100644 GIT binary patch delta 94794 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 g_Ba==Q+6Zc6JEd-KknX(qV?q(YldJj38H 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

  • l-8XY2`^Ahq9UE%2~1bu&|SpzROt0)uByb*+UJA{Z4I z|FW1wwpeePqNnTzJh0XI*Y)V#lP0?K89$ZWv?Xg7huu1{NTCK6}6?tV7emfdwo%oXS3`%NF%0 zv|Ko(7gzB$b*h(};gdURi@{Y7@Ki5_#_PbiE(}K42aJW*2DKi8`_{+%S7s=ixJd1E zmdSK_@#ouv@T3>};FJLM2tHn) zU<`D@2O(g>i`yh1gB+s3plt{S>Ya1w*lbWxJtzfLR^VWSCm07NH?;*S6DO)4*a*t7 z4d#L~KDb8aOYt3dLP}N+O!YwL?&93@5jgY&Y(O0YwkW~%2{Z=a>;vY&67c7 Date: Thu, 29 Feb 2024 18:54:36 +0000 Subject: [PATCH 094/120] Add more abstract methods into the base Uploader class --- pixl_core/src/core/upload.py | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/pixl_core/src/core/upload.py b/pixl_core/src/core/upload.py index 9a316c11f..8b2361532 100644 --- a/pixl_core/src/core/upload.py +++ b/pixl_core/src/core/upload.py @@ -20,7 +20,7 @@ from abc import ABC, abstractmethod from datetime import datetime, timezone from pathlib import Path -from typing import TYPE_CHECKING, BinaryIO +from typing import TYPE_CHECKING, Any, BinaryIO if TYPE_CHECKING: from core.exports import ParquetExport @@ -48,21 +48,40 @@ def __init__(self, project_config: PixlConfig) -> None: AzureKeyvault instance. The keyvault is used to fetch the secrets required to connect to the remote destination. + Child classes should implement the _set_config method to set the configuration for the + upload strategy. + :param project: The project name for which the uploader is being initialised. Used to fetch the correct secrets from the keyvault. """ self.project_config = project_config self.keyvault = AzureKeyVault() + self._set_config() + + @abstractmethod + def _set_config(self) -> None: + """Set the configuration for the uploader.""" + + @abstractmethod + def upload_dicom_image(self, *args: Any, **kwargs: Any) -> None: + """ + Abstract method to upload DICOM images. To be overwritten by child classes. + If an upload strategy does not support DICOM images, this method should raise a + NotImplementedError. + """ + + @abstractmethod + def upload_parquet_files(self, *args: Any, **kwargs: Any) -> None: + """ + Abstract method to upload parquet files. To be overwritten by child classes. + If an upload strategy does not support parquet files, this method should raise a + NotImplementedError. + """ class FTPSUploader(Uploader): """Upload strategy for an FTPS server.""" - def __init__(self, project_config: PixlConfig) -> None: - """Initialise the uploader with the destination configuration.""" - Uploader.__init__(self, project_config) - self._set_config() - def _set_config(self) -> None: # Use the Azure KV alias as prefix if it exists, otherwise use the project name az_prefix = self.project_config.project.azure_kv_alias From f1e60ceeefe6ede36200cbb7d93123196a7e49fa Mon Sep 17 00:00:00 2001 From: peshence Date: Thu, 29 Feb 2024 16:50:43 +0000 Subject: [PATCH 095/120] add slugify reference --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 77efc8746..30a1faf6c 100644 --- a/README.md +++ b/README.md @@ -205,9 +205,9 @@ To configure a new project, follow these steps: 1. The filename of the project config should be ``.yaml >[!NOTE] - > The project slug should match the project name in the `extract_summary.json` log file! + > The project slug should match the [slugify](https://github.com/un33k/python-slugify)-ed project name in the `extract_summary.json` log file! -1. [Open a PR in PIXL](https://github.com/UCLH-Foundry/PIXL/compare) to merge the new project config into `main` +2. [Open a PR in PIXL](https://github.com/UCLH-Foundry/PIXL/compare) to merge the new project config into `main` #### The config YAML file From 2a557b2be578ceadd73087108304254ffcd7ebe0 Mon Sep 17 00:00:00 2001 From: peshence Date: Thu, 29 Feb 2024 17:05:53 +0000 Subject: [PATCH 096/120] fix[dcmd]: type hints --- pixl_dcmd/src/pixl_dcmd/main.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pixl_dcmd/src/pixl_dcmd/main.py b/pixl_dcmd/src/pixl_dcmd/main.py index 87ad51509..879f708e4 100644 --- a/pixl_dcmd/src/pixl_dcmd/main.py +++ b/pixl_dcmd/src/pixl_dcmd/main.py @@ -93,7 +93,7 @@ def anonymise_dicom(dataset: Dataset) -> Dataset: return dataset -def merge_tag_schemes(tag_operation_files: list[Path]) -> Any: +def merge_tag_schemes(tag_operation_files: list[Path]) -> list[dict]: """ NOT IMPLEMENTED, WORKS ONLY WITH A SINGLE TAG SCHEME Merge multiple tag schemes into a single dictionary. @@ -103,6 +103,10 @@ def merge_tag_schemes(tag_operation_files: list[Path]) -> Any: with tag_operation_files[0].open() as file: # Load tag operations scheme from YAML. tags = yaml.safe_load(file) + if not isinstance(tags, list) or not all( + [isinstance(tag, dict) for tag in tags] + ): + raise ValueError("Tag operation file must contain a list of dictionaries") return tags @@ -138,7 +142,7 @@ def remove_overlays(dataset: Dataset) -> Dataset: return dataset -def enforce_whitelist(dataset: dict, tags: list[dict]) -> dict: +def enforce_whitelist(dataset: Dataset, tags: list[dict]) -> Dataset: """Delete any tags not in the tagging scheme.""" # For every element: logger.debug("Enforcing whitelist") @@ -167,7 +171,7 @@ def enforce_whitelist(dataset: dict, tags: list[dict]) -> dict: return dataset -def apply_tag_scheme(dataset: dict, tags: list[dict]) -> dict: +def apply_tag_scheme(dataset: Dataset, tags: list[dict]) -> Dataset: """ Apply anonymisation operations for a given set of tags to a dataset. The original study time is kept before any operations are applied. From 682a882f9cd68fb0deb9f3adcd78ba13c9f88bfe Mon Sep 17 00:00:00 2001 From: peshence Date: Thu, 29 Feb 2024 19:12:10 +0000 Subject: [PATCH 097/120] refactor[core]: define uploader subpackage --- orthanc/orthanc-anon/plugin/pixl.py | 4 +- pixl_core/src/core/_upload_ftps.py | 91 -------------- pixl_core/src/core/exports.py | 2 +- pixl_core/src/core/uploader/__init__.py | 22 ++++ pixl_core/src/core/uploader/_base.py | 69 +++++++++++ pixl_core/src/core/{ => uploader}/_secrets.py | 0 .../src/core/{upload.py => uploader/ftps.py} | 111 ++++++++++-------- pixl_core/tests/conftest.py | 2 +- .../{test_upload.py => uploader/test_ftps.py} | 2 +- .../test_ftpserver_login.py | 2 +- 10 files changed, 162 insertions(+), 143 deletions(-) delete mode 100644 pixl_core/src/core/_upload_ftps.py create mode 100644 pixl_core/src/core/uploader/__init__.py create mode 100644 pixl_core/src/core/uploader/_base.py rename pixl_core/src/core/{ => uploader}/_secrets.py (100%) rename pixl_core/src/core/{upload.py => uploader/ftps.py} (69%) rename pixl_core/tests/{test_upload.py => uploader/test_ftps.py} (98%) diff --git a/orthanc/orthanc-anon/plugin/pixl.py b/orthanc/orthanc-anon/plugin/pixl.py index f66b6b3c8..1914cf6ad 100644 --- a/orthanc/orthanc-anon/plugin/pixl.py +++ b/orthanc/orthanc-anon/plugin/pixl.py @@ -30,9 +30,9 @@ from typing import TYPE_CHECKING import requests -from core import upload from core.db.queries import get_project_slug_from_hashid from core.project_config import load_project_config +from core.uploader.ftps import FTPSUploader from decouple import config from pydicom import dcmread @@ -150,7 +150,7 @@ def Send(resourceId: str) -> None: logger.debug(msg) zip_content = _get_study_zip_archive(resourceId) - upload.FTPSUploader(project_config).upload_dicom_image(zip_content, hashed_patient_id) + FTPSUploader(project_config).upload_dicom_image(zip_content, hashed_patient_id) else: msg = f"Invalid destination: {project_config.destination.dicom}" raise ValueError(msg) diff --git a/pixl_core/src/core/_upload_ftps.py b/pixl_core/src/core/_upload_ftps.py deleted file mode 100644 index f5d9bf5f9..000000000 --- a/pixl_core/src/core/_upload_ftps.py +++ /dev/null @@ -1,91 +0,0 @@ -# Copyright (c) University College London Hospitals NHS Foundation Trust -# -# 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 -# -# http://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. - -"""Helper functions for setting up a connection to an FTPS server.""" - -from __future__ import annotations - -import ftplib -import logging -import ssl -from ftplib import FTP_TLS -from typing import TYPE_CHECKING, Any - -if TYPE_CHECKING: - from pathlib import Path - from socket import socket - -logger = logging.getLogger(__name__) - - -class ImplicitFtpTls(ftplib.FTP_TLS): - """ - FTP_TLS subclass that automatically wraps sockets in SSL to support implicit FTPS. - - https://stackoverflow.com/questions/12164470/python-ftp-implicit-tls-connection-issue - """ - - def __init__(self, *args: Any, **kwargs: Any) -> None: - """Create instance from parent class.""" - super().__init__(*args, **kwargs) - self._sock: socket | None = None - - @property - def sock(self) -> socket | None: - """Return the socket.""" - return self._sock - - @sock.setter - def sock(self, value: socket) -> None: - """When modifying the socket, ensure that it is ssl wrapped.""" - if value is not None and not isinstance(value, ssl.SSLSocket): - value = self.context.wrap_socket(value) - self._sock = value - - -def _connect_to_ftp(ftp_host: str, ftp_port: int, ftp_user: str, ftp_password: str) -> FTP_TLS: - # Connect to the server and login - try: - ftp = ImplicitFtpTls() - ftp.connect(ftp_host, int(ftp_port)) - ftp.login(ftp_user, ftp_password) - ftp.prot_p() - except ftplib.all_errors as ftp_error: - error_msg = "Failed to connect to FTPS server" - raise ConnectionError(error_msg, ftp_error) from ftp_error - return ftp - - -def _create_and_set_as_cwd_multi_path(ftp: FTP_TLS, remote_multi_dir: Path) -> None: - """Create (and cwd into) a multi dir path, analogously to mkdir -p""" - if remote_multi_dir.is_absolute(): - # would require some special handling and we don't need it - err = "must be relative path" - raise ValueError(err) - logger.info("_create_and_set_as_cwd_multi_path %s", remote_multi_dir) - # path should be pretty normalised, so assume split is safe - sub_dirs = str(remote_multi_dir).split("/") - for sd in sub_dirs: - _create_and_set_as_cwd(ftp, sd) - - -def _create_and_set_as_cwd(ftp: FTP_TLS, project_dir: str) -> None: - try: - ftp.cwd(project_dir) - logger.debug("'%s' exists on remote ftp, so moving into it", project_dir) - except ftplib.error_perm: - logger.info("creating '%s' on remote ftp and moving into it", project_dir) - # Directory doesn't exist, so create it - ftp.mkd(project_dir) - ftp.cwd(project_dir) diff --git a/pixl_core/src/core/exports.py b/pixl_core/src/core/exports.py index 29871d93c..7ff64a9b6 100644 --- a/pixl_core/src/core/exports.py +++ b/pixl_core/src/core/exports.py @@ -21,7 +21,7 @@ import slugify from core.project_config import load_project_config -from core.upload import FTPSUploader +from core.uploader.ftps import FTPSUploader if TYPE_CHECKING: import datetime diff --git a/pixl_core/src/core/uploader/__init__.py b/pixl_core/src/core/uploader/__init__.py new file mode 100644 index 000000000..4098fa2b1 --- /dev/null +++ b/pixl_core/src/core/uploader/__init__.py @@ -0,0 +1,22 @@ +# Copyright (c) University College London Hospitals NHS Foundation Trust +# +# 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 +# +# http://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. +""" +Uploader package +Contains base uploader class and +- FTPS uploader class +- **in progress** Azure uploader class +- **in progress** DICOMWeb uploader class + +Uploader class gets appropriate secret credentials from Azure key vault and uploads data +""" diff --git a/pixl_core/src/core/uploader/_base.py b/pixl_core/src/core/uploader/_base.py new file mode 100644 index 000000000..97fb84744 --- /dev/null +++ b/pixl_core/src/core/uploader/_base.py @@ -0,0 +1,69 @@ +# Copyright (c) University College London Hospitals NHS Foundation Trust +# +# 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 +# +# http://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. +"""Functionality to upload files to a remote server.""" + +from __future__ import annotations + +import logging +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from core.project_config import PixlConfig + + +from core.uploader._secrets import AzureKeyVault + +logger = logging.getLogger(__name__) + + +class Uploader(ABC): + """Upload strategy interface.""" + + @abstractmethod + def __init__(self, project_config: PixlConfig) -> None: + """ + Initialise the uploader for a specific project with the destination configuration and an + AzureKeyvault instance. The keyvault is used to fetch the secrets required to connect to + the remote destination. + + Child classes should implement the _set_config method to set the configuration for the + upload strategy. + + :param project: The project name for which the uploader is being initialised. Used to fetch + the correct secrets from the keyvault. + """ + self.project_config = project_config + self.keyvault = AzureKeyVault() + self._set_config() + + @abstractmethod + def _set_config(self) -> None: + """Set the configuration for the uploader.""" + + @abstractmethod + def upload_dicom_image(self, *args: Any, **kwargs: Any) -> None: + """ + Abstract method to upload DICOM images. To be overwritten by child classes. + If an upload strategy does not support DICOM images, this method should raise a + NotImplementedError. + """ + + @abstractmethod + def upload_parquet_files(self, *args: Any, **kwargs: Any) -> None: + """ + Abstract method to upload parquet files. To be overwritten by child classes. + If an upload strategy does not support parquet files, this method should raise a + NotImplementedError. + """ diff --git a/pixl_core/src/core/_secrets.py b/pixl_core/src/core/uploader/_secrets.py similarity index 100% rename from pixl_core/src/core/_secrets.py rename to pixl_core/src/core/uploader/_secrets.py diff --git a/pixl_core/src/core/upload.py b/pixl_core/src/core/uploader/ftps.py similarity index 69% rename from pixl_core/src/core/upload.py rename to pixl_core/src/core/uploader/ftps.py index 8b2361532..0ed459768 100644 --- a/pixl_core/src/core/upload.py +++ b/pixl_core/src/core/uploader/ftps.py @@ -11,72 +11,54 @@ # 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. -"""Functionality to upload files to a remote server.""" + +"""Helper functions for setting up a connection to an FTPS server.""" from __future__ import annotations import ftplib import logging -from abc import ABC, abstractmethod +import ssl from datetime import datetime, timezone +from ftplib import FTP_TLS from pathlib import Path from typing import TYPE_CHECKING, Any, BinaryIO +from core.db.queries import get_project_slug_from_hashid, update_exported_at +from core.uploader._base import Uploader + if TYPE_CHECKING: + from socket import socket + from core.exports import ParquetExport from core.project_config import PixlConfig - -from core._secrets import AzureKeyVault -from core._upload_ftps import ( - _connect_to_ftp, - _create_and_set_as_cwd, - _create_and_set_as_cwd_multi_path, -) -from core.db.queries import get_project_slug_from_hashid, update_exported_at - logger = logging.getLogger(__name__) -class Uploader(ABC): - """Upload strategy interface.""" - - @abstractmethod - def __init__(self, project_config: PixlConfig) -> None: - """ - Initialise the uploader for a specific project with the destination configuration and an - AzureKeyvault instance. The keyvault is used to fetch the secrets required to connect to - the remote destination. - - Child classes should implement the _set_config method to set the configuration for the - upload strategy. +class ImplicitFtpTls(ftplib.FTP_TLS): + """ + FTP_TLS subclass that automatically wraps sockets in SSL to support implicit FTPS. - :param project: The project name for which the uploader is being initialised. Used to fetch - the correct secrets from the keyvault. - """ - self.project_config = project_config - self.keyvault = AzureKeyVault() - self._set_config() + https://stackoverflow.com/questions/12164470/python-ftp-implicit-tls-connection-issue + """ - @abstractmethod - def _set_config(self) -> None: - """Set the configuration for the uploader.""" + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Create instance from parent class.""" + super().__init__(*args, **kwargs) + self._sock: socket | None = None - @abstractmethod - def upload_dicom_image(self, *args: Any, **kwargs: Any) -> None: - """ - Abstract method to upload DICOM images. To be overwritten by child classes. - If an upload strategy does not support DICOM images, this method should raise a - NotImplementedError. - """ + @property + def sock(self) -> socket | None: + """Return the socket.""" + return self._sock - @abstractmethod - def upload_parquet_files(self, *args: Any, **kwargs: Any) -> None: - """ - Abstract method to upload parquet files. To be overwritten by child classes. - If an upload strategy does not support parquet files, this method should raise a - NotImplementedError. - """ + @sock.setter + def sock(self, value: socket) -> None: + """When modifying the socket, ensure that it is ssl wrapped.""" + if value is not None and not isinstance(value, ssl.SSLSocket): + value = self.context.wrap_socket(value) + self._sock = value class FTPSUploader(Uploader): @@ -174,3 +156,40 @@ def upload_parquet_files(self, parquet_export: ParquetExport) -> None: # Close the FTP connection ftp.quit() logger.info("Finished FTPS upload of files for '%s'", parquet_export.project_slug) + + +def _connect_to_ftp(ftp_host: str, ftp_port: int, ftp_user: str, ftp_password: str) -> FTP_TLS: + # Connect to the server and login + try: + ftp = ImplicitFtpTls() + ftp.connect(ftp_host, int(ftp_port)) + ftp.login(ftp_user, ftp_password) + ftp.prot_p() + except ftplib.all_errors as ftp_error: + error_msg = "Failed to connect to FTPS server" + raise ConnectionError(error_msg, ftp_error) from ftp_error + return ftp + + +def _create_and_set_as_cwd_multi_path(ftp: FTP_TLS, remote_multi_dir: Path) -> None: + """Create (and cwd into) a multi dir path, analogously to mkdir -p""" + if remote_multi_dir.is_absolute(): + # would require some special handling and we don't need it + err = "must be relative path" + raise ValueError(err) + logger.info("_create_and_set_as_cwd_multi_path %s", remote_multi_dir) + # path should be pretty normalised, so assume split is safe + sub_dirs = str(remote_multi_dir).split("/") + for sd in sub_dirs: + _create_and_set_as_cwd(ftp, sd) + + +def _create_and_set_as_cwd(ftp: FTP_TLS, project_dir: str) -> None: + try: + ftp.cwd(project_dir) + logger.debug("'%s' exists on remote ftp, so moving into it", project_dir) + except ftplib.error_perm: + logger.info("creating '%s' on remote ftp and moving into it", project_dir) + # Directory doesn't exist, so create it + ftp.mkd(project_dir) + ftp.cwd(project_dir) diff --git a/pixl_core/tests/conftest.py b/pixl_core/tests/conftest.py index 5a409f5fe..8e82ec984 100644 --- a/pixl_core/tests/conftest.py +++ b/pixl_core/tests/conftest.py @@ -22,7 +22,7 @@ import pytest from core.db.models import Base, Extract, Image -from core.upload import FTPSUploader +from core.uploader.ftps import FTPSUploader from pytest_pixl.helpers import run_subprocess from pytest_pixl.plugin import FtpHostAddress from sqlalchemy import Engine, create_engine diff --git a/pixl_core/tests/test_upload.py b/pixl_core/tests/uploader/test_ftps.py similarity index 98% rename from pixl_core/tests/test_upload.py rename to pixl_core/tests/uploader/test_ftps.py index 827d23c2e..4e3cdf32f 100644 --- a/pixl_core/tests/test_upload.py +++ b/pixl_core/tests/uploader/test_ftps.py @@ -94,7 +94,7 @@ def test_upload_parquet(parquet_export, ftps_home_dir, ftps_uploader) -> None: # ARRANGE parquet_export.copy_to_exports( - pathlib.Path(__file__).parents[2] / "test" / "resources" / "omop" + pathlib.Path(__file__).parents[3] / "test" / "resources" / "omop" ) parquet_export.export_radiology(pd.DataFrame(list("dummy"), columns=["D"])) diff --git a/pytest-pixl/tests/samples_for_fixture_tests/test_ftpserver_fixture/test_ftpserver_login.py b/pytest-pixl/tests/samples_for_fixture_tests/test_ftpserver_fixture/test_ftpserver_login.py index 962089552..4e6369163 100644 --- a/pytest-pixl/tests/samples_for_fixture_tests/test_ftpserver_fixture/test_ftpserver_login.py +++ b/pytest-pixl/tests/samples_for_fixture_tests/test_ftpserver_fixture/test_ftpserver_login.py @@ -15,7 +15,7 @@ from pathlib import Path -from core.upload import _connect_to_ftp +from core.uploader.ftps import _connect_to_ftp TEST_FILE_CONTENT = "test text" TEST_FILENAME = "testfile.txt" From c2774231958e85c0211376ffd87a41fa79129214 Mon Sep 17 00:00:00 2001 From: peshence Date: Thu, 29 Feb 2024 19:29:30 +0000 Subject: [PATCH 098/120] fix[cli]: convert to_posix to str() --- cli/tests/test_check_env.py | 2 +- pixl_core/src/core/uploader/_base.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/tests/test_check_env.py b/cli/tests/test_check_env.py index d968f5605..1dbde5616 100644 --- a/cli/tests/test_check_env.py +++ b/cli/tests/test_check_env.py @@ -39,5 +39,5 @@ def test_check_env_fails(tmp_path): tmp_sample_env_file.write_text("NONEXISTENT_VARIABLE=") runner = CliRunner() - result = runner.invoke(check_env, tmp_sample_env_file.as_posix()) + result = runner.invoke(check_env, str(tmp_sample_env_file)) assert result.exit_code != 0 diff --git a/pixl_core/src/core/uploader/_base.py b/pixl_core/src/core/uploader/_base.py index 97fb84744..2129734f7 100644 --- a/pixl_core/src/core/uploader/_base.py +++ b/pixl_core/src/core/uploader/_base.py @@ -17,7 +17,7 @@ import logging from abc import ABC, abstractmethod -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from core.project_config import PixlConfig From e92819a67fa6480db7fa72e3040e3547777d66f9 Mon Sep 17 00:00:00 2001 From: peshence Date: Thu, 29 Feb 2024 19:31:22 +0000 Subject: [PATCH 099/120] fix[imports] --- pixl_core/src/core/uploader/ftps.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pixl_core/src/core/uploader/ftps.py b/pixl_core/src/core/uploader/ftps.py index 0ed459768..61ce511c5 100644 --- a/pixl_core/src/core/uploader/ftps.py +++ b/pixl_core/src/core/uploader/ftps.py @@ -31,7 +31,6 @@ from socket import socket from core.exports import ParquetExport - from core.project_config import PixlConfig logger = logging.getLogger(__name__) From 56c839423d6634498fbbb558cf0cf70aa391b0f1 Mon Sep 17 00:00:00 2001 From: peshence Date: Thu, 29 Feb 2024 19:54:29 +0000 Subject: [PATCH 100/120] fix docker compose --- docker-compose.yml | 46 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 5e04cbfb1..66d7430d5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -35,7 +35,7 @@ x-pixl-common-env: &pixl-common-env DEBUG: ${DEBUG} x-pixl-rabbit-mq: &pixl-rabbit-mq - RABBITMQ_HOST: "queue" # Name of the queue service + RABBITMQ_HOST: "queue" # Name of the queue service RABBITMQ_PORT: "5672" RABBITMQ_USERNAME: ${RABBITMQ_USERNAME} RABBITMQ_PASSWORD: ${RABBITMQ_PASSWORD} @@ -79,7 +79,6 @@ networks: ################################################################################ # Services services: - hasher-api: build: context: . @@ -102,7 +101,7 @@ services: networks: - pixl-net healthcheck: - test: [ "CMD", "curl", "-f", "http://hasher-api:8000/heart-beat" ] + test: ["CMD", "curl", "-f", "http://hasher-api:8000/heart-beat"] interval: 10s timeout: 30s retries: 5 @@ -143,6 +142,7 @@ services: AZ_DICOM_TOKEN_REFRESH_SECS: "600" TIME_OFFSET: "${STUDY_TIME_OFFSET}" SALT_VALUE: ${SALT_VALUE}" + PROJECT_CONFIGS_DIR: /${PROJECT_CONFIGS_DIR:-/projects/configs} ports: - "${ORTHANC_ANON_DICOM_PORT}:4242" - "${ORTHANC_ANON_WEB_PORT}:8042" @@ -161,7 +161,15 @@ services: postgres: condition: service_healthy healthcheck: - test: [ "CMD", "curl", "-f", "-u" , "${ORTHANC_ANON_USERNAME}:${ORTHANC_ANON_PASSWORD}", "http://orthanc-anon:8042/heart-beat" ] + test: + [ + "CMD", + "curl", + "-f", + "-u", + "${ORTHANC_ANON_USERNAME}:${ORTHANC_ANON_PASSWORD}", + "http://orthanc-anon:8042/heart-beat", + ] interval: 10s timeout: 30s retries: 5 @@ -183,7 +191,7 @@ services: ORTHANC_RAW_AE_TITLE: ${ORTHANC_RAW_AE_TITLE} ORTHANC_AUTOROUTE_RAW_TO_ANON: ${ORTHANC_AUTOROUTE_RAW_TO_ANON} ORTHANC_RAW_MAXIMUM_STORAGE_SIZE: ${ORTHANC_RAW_MAXIMUM_STORAGE_SIZE} - VNAQR_AE_TITLE : ${VNAQR_AE_TITLE} + VNAQR_AE_TITLE: ${VNAQR_AE_TITLE} VNAQR_DICOM_PORT: ${VNAQR_DICOM_PORT} VNAQR_IP_ADDR: ${VNAQR_IP_ADDR} ORTHANC_ANON_AE_TITLE: ${ORTHANC_ANON_AE_TITLE} @@ -204,7 +212,15 @@ services: orthanc-anon: condition: service_started healthcheck: - test: [ "CMD", "curl", "-f", "-u" , "${ORTHANC_RAW_USERNAME}:${ORTHANC_RAW_PASSWORD}", "http://orthanc-raw:8042/heart-beat" ] + test: + [ + "CMD", + "curl", + "-f", + "-u", + "${ORTHANC_RAW_USERNAME}:${ORTHANC_RAW_PASSWORD}", + "http://orthanc-raw:8042/heart-beat", + ] interval: 10s timeout: 30s retries: 5 @@ -233,7 +249,15 @@ services: args: <<: *build-args-common environment: - <<: [*pixl-db, *emap-db, *proxy-common, *pixl-common-env, *pixl-rabbit-mq, *azure-keyvault] + <<: + [ + *pixl-db, + *emap-db, + *proxy-common, + *pixl-common-env, + *pixl-rabbit-mq, + *azure-keyvault, + ] AZ_STORAGE_ACCOUNT_NAME: ${PIXL_EHR_API_AZ_STORAGE_ACCOUNT_NAME} AZ_STORAGE_CONTAINER_NAME: ${PIXL_EHR_API_AZ_STORAGE_CONTAINER_NAME} COGSTACK_REDACT_URL: ${PIXL_EHR_COGSTACK_REDACT_URL} @@ -260,7 +284,7 @@ services: extra_hosts: - "host.docker.internal:host-gateway" volumes: - - ${PWD}/exports:/run/exports + - ${PWD}/projects/exports:/run/projects/exports - ${PWD}/projects/configs:/${PROJECT_CONFIGS_DIR:-/projects/configs}:ro imaging-api: @@ -298,8 +322,8 @@ services: ports: - "${PIXL_IMAGING_API_PORT}:8000" -################################################################################ -# Data Stores + ################################################################################ + # Data Stores postgres: build: context: . @@ -321,7 +345,7 @@ services: ports: - "${POSTGRES_PORT}:5432" healthcheck: - test: [ "CMD", "pg_isready", "-U", "${PIXL_DB_USER}" ] + test: ["CMD", "pg_isready", "-U", "${PIXL_DB_USER}"] interval: 10s timeout: 30s retries: 5 From c7a20a4a124a084f98e0601a942c8e9005763a07 Mon Sep 17 00:00:00 2001 From: peshence Date: Fri, 1 Mar 2024 11:04:52 +0000 Subject: [PATCH 101/120] fix[core]: add back ftps uploader __init__ --- pixl_core/src/core/uploader/ftps.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pixl_core/src/core/uploader/ftps.py b/pixl_core/src/core/uploader/ftps.py index 61ce511c5..7fb5a93c2 100644 --- a/pixl_core/src/core/uploader/ftps.py +++ b/pixl_core/src/core/uploader/ftps.py @@ -31,6 +31,7 @@ from socket import socket from core.exports import ParquetExport + from core.project_config import PixlConfig logger = logging.getLogger(__name__) @@ -63,6 +64,10 @@ def sock(self, value: socket) -> None: class FTPSUploader(Uploader): """Upload strategy for an FTPS server.""" + def __init__(self, project_config: PixlConfig) -> None: + """Create instance of parent class""" + super().__init__(project_config) + def _set_config(self) -> None: # Use the Azure KV alias as prefix if it exists, otherwise use the project name az_prefix = self.project_config.project.azure_kv_alias From b0725c0157fe316b0e4f62c5d930e654cb762614 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Fri, 1 Mar 2024 15:56:25 +0000 Subject: [PATCH 102/120] Add static `create` method to `Uploader` base class to handle upload strategies --- pixl_core/src/core/uploader/_base.py | 25 ++++++++++++++++--------- pixl_core/src/core/uploader/ftps.py | 11 +++++------ 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/pixl_core/src/core/uploader/_base.py b/pixl_core/src/core/uploader/_base.py index 2129734f7..85658b1fb 100644 --- a/pixl_core/src/core/uploader/_base.py +++ b/pixl_core/src/core/uploader/_base.py @@ -17,13 +17,10 @@ import logging from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Any - -if TYPE_CHECKING: - from core.project_config import PixlConfig - +from typing import Any, Optional from core.uploader._secrets import AzureKeyVault +from core.uploader.ftps import FTPSUploader logger = logging.getLogger(__name__) @@ -32,7 +29,7 @@ class Uploader(ABC): """Upload strategy interface.""" @abstractmethod - def __init__(self, project_config: PixlConfig) -> None: + def __init__(self, project_slug: str, keyvault_alias: Optional[str]) -> None: """ Initialise the uploader for a specific project with the destination configuration and an AzureKeyvault instance. The keyvault is used to fetch the secrets required to connect to @@ -41,10 +38,10 @@ def __init__(self, project_config: PixlConfig) -> None: Child classes should implement the _set_config method to set the configuration for the upload strategy. - :param project: The project name for which the uploader is being initialised. Used to fetch - the correct secrets from the keyvault. + :param : """ - self.project_config = project_config + self.project_slug = project_slug + self.keyvault_alias = keyvault_alias self.keyvault = AzureKeyVault() self._set_config() @@ -67,3 +64,13 @@ def upload_parquet_files(self, *args: Any, **kwargs: Any) -> None: If an upload strategy does not support parquet files, this method should raise a NotImplementedError. """ + + @staticmethod + def create(project_slug: str, destination: str, keyvault_alias: Optional[str]) -> Uploader: + choices: dict[str, type[Uploader]] = {"ftps": FTPSUploader} + try: + return choices[destination](project_slug, keyvault_alias) + + except KeyError: + error_msg = f"Destination '{destination}' is currently not supported" + raise NotImplementedError(error_msg) from None diff --git a/pixl_core/src/core/uploader/ftps.py b/pixl_core/src/core/uploader/ftps.py index 7fb5a93c2..bfa22fa05 100644 --- a/pixl_core/src/core/uploader/ftps.py +++ b/pixl_core/src/core/uploader/ftps.py @@ -22,7 +22,7 @@ from datetime import datetime, timezone from ftplib import FTP_TLS from pathlib import Path -from typing import TYPE_CHECKING, Any, BinaryIO +from typing import TYPE_CHECKING, Any, BinaryIO, Optional from core.db.queries import get_project_slug_from_hashid, update_exported_at from core.uploader._base import Uploader @@ -31,7 +31,6 @@ from socket import socket from core.exports import ParquetExport - from core.project_config import PixlConfig logger = logging.getLogger(__name__) @@ -64,14 +63,14 @@ def sock(self, value: socket) -> None: class FTPSUploader(Uploader): """Upload strategy for an FTPS server.""" - def __init__(self, project_config: PixlConfig) -> None: + def __init__(self, project_slug: str, keyvault_alias: Optional[str]) -> None: """Create instance of parent class""" - super().__init__(project_config) + super().__init__(project_slug, keyvault_alias) def _set_config(self) -> None: # Use the Azure KV alias as prefix if it exists, otherwise use the project name - az_prefix = self.project_config.project.azure_kv_alias - az_prefix = az_prefix if az_prefix else self.project_config.project.name + az_prefix = self.keyvault_alias + az_prefix = az_prefix if az_prefix else self.project_slug self.host = self.keyvault.fetch_secret(f"{az_prefix}--ftp--host") self.user = self.keyvault.fetch_secret(f"{az_prefix}--ftp--username") From b137f3172d83c2d9f08376ed7777ee45a57ecc7e Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Fri, 1 Mar 2024 16:13:00 +0000 Subject: [PATCH 103/120] Make `uploader._base` non-private --- pixl_core/src/core/uploader/{_base.py => base.py} | 1 + 1 file changed, 1 insertion(+) rename pixl_core/src/core/uploader/{_base.py => base.py} (97%) diff --git a/pixl_core/src/core/uploader/_base.py b/pixl_core/src/core/uploader/base.py similarity index 97% rename from pixl_core/src/core/uploader/_base.py rename to pixl_core/src/core/uploader/base.py index 85658b1fb..24d1d6188 100644 --- a/pixl_core/src/core/uploader/_base.py +++ b/pixl_core/src/core/uploader/base.py @@ -67,6 +67,7 @@ def upload_parquet_files(self, *args: Any, **kwargs: Any) -> None: @staticmethod def create(project_slug: str, destination: str, keyvault_alias: Optional[str]) -> Uploader: + """Create an uploader instance based on the destination.""" choices: dict[str, type[Uploader]] = {"ftps": FTPSUploader} try: return choices[destination](project_slug, keyvault_alias) From 87e60646e3ed048b866fefef782dd3df806d1770 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Fri, 1 Mar 2024 16:15:01 +0000 Subject: [PATCH 104/120] Make `uploader.ftps` private --- pixl_core/src/core/uploader/{ftps.py => _ftps.py} | 0 pixl_core/src/core/uploader/base.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename pixl_core/src/core/uploader/{ftps.py => _ftps.py} (100%) diff --git a/pixl_core/src/core/uploader/ftps.py b/pixl_core/src/core/uploader/_ftps.py similarity index 100% rename from pixl_core/src/core/uploader/ftps.py rename to pixl_core/src/core/uploader/_ftps.py diff --git a/pixl_core/src/core/uploader/base.py b/pixl_core/src/core/uploader/base.py index 24d1d6188..b33057648 100644 --- a/pixl_core/src/core/uploader/base.py +++ b/pixl_core/src/core/uploader/base.py @@ -19,8 +19,8 @@ from abc import ABC, abstractmethod from typing import Any, Optional +from core.uploader._ftps import FTPSUploader from core.uploader._secrets import AzureKeyVault -from core.uploader.ftps import FTPSUploader logger = logging.getLogger(__name__) From c6f39e053756f5d3391d8e809e6049bd2ee95a27 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Fri, 1 Mar 2024 16:23:29 +0000 Subject: [PATCH 105/120] Update client code to use new `Uploader.create` method --- orthanc/orthanc-anon/plugin/pixl.py | 22 +++++++++------------- pixl_core/src/core/exports.py | 16 +++++++++------- pixl_core/tests/conftest.py | 2 +- 3 files changed, 19 insertions(+), 21 deletions(-) diff --git a/orthanc/orthanc-anon/plugin/pixl.py b/orthanc/orthanc-anon/plugin/pixl.py index 1914cf6ad..752d287a3 100644 --- a/orthanc/orthanc-anon/plugin/pixl.py +++ b/orthanc/orthanc-anon/plugin/pixl.py @@ -32,7 +32,7 @@ import requests from core.db.queries import get_project_slug_from_hashid from core.project_config import load_project_config -from core.uploader.ftps import FTPSUploader +from core.uploader.base import Uploader from decouple import config from pydicom import dcmread @@ -141,19 +141,15 @@ def Send(resourceId: str) -> None: logger.debug(msg) hashed_patient_id = _get_patient_id(resourceId) - slug = get_project_slug_from_hashid(hashed_patient_id) - project_config = load_project_config(slug) + project_slug = get_project_slug_from_hashid(hashed_patient_id) + project_config = load_project_config(project_slug) + destination = project_config.destination.dicom - # send to destination - if project_config.destination.dicom == "ftps": - msg = f"Sending {resourceId} via FTPS" - logger.debug(msg) - - zip_content = _get_study_zip_archive(resourceId) - FTPSUploader(project_config).upload_dicom_image(zip_content, hashed_patient_id) - else: - msg = f"Invalid destination: {project_config.destination.dicom}" - raise ValueError(msg) + uploader = Uploader.create(project_slug, destination, project_config.project.azure_kv_alias) + msg = f"Sending {resourceId} via '{destination}'" + logger.debug(msg) + zip_content = _get_study_zip_archive(resourceId) + uploader.upload_dicom_image(zip_content, hashed_patient_id) def _get_study_zip_archive(resourceId: str) -> BytesIO: diff --git a/pixl_core/src/core/exports.py b/pixl_core/src/core/exports.py index 7ff64a9b6..58f7213f1 100644 --- a/pixl_core/src/core/exports.py +++ b/pixl_core/src/core/exports.py @@ -21,7 +21,7 @@ import slugify from core.project_config import load_project_config -from core.uploader.ftps import FTPSUploader +from core.uploader.base import Uploader if TYPE_CHECKING: import datetime @@ -130,10 +130,12 @@ def upload(self) -> None: "Skipping upload." ) logger.info(msg) - if destination == "ftps": - msg = f"Uploading parquet files for project {self.project_slug} via FTPS" - logger.info(msg) - FTPSUploader(project_config).upload_parquet_files(self) + else: - msg = f"Destination {destination} for parquet files not supported. Skipping upload." - logger.warning(msg) + uploader = Uploader.create( + self.project_slug, destination, project_config.project.azure_kv_alias + ) + + msg = f"Uploading parquet files for project {self.project_slug} via '{destination}'" + logger.info(msg) + uploader.upload_parquet_files(self) diff --git a/pixl_core/tests/conftest.py b/pixl_core/tests/conftest.py index 8e82ec984..b0561c1e1 100644 --- a/pixl_core/tests/conftest.py +++ b/pixl_core/tests/conftest.py @@ -22,7 +22,7 @@ import pytest from core.db.models import Base, Extract, Image -from core.uploader.ftps import FTPSUploader +from core.uploader._ftps import FTPSUploader from pytest_pixl.helpers import run_subprocess from pytest_pixl.plugin import FtpHostAddress from sqlalchemy import Engine, create_engine From 6ce589e21a01eda9075c8233473c669ca361c5cc Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Fri, 1 Mar 2024 17:10:44 +0000 Subject: [PATCH 106/120] Move `FTPSUploader` to `core.uploader.base` and make private Avoids circular imports --- pixl_core/src/core/uploader/_ftps.py | 122 ++------------------------- pixl_core/src/core/uploader/base.py | 118 +++++++++++++++++++++++++- 2 files changed, 123 insertions(+), 117 deletions(-) diff --git a/pixl_core/src/core/uploader/_ftps.py b/pixl_core/src/core/uploader/_ftps.py index bfa22fa05..925ed9986 100644 --- a/pixl_core/src/core/uploader/_ftps.py +++ b/pixl_core/src/core/uploader/_ftps.py @@ -19,23 +19,18 @@ import ftplib import logging import ssl -from datetime import datetime, timezone from ftplib import FTP_TLS -from pathlib import Path -from typing import TYPE_CHECKING, Any, BinaryIO, Optional - -from core.db.queries import get_project_slug_from_hashid, update_exported_at -from core.uploader._base import Uploader +from typing import TYPE_CHECKING, Any if TYPE_CHECKING: + from pathlib import Path from socket import socket - from core.exports import ParquetExport logger = logging.getLogger(__name__) -class ImplicitFtpTls(ftplib.FTP_TLS): +class _ImplicitFtpTls(ftplib.FTP_TLS): """ FTP_TLS subclass that automatically wraps sockets in SSL to support implicit FTPS. @@ -60,111 +55,10 @@ def sock(self, value: socket) -> None: self._sock = value -class FTPSUploader(Uploader): - """Upload strategy for an FTPS server.""" - - def __init__(self, project_slug: str, keyvault_alias: Optional[str]) -> None: - """Create instance of parent class""" - super().__init__(project_slug, keyvault_alias) - - def _set_config(self) -> None: - # Use the Azure KV alias as prefix if it exists, otherwise use the project name - az_prefix = self.keyvault_alias - az_prefix = az_prefix if az_prefix else self.project_slug - - self.host = self.keyvault.fetch_secret(f"{az_prefix}--ftp--host") - self.user = self.keyvault.fetch_secret(f"{az_prefix}--ftp--username") - self.password = self.keyvault.fetch_secret(f"{az_prefix}--ftp--password") - self.port = int(self.keyvault.fetch_secret(f"{az_prefix}--ftp--port")) - - def upload_dicom_image(self, zip_content: BinaryIO, pseudo_anon_id: str) -> None: - """Upload a DICOM image to the FTPS server.""" - logger.info("Starting FTPS upload of '%s'", pseudo_anon_id) - - # rename destination to {project-slug}/{study-pseduonymised-id}.zip - remote_directory = get_project_slug_from_hashid(pseudo_anon_id) - - # Create the remote directory if it doesn't exist - ftp = _connect_to_ftp(self.host, self.port, self.user, self.password) - _create_and_set_as_cwd(ftp, remote_directory) - command = f"STOR {pseudo_anon_id}.zip" - logger.debug("Running %s", command) - - # Store the file using a binary handler - try: - ftp.storbinary(command, zip_content) - except ftplib.all_errors as ftp_error: - ftp.quit() - error_msg = "Failed to run STOR command '%s': '%s'" - raise ConnectionError(error_msg, command, ftp_error) from ftp_error - - # Close the FTP connection - ftp.quit() - - # Update the exported_at timestamp in the PIXL database - update_exported_at(pseudo_anon_id, datetime.now(tz=timezone.utc)) - logger.info("Finished FTPS upload of '%s'", pseudo_anon_id) - - def upload_parquet_files(self, parquet_export: ParquetExport) -> None: - """ - Upload parquet to FTPS under //parquet. - :param parquet_export: instance of the ParquetExport class - The final directory structure will look like this: - - ├── - │ └── parquet - │ ├── omop - │ │ └── public - │ │ └── PROCEDURE_OCCURRENCE.parquet - │ └── radiology - │ └── radiology.parquet - ├── .zip - └── .zip - ... - """ - logger.info("Starting FTPS upload of files for '%s'", parquet_export.project_slug) - - source_root_dir = parquet_export.current_extract_base - # Create the remote directory if it doesn't exist - ftp = _connect_to_ftp(self.host, self.port, self.user, self.password) - _create_and_set_as_cwd(ftp, parquet_export.project_slug) - _create_and_set_as_cwd(ftp, parquet_export.extract_time_slug) - _create_and_set_as_cwd(ftp, "parquet") - - # get the upload root directory before we do anything as we'll need - # to return to it (will it always be absolute?) - upload_root_dir = Path(ftp.pwd()) - if not upload_root_dir.is_absolute(): - logger.error("server remote path is not absolute, what are we going to do?") - - # absolute paths of the source - source_files = [x for x in source_root_dir.rglob("*.parquet") if x.is_file()] - if not source_files: - msg = f"No files found in {source_root_dir}" - raise FileNotFoundError(msg) - - # throw exception if empty dir - for source_path in source_files: - _create_and_set_as_cwd(ftp, str(upload_root_dir)) - source_rel_path = source_path.relative_to(source_root_dir) - source_rel_dir = source_rel_path.parent - source_filename_only = source_rel_path.relative_to(source_rel_dir) - _create_and_set_as_cwd_multi_path(ftp, source_rel_dir) - with source_path.open("rb") as handle: - command = f"STOR {source_filename_only}" - - # Store the file using a binary handler - ftp.storbinary(command, handle) - - # Close the FTP connection - ftp.quit() - logger.info("Finished FTPS upload of files for '%s'", parquet_export.project_slug) - - -def _connect_to_ftp(ftp_host: str, ftp_port: int, ftp_user: str, ftp_password: str) -> FTP_TLS: +def connect_to_ftp(ftp_host: str, ftp_port: int, ftp_user: str, ftp_password: str) -> FTP_TLS: # Connect to the server and login try: - ftp = ImplicitFtpTls() + ftp = _ImplicitFtpTls() ftp.connect(ftp_host, int(ftp_port)) ftp.login(ftp_user, ftp_password) ftp.prot_p() @@ -174,7 +68,7 @@ def _connect_to_ftp(ftp_host: str, ftp_port: int, ftp_user: str, ftp_password: s return ftp -def _create_and_set_as_cwd_multi_path(ftp: FTP_TLS, remote_multi_dir: Path) -> None: +def create_and_set_as_cwd_multi_path(ftp: FTP_TLS, remote_multi_dir: Path) -> None: """Create (and cwd into) a multi dir path, analogously to mkdir -p""" if remote_multi_dir.is_absolute(): # would require some special handling and we don't need it @@ -184,10 +78,10 @@ def _create_and_set_as_cwd_multi_path(ftp: FTP_TLS, remote_multi_dir: Path) -> N # path should be pretty normalised, so assume split is safe sub_dirs = str(remote_multi_dir).split("/") for sd in sub_dirs: - _create_and_set_as_cwd(ftp, sd) + create_and_set_as_cwd(ftp, sd) -def _create_and_set_as_cwd(ftp: FTP_TLS, project_dir: str) -> None: +def create_and_set_as_cwd(ftp: FTP_TLS, project_dir: str) -> None: try: ftp.cwd(project_dir) logger.debug("'%s' exists on remote ftp, so moving into it", project_dir) diff --git a/pixl_core/src/core/uploader/base.py b/pixl_core/src/core/uploader/base.py index b33057648..0cfbd7a92 100644 --- a/pixl_core/src/core/uploader/base.py +++ b/pixl_core/src/core/uploader/base.py @@ -15,13 +15,24 @@ from __future__ import annotations +import ftplib import logging from abc import ABC, abstractmethod -from typing import Any, Optional +from datetime import datetime, timezone +from pathlib import Path +from typing import TYPE_CHECKING, Any, BinaryIO, Optional -from core.uploader._ftps import FTPSUploader +from core.db.queries import get_project_slug_from_hashid, update_exported_at +from core.uploader._ftps import ( + connect_to_ftp, + create_and_set_as_cwd, + create_and_set_as_cwd_multi_path, +) from core.uploader._secrets import AzureKeyVault +if TYPE_CHECKING: + from core.exports import ParquetExport + logger = logging.getLogger(__name__) @@ -68,10 +79,111 @@ def upload_parquet_files(self, *args: Any, **kwargs: Any) -> None: @staticmethod def create(project_slug: str, destination: str, keyvault_alias: Optional[str]) -> Uploader: """Create an uploader instance based on the destination.""" - choices: dict[str, type[Uploader]] = {"ftps": FTPSUploader} + choices: dict[str, type[Uploader]] = {"ftps": _FTPSUploader} try: return choices[destination](project_slug, keyvault_alias) except KeyError: error_msg = f"Destination '{destination}' is currently not supported" raise NotImplementedError(error_msg) from None + + +class _FTPSUploader(Uploader): + """Upload strategy for an FTPS server.""" + + def __init__(self, project_slug: str, keyvault_alias: Optional[str]) -> None: + """Create instance of parent class""" + super().__init__(project_slug, keyvault_alias) + + def _set_config(self) -> None: + # Use the Azure KV alias as prefix if it exists, otherwise use the project name + az_prefix = self.keyvault_alias + az_prefix = az_prefix if az_prefix else self.project_slug + + self.host = self.keyvault.fetch_secret(f"{az_prefix}--ftp--host") + self.user = self.keyvault.fetch_secret(f"{az_prefix}--ftp--username") + self.password = self.keyvault.fetch_secret(f"{az_prefix}--ftp--password") + self.port = int(self.keyvault.fetch_secret(f"{az_prefix}--ftp--port")) + + def upload_dicom_image(self, zip_content: BinaryIO, pseudo_anon_id: str) -> None: + """Upload a DICOM image to the FTPS server.""" + logger.info("Starting FTPS upload of '%s'", pseudo_anon_id) + + # rename destination to {project-slug}/{study-pseduonymised-id}.zip + remote_directory = get_project_slug_from_hashid(pseudo_anon_id) + + # Create the remote directory if it doesn't exist + ftp = connect_to_ftp(self.host, self.port, self.user, self.password) + create_and_set_as_cwd(ftp, remote_directory) + command = f"STOR {pseudo_anon_id}.zip" + logger.debug("Running %s", command) + + # Store the file using a binary handler + try: + ftp.storbinary(command, zip_content) + except ftplib.all_errors as ftp_error: + ftp.quit() + error_msg = "Failed to run STOR command '%s': '%s'" + raise ConnectionError(error_msg, command, ftp_error) from ftp_error + + # Close the FTP connection + ftp.quit() + + # Update the exported_at timestamp in the PIXL database + update_exported_at(pseudo_anon_id, datetime.now(tz=timezone.utc)) + logger.info("Finished FTPS upload of '%s'", pseudo_anon_id) + + def upload_parquet_files(self, parquet_export: ParquetExport) -> None: + """ + Upload parquet to FTPS under //parquet. + :param parquet_export: instance of the ParquetExport class + The final directory structure will look like this: + + ├── + │ └── parquet + │ ├── omop + │ │ └── public + │ │ └── PROCEDURE_OCCURRENCE.parquet + │ └── radiology + │ └── radiology.parquet + ├── .zip + └── .zip + ... + """ + logger.info("Starting FTPS upload of files for '%s'", parquet_export.project_slug) + + source_root_dir = parquet_export.current_extract_base + # Create the remote directory if it doesn't exist + ftp = connect_to_ftp(self.host, self.port, self.user, self.password) + create_and_set_as_cwd(ftp, parquet_export.project_slug) + create_and_set_as_cwd(ftp, parquet_export.extract_time_slug) + create_and_set_as_cwd(ftp, "parquet") + + # get the upload root directory before we do anything as we'll need + # to return to it (will it always be absolute?) + upload_root_dir = Path(ftp.pwd()) + if not upload_root_dir.is_absolute(): + logger.error("server remote path is not absolute, what are we going to do?") + + # absolute paths of the source + source_files = [x for x in source_root_dir.rglob("*.parquet") if x.is_file()] + if not source_files: + msg = f"No files found in {source_root_dir}" + raise FileNotFoundError(msg) + + # throw exception if empty dir + for source_path in source_files: + create_and_set_as_cwd(ftp, str(upload_root_dir)) + source_rel_path = source_path.relative_to(source_root_dir) + source_rel_dir = source_rel_path.parent + source_filename_only = source_rel_path.relative_to(source_rel_dir) + create_and_set_as_cwd_multi_path(ftp, source_rel_dir) + with source_path.open("rb") as handle: + command = f"STOR {source_filename_only}" + + # Store the file using a binary handler + ftp.storbinary(command, handle) + + # Close the FTP connection + ftp.quit() + logger.info("Finished FTPS upload of files for '%s'", parquet_export.project_slug) From 19ca997067a6e7715c56d11d238586a3222c4e5c Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Fri, 1 Mar 2024 17:12:12 +0000 Subject: [PATCH 107/120] Fix test --- pixl_core/tests/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pixl_core/tests/conftest.py b/pixl_core/tests/conftest.py index b0561c1e1..7b356e5f8 100644 --- a/pixl_core/tests/conftest.py +++ b/pixl_core/tests/conftest.py @@ -22,7 +22,7 @@ import pytest from core.db.models import Base, Extract, Image -from core.uploader._ftps import FTPSUploader +from core.uploader.base import _FTPSUploader from pytest_pixl.helpers import run_subprocess from pytest_pixl.plugin import FtpHostAddress from sqlalchemy import Engine, create_engine @@ -69,7 +69,7 @@ def run_containers() -> subprocess.CompletedProcess[bytes]: ) -class MockFTPSUploader(FTPSUploader): +class MockFTPSUploader(_FTPSUploader): """Mock FTPSUploader for testing.""" def __init__(self) -> None: From 6e62cb7a560b9b1e74f7dc9918d9af0599b07964 Mon Sep 17 00:00:00 2001 From: peshence Date: Fri, 1 Mar 2024 17:16:13 +0000 Subject: [PATCH 108/120] remove mention of non-existing options in enum and in diagram --- .../diagrams/pixl-multi-project-config.drawio | 10 +++++----- .../diagrams/pixl-multi-project-config.png | Bin 127320 -> 138174 bytes pixl_core/src/core/project_config.py | 2 -- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/docs/design/diagrams/pixl-multi-project-config.drawio b/docs/design/diagrams/pixl-multi-project-config.drawio index aa6979bf3..62a6f07ac 100644 --- a/docs/design/diagrams/pixl-multi-project-config.drawio +++ b/docs/design/diagrams/pixl-multi-project-config.drawio @@ -1,6 +1,6 @@ - + - + @@ -85,7 +85,7 @@ - + @@ -115,8 +115,8 @@ - - + + diff --git a/docs/design/diagrams/pixl-multi-project-config.png b/docs/design/diagrams/pixl-multi-project-config.png index 986c54d1bd199ce15dd1e8ff92e4f680a5bc7c66..a79b08fefa4ca7c6fc30a0e1447997a1a6ba056e 100644 GIT binary patch literal 138174 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 literal 127320 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

    U{?ifs}<3GT)U*tC)7Z{J#d?4TO zW83o>aj~x;Lgp7~UoXDD1i5{$vy)#F_l5T5&gX=A*mMR|V_~Fjxd}v4w#H?DmYFQr z6uxHnTB5dCpKGxF(0cc#Crg%K7&>KKRnr-PIr@bHbEF@Z@ae0{Z-n}6A z&5=tQs_Lj{oZ)C~eEr!Rk+CS=94V=PyAl)093jESj-kYoBaJB5_trj&W~5Or(_0~U zCY?D$lgPzS{@dt+Vzjg8FywZtO*0!+Rfu0@*2p1SZVa{d-I}y#Wn)#hvR+hJ;FpM_ zBg48y*C<#IIgZ!d%YGF~?4W(8<_#&| zq1o=-P=P_9mHD_Tx-V2u3SWn2rKCcUW9!s{>rLBe8Z9RV$g@K@Av0!nUk2R$3GtMk|A7!^&*@eZy!A@kXuh-kX9NjG zn55-I3;#2P1B4laj>J8|>?~>)7Q(4wDg2?{C@uhHE>69F?9Cyu_2#!E#p;+sYlxO<~asDGvLfHv-1&LXSuymHdD4(l|l78N&_ z4UK6UIliy?Ylh%3O6KTkGF_-C|$f^M3JJAAb z);O@l!1;Sx{BpP%oqatCH$5b02g`!4_hfLgGcu@u-#0hy^ylN5_HsXa61eih=#e(_ z;HP+Nljet0qW;6Izp-<&lh`5|>rEns=rnPORmN;X!bS(Wy6gp$_ov+7aSYg;AHA!K zoZU3~$Y?ezRG1(BfltgV!q#P;M0r!u6iZZ=JCGixx;f!&GEPpb#M2n-nG)H}9^JQP zHruQtycf3s5v%1d_8H#$a>Pt-})kT@>9|LS06II|eW_&*_Eb}ER|?MK_+_)4nS zRWsNpLKB5_jn+rINu?#4RW4jNZJrBWouxhE2cjqc3y59dmmbb_*9dEPJ z!w2?q5*Vuo93H>B?|FAQ%(*V7l$zqp;rh+>N@lKD8uIvpEna6!@T4308I3pUi3Y{j z6#6n+yoRz(xxL;=;>wk-UgdmAk+XPnZQS1z4M_*YJaZu`Pg|&(q8k^KPu&JsO&WvU zdRklDjdpz-21`^(rD~v`UZp&zplM%=dmP0c7n=tSYAh0^6p_OJPpQEVyXdkXYG<0xIwnB(a z2|J!kLky9YArH)L%^0#nMTaV?XX=txF&Pa9%wU(3UhXrYuR*8&ns+;+T%5zZFYs{^ z zzluB$ztHck-Fpu$@lr|P^{gcORi^=Op7he7cYFwWmr8k3&uKNN6Z38)8PK<@JzeDbz-;k&+wr>DqvY2`+f zpYqb+-NCH>D&^}=lS-oj0pefhIE;$r5f`o2p`6yE(n9Ah#X<4DfrA&=QUt+JxA~l9 zqw#3OV9~Z>LCmPg)l`kj__SA6G=n3enSBY5m~RIw;b=@EsP{HJ_&ZD8?}E+`H5G;e zN*}dtfp-{SGa_*3yQ=?fv6aY)QtVB${cL zC8*Q~Rl4E&7h=^v)HICs(D3C(8I=NU3zXsL@Fd5^#}F5bk#8Okr-uC`X;>70*rf15 z@@YRy_`YrAt)LrCXwo=bHz#tPz=t=pgoAjW;VAbGonUZ@W z&Y_Q2gUSIL>-`;9d#MC_(9>qRa2jK)RV_4Ujv&Uk>!_p>d~;^@v_sZnpF3qV5ba2} zby2EkS`1u_B@Fg@X^RU zl_znm$anp1nF~XvcwN$G#j9)D5eCbtaWdmm=RP`Ki(E<>0Z}kKVHX)MoeEW<(;eNB zt~bur8g0iE&vbIKek*P&I|}aTR6f)7+zxJdV*Gt6bZMF*uI~$#SJy0aToB6a z^T7(nBagoSKa{-*IMn_7He8nMjLE*vj3HaemL*vSGeoxRWGh5=Swfb{;K;_wpk38;wsK7%D9C-d!glE>T{N>D`zPr1PB2@%Bj5i z_I#q)PUj~2^#!S4XSEMsdfR5aw^w)j&BupLYSr?gE0vvogz5DMNzZ1O4OT?;ih8z< z|5GJ_tOG}e(%&;TR52S%`iTagFi$?Xr~ED1)i52`xnx-JHKuF4mm@DFu2I_Apup&j zQMtZGB#KE-ml-{v^Q4Ra^OBsLa~0b=WZc78ZeAjDiZneExA!5eq&3!~*J4!b@?wB& zc$m-8vfgCvW38g7v_X6CS7qw-x4pm|4tEG0N|3!cu7`b6`1mO19_l{gEHN&`qE?b( zq$dgXzFkrE`APQ7%lK)SXz zbu&AK<)@LcF-cU@#keBpG=VBazW$5?x8jep?*2ip(l360%DI%Cb#UJ>=$&MfceC+; zMGclRR1Kp)xnfp%aSe|d{B_qNuD{+jRR4Wv@z`dyS+($C)q*3ySioPj^)1YBb0gVD zi?;2jA2GAex0`SHEeio6mm_)Z);#~__fdV2G_Opf&HK5lmk(OW3+N_b%JG;h4_@ZB zV{X$|>f@agw~oHRf7MjOhd;liV8q-1B0%*1WXysF6*CVNFYXpUlXq^+n1r>QayJel zz<6frDGbxFVQ+&^5jHW_{9Wn(mlWV>ZKx_VLZf6MkFac4zlTN*I^9n%ad8ted`~Vssvd^!gF6 zJeq#-)#l^QfUs6I%nH3zDwq+kG>xbkjN)YN5)rmBScZRP9b+`yPBe?XJxq zvvhq#@JDfTnIV1}CqX{7h|^W$r%m0HI)Pd9`iT5GgfaieRT>d*DeO{%#D&Hg@>|SE zd{7{;DTvY(&?XQ`;$ym_^08IsOt@e5R%alq4=uaA$z_0?ZLP2(&cA&3JGiXU=|%5N z975YvmAkyi?D2l!_0aaVT0lM$P^H##ZRX?eHN+=&-cM9@z38OAsYSrNJ(8&Yuzf3{ zKSREH@WxI;z}M+BkWSb%;xw17@8?`P+m6Dm-4Q`p7ks;Iws)sj;k`(9wfXhN9fOjw zr%{s%e|~hj@ z{-Tqp9Z%Ex{6j>ln6%r84AJzp#rEi%1~KP?B@VyA&-mUkL=z)pNDN*8U%yp?iq-!f zoLr8#+!T?mrcTcc>SqoFqSH8D;Z5@& z1@``!vf?vl2fqlOsMH)Dxf!uqXI1{V%0DKqQ5N^>4jdMhDt_<3A|(@6+tng$gzQef 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 ~SYv)NCl=~@ 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

  • Qof^^c+Lc0Lfi~sjq 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 S;!g<{_%ooGlFNt(!u}6NX~JFr diff --git a/pixl_core/src/core/project_config.py b/pixl_core/src/core/project_config.py index 9de1904c2..e9ddc9789 100644 --- a/pixl_core/src/core/project_config.py +++ b/pixl_core/src/core/project_config.py @@ -64,8 +64,6 @@ class _DestinationEnum(str, Enum): none = "none" ftps = "ftps" - azure = "azure" - dicomweb = "dicomweb" class _Destination(BaseModel): From bc6463697ac505020cb6d544c6e91b9cae069874 Mon Sep 17 00:00:00 2001 From: peshence Date: Fri, 1 Mar 2024 17:16:23 +0000 Subject: [PATCH 109/120] clarify lastpass secrets note --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 30a1faf6c..ff0add2ab 100644 --- a/README.md +++ b/README.md @@ -243,7 +243,7 @@ Create the `.secrets.env` file in the _PIXL_ directory by copying the sample: cp .secrets.env.sample .secrets.env ``` -and fill in the missing values. +and fill in the missing values (for dev purposes find the `pixl_dev_secrets.env` note on LastPass).
    Azure Keyvault setup @@ -260,7 +260,7 @@ variables. See [here](https://learn.microsoft.com/en-us/python/api/azure-identit for more info. The Key Vault and ServicePrincipal have already been created for the `dev` environment and details -are stored in the `pixl-secrets` note in the shared PIXL folder on _LastPass_. +are stored in the `pixl-dev-secrets.env` note in the shared PIXL folder on _LastPass_. The process for doing so using the `az` CLI tool is described below. This process must be repeated for `staging` & `prod` environments. From 3fa54113c852707f395527c3395960870b803161 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Fri, 1 Mar 2024 17:21:28 +0000 Subject: [PATCH 110/120] Limit scope of secret envvars --- .github/workflows/main.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5f27f6730..d1db15f37 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -182,11 +182,6 @@ jobs: if: ${{ ! github.event.pull_request.draft || contains(github.event.pull_request.title, '[force-system-test]') }} runs-on: ubuntu-22.04 timeout-minutes: 30 - env: - EXPORT_AZ_CLIENT_ID: ${{ secrets.EXPORT_AZ_CLIENT_ID }} - EXPORT_AZ_CLIENT_PASSWORD: ${{ secrets.EXPORT_AZ_CLIENT_PASSWORD }} - EXPORT_AZ_TENANT_ID: ${{ secrets.EXPORT_AZ_TENANT_ID }} - EXPORT_AZ_KEY_VAULT_NAME: ${{ secrets.EXPORT_AZ_KEY_VAULT_NAME }} steps: - uses: actions/checkout@v3 - uses: docker/setup-buildx-action@v3 @@ -242,6 +237,11 @@ jobs: - name: Run tests working-directory: test + env: + EXPORT_AZ_CLIENT_ID: ${{ secrets.EXPORT_AZ_CLIENT_ID }} + EXPORT_AZ_CLIENT_PASSWORD: ${{ secrets.EXPORT_AZ_CLIENT_PASSWORD }} + EXPORT_AZ_TENANT_ID: ${{ secrets.EXPORT_AZ_TENANT_ID }} + EXPORT_AZ_KEY_VAULT_NAME: ${{ secrets.EXPORT_AZ_KEY_VAULT_NAME }} run: | ./run-system-test.sh echo FINISHED SYSTEM TEST SCRIPT From cd0d13465e39d578e9b428aa30d507062d31a438 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Mon, 4 Mar 2024 10:14:10 +0000 Subject: [PATCH 111/120] Revert "Move `FTPSUploader` to `core.uploader.base` and make private" This reverts commit 6ce589e21a01eda9075c8233473c669ca361c5cc. --- pixl_core/src/core/uploader/_ftps.py | 122 +++++++++++++++++++++++++-- pixl_core/src/core/uploader/base.py | 118 +------------------------- 2 files changed, 117 insertions(+), 123 deletions(-) diff --git a/pixl_core/src/core/uploader/_ftps.py b/pixl_core/src/core/uploader/_ftps.py index 925ed9986..bfa22fa05 100644 --- a/pixl_core/src/core/uploader/_ftps.py +++ b/pixl_core/src/core/uploader/_ftps.py @@ -19,18 +19,23 @@ import ftplib import logging import ssl +from datetime import datetime, timezone from ftplib import FTP_TLS -from typing import TYPE_CHECKING, Any +from pathlib import Path +from typing import TYPE_CHECKING, Any, BinaryIO, Optional + +from core.db.queries import get_project_slug_from_hashid, update_exported_at +from core.uploader._base import Uploader if TYPE_CHECKING: - from pathlib import Path from socket import socket + from core.exports import ParquetExport logger = logging.getLogger(__name__) -class _ImplicitFtpTls(ftplib.FTP_TLS): +class ImplicitFtpTls(ftplib.FTP_TLS): """ FTP_TLS subclass that automatically wraps sockets in SSL to support implicit FTPS. @@ -55,10 +60,111 @@ def sock(self, value: socket) -> None: self._sock = value -def connect_to_ftp(ftp_host: str, ftp_port: int, ftp_user: str, ftp_password: str) -> FTP_TLS: +class FTPSUploader(Uploader): + """Upload strategy for an FTPS server.""" + + def __init__(self, project_slug: str, keyvault_alias: Optional[str]) -> None: + """Create instance of parent class""" + super().__init__(project_slug, keyvault_alias) + + def _set_config(self) -> None: + # Use the Azure KV alias as prefix if it exists, otherwise use the project name + az_prefix = self.keyvault_alias + az_prefix = az_prefix if az_prefix else self.project_slug + + self.host = self.keyvault.fetch_secret(f"{az_prefix}--ftp--host") + self.user = self.keyvault.fetch_secret(f"{az_prefix}--ftp--username") + self.password = self.keyvault.fetch_secret(f"{az_prefix}--ftp--password") + self.port = int(self.keyvault.fetch_secret(f"{az_prefix}--ftp--port")) + + def upload_dicom_image(self, zip_content: BinaryIO, pseudo_anon_id: str) -> None: + """Upload a DICOM image to the FTPS server.""" + logger.info("Starting FTPS upload of '%s'", pseudo_anon_id) + + # rename destination to {project-slug}/{study-pseduonymised-id}.zip + remote_directory = get_project_slug_from_hashid(pseudo_anon_id) + + # Create the remote directory if it doesn't exist + ftp = _connect_to_ftp(self.host, self.port, self.user, self.password) + _create_and_set_as_cwd(ftp, remote_directory) + command = f"STOR {pseudo_anon_id}.zip" + logger.debug("Running %s", command) + + # Store the file using a binary handler + try: + ftp.storbinary(command, zip_content) + except ftplib.all_errors as ftp_error: + ftp.quit() + error_msg = "Failed to run STOR command '%s': '%s'" + raise ConnectionError(error_msg, command, ftp_error) from ftp_error + + # Close the FTP connection + ftp.quit() + + # Update the exported_at timestamp in the PIXL database + update_exported_at(pseudo_anon_id, datetime.now(tz=timezone.utc)) + logger.info("Finished FTPS upload of '%s'", pseudo_anon_id) + + def upload_parquet_files(self, parquet_export: ParquetExport) -> None: + """ + Upload parquet to FTPS under //parquet. + :param parquet_export: instance of the ParquetExport class + The final directory structure will look like this: + + ├── + │ └── parquet + │ ├── omop + │ │ └── public + │ │ └── PROCEDURE_OCCURRENCE.parquet + │ └── radiology + │ └── radiology.parquet + ├── .zip + └── .zip + ... + """ + logger.info("Starting FTPS upload of files for '%s'", parquet_export.project_slug) + + source_root_dir = parquet_export.current_extract_base + # Create the remote directory if it doesn't exist + ftp = _connect_to_ftp(self.host, self.port, self.user, self.password) + _create_and_set_as_cwd(ftp, parquet_export.project_slug) + _create_and_set_as_cwd(ftp, parquet_export.extract_time_slug) + _create_and_set_as_cwd(ftp, "parquet") + + # get the upload root directory before we do anything as we'll need + # to return to it (will it always be absolute?) + upload_root_dir = Path(ftp.pwd()) + if not upload_root_dir.is_absolute(): + logger.error("server remote path is not absolute, what are we going to do?") + + # absolute paths of the source + source_files = [x for x in source_root_dir.rglob("*.parquet") if x.is_file()] + if not source_files: + msg = f"No files found in {source_root_dir}" + raise FileNotFoundError(msg) + + # throw exception if empty dir + for source_path in source_files: + _create_and_set_as_cwd(ftp, str(upload_root_dir)) + source_rel_path = source_path.relative_to(source_root_dir) + source_rel_dir = source_rel_path.parent + source_filename_only = source_rel_path.relative_to(source_rel_dir) + _create_and_set_as_cwd_multi_path(ftp, source_rel_dir) + with source_path.open("rb") as handle: + command = f"STOR {source_filename_only}" + + # Store the file using a binary handler + ftp.storbinary(command, handle) + + # Close the FTP connection + ftp.quit() + logger.info("Finished FTPS upload of files for '%s'", parquet_export.project_slug) + + +def _connect_to_ftp(ftp_host: str, ftp_port: int, ftp_user: str, ftp_password: str) -> FTP_TLS: # Connect to the server and login try: - ftp = _ImplicitFtpTls() + ftp = ImplicitFtpTls() ftp.connect(ftp_host, int(ftp_port)) ftp.login(ftp_user, ftp_password) ftp.prot_p() @@ -68,7 +174,7 @@ def connect_to_ftp(ftp_host: str, ftp_port: int, ftp_user: str, ftp_password: st return ftp -def create_and_set_as_cwd_multi_path(ftp: FTP_TLS, remote_multi_dir: Path) -> None: +def _create_and_set_as_cwd_multi_path(ftp: FTP_TLS, remote_multi_dir: Path) -> None: """Create (and cwd into) a multi dir path, analogously to mkdir -p""" if remote_multi_dir.is_absolute(): # would require some special handling and we don't need it @@ -78,10 +184,10 @@ def create_and_set_as_cwd_multi_path(ftp: FTP_TLS, remote_multi_dir: Path) -> No # path should be pretty normalised, so assume split is safe sub_dirs = str(remote_multi_dir).split("/") for sd in sub_dirs: - create_and_set_as_cwd(ftp, sd) + _create_and_set_as_cwd(ftp, sd) -def create_and_set_as_cwd(ftp: FTP_TLS, project_dir: str) -> None: +def _create_and_set_as_cwd(ftp: FTP_TLS, project_dir: str) -> None: try: ftp.cwd(project_dir) logger.debug("'%s' exists on remote ftp, so moving into it", project_dir) diff --git a/pixl_core/src/core/uploader/base.py b/pixl_core/src/core/uploader/base.py index 0cfbd7a92..b33057648 100644 --- a/pixl_core/src/core/uploader/base.py +++ b/pixl_core/src/core/uploader/base.py @@ -15,24 +15,13 @@ from __future__ import annotations -import ftplib import logging from abc import ABC, abstractmethod -from datetime import datetime, timezone -from pathlib import Path -from typing import TYPE_CHECKING, Any, BinaryIO, Optional +from typing import Any, Optional -from core.db.queries import get_project_slug_from_hashid, update_exported_at -from core.uploader._ftps import ( - connect_to_ftp, - create_and_set_as_cwd, - create_and_set_as_cwd_multi_path, -) +from core.uploader._ftps import FTPSUploader from core.uploader._secrets import AzureKeyVault -if TYPE_CHECKING: - from core.exports import ParquetExport - logger = logging.getLogger(__name__) @@ -79,111 +68,10 @@ def upload_parquet_files(self, *args: Any, **kwargs: Any) -> None: @staticmethod def create(project_slug: str, destination: str, keyvault_alias: Optional[str]) -> Uploader: """Create an uploader instance based on the destination.""" - choices: dict[str, type[Uploader]] = {"ftps": _FTPSUploader} + choices: dict[str, type[Uploader]] = {"ftps": FTPSUploader} try: return choices[destination](project_slug, keyvault_alias) except KeyError: error_msg = f"Destination '{destination}' is currently not supported" raise NotImplementedError(error_msg) from None - - -class _FTPSUploader(Uploader): - """Upload strategy for an FTPS server.""" - - def __init__(self, project_slug: str, keyvault_alias: Optional[str]) -> None: - """Create instance of parent class""" - super().__init__(project_slug, keyvault_alias) - - def _set_config(self) -> None: - # Use the Azure KV alias as prefix if it exists, otherwise use the project name - az_prefix = self.keyvault_alias - az_prefix = az_prefix if az_prefix else self.project_slug - - self.host = self.keyvault.fetch_secret(f"{az_prefix}--ftp--host") - self.user = self.keyvault.fetch_secret(f"{az_prefix}--ftp--username") - self.password = self.keyvault.fetch_secret(f"{az_prefix}--ftp--password") - self.port = int(self.keyvault.fetch_secret(f"{az_prefix}--ftp--port")) - - def upload_dicom_image(self, zip_content: BinaryIO, pseudo_anon_id: str) -> None: - """Upload a DICOM image to the FTPS server.""" - logger.info("Starting FTPS upload of '%s'", pseudo_anon_id) - - # rename destination to {project-slug}/{study-pseduonymised-id}.zip - remote_directory = get_project_slug_from_hashid(pseudo_anon_id) - - # Create the remote directory if it doesn't exist - ftp = connect_to_ftp(self.host, self.port, self.user, self.password) - create_and_set_as_cwd(ftp, remote_directory) - command = f"STOR {pseudo_anon_id}.zip" - logger.debug("Running %s", command) - - # Store the file using a binary handler - try: - ftp.storbinary(command, zip_content) - except ftplib.all_errors as ftp_error: - ftp.quit() - error_msg = "Failed to run STOR command '%s': '%s'" - raise ConnectionError(error_msg, command, ftp_error) from ftp_error - - # Close the FTP connection - ftp.quit() - - # Update the exported_at timestamp in the PIXL database - update_exported_at(pseudo_anon_id, datetime.now(tz=timezone.utc)) - logger.info("Finished FTPS upload of '%s'", pseudo_anon_id) - - def upload_parquet_files(self, parquet_export: ParquetExport) -> None: - """ - Upload parquet to FTPS under //parquet. - :param parquet_export: instance of the ParquetExport class - The final directory structure will look like this: - - ├── - │ └── parquet - │ ├── omop - │ │ └── public - │ │ └── PROCEDURE_OCCURRENCE.parquet - │ └── radiology - │ └── radiology.parquet - ├── .zip - └── .zip - ... - """ - logger.info("Starting FTPS upload of files for '%s'", parquet_export.project_slug) - - source_root_dir = parquet_export.current_extract_base - # Create the remote directory if it doesn't exist - ftp = connect_to_ftp(self.host, self.port, self.user, self.password) - create_and_set_as_cwd(ftp, parquet_export.project_slug) - create_and_set_as_cwd(ftp, parquet_export.extract_time_slug) - create_and_set_as_cwd(ftp, "parquet") - - # get the upload root directory before we do anything as we'll need - # to return to it (will it always be absolute?) - upload_root_dir = Path(ftp.pwd()) - if not upload_root_dir.is_absolute(): - logger.error("server remote path is not absolute, what are we going to do?") - - # absolute paths of the source - source_files = [x for x in source_root_dir.rglob("*.parquet") if x.is_file()] - if not source_files: - msg = f"No files found in {source_root_dir}" - raise FileNotFoundError(msg) - - # throw exception if empty dir - for source_path in source_files: - create_and_set_as_cwd(ftp, str(upload_root_dir)) - source_rel_path = source_path.relative_to(source_root_dir) - source_rel_dir = source_rel_path.parent - source_filename_only = source_rel_path.relative_to(source_rel_dir) - create_and_set_as_cwd_multi_path(ftp, source_rel_dir) - with source_path.open("rb") as handle: - command = f"STOR {source_filename_only}" - - # Store the file using a binary handler - ftp.storbinary(command, handle) - - # Close the FTP connection - ftp.quit() - logger.info("Finished FTPS upload of files for '%s'", parquet_export.project_slug) From adee916c4b5cb9abbc8b2d657a61a929b910bd15 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Mon, 4 Mar 2024 10:31:35 +0000 Subject: [PATCH 112/120] Use package-level `get_uploader` factory instead of static method --- pixl_core/src/core/uploader/__init__.py | 21 +++++++++++++++++++++ pixl_core/src/core/uploader/_ftps.py | 2 +- pixl_core/src/core/uploader/base.py | 12 ------------ 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/pixl_core/src/core/uploader/__init__.py b/pixl_core/src/core/uploader/__init__.py index 4098fa2b1..23cbdb0f4 100644 --- a/pixl_core/src/core/uploader/__init__.py +++ b/pixl_core/src/core/uploader/__init__.py @@ -20,3 +20,24 @@ Uploader class gets appropriate secret credentials from Azure key vault and uploads data """ + +from __future__ import annotations + +from typing import TYPE_CHECKING, Optional + +from core.uploader._ftps import FTPSUploader + +if TYPE_CHECKING: + from core.uploader.base import Uploader + + +# Intenitonally defined in __init__.py to avoid circular imports +def get_uploader(project_slug: str, destination: str, keyvault_alias: Optional[str]) -> Uploader: + """Uploader Factory, returns uploader instance based on destination.""" + choices: dict[str, type[Uploader]] = {"ftps": FTPSUploader} + try: + return choices[destination](project_slug, keyvault_alias) + + except KeyError: + error_msg = f"Destination '{destination}' is currently not supported" + raise NotImplementedError(error_msg) from None diff --git a/pixl_core/src/core/uploader/_ftps.py b/pixl_core/src/core/uploader/_ftps.py index bfa22fa05..547090e8f 100644 --- a/pixl_core/src/core/uploader/_ftps.py +++ b/pixl_core/src/core/uploader/_ftps.py @@ -25,7 +25,7 @@ from typing import TYPE_CHECKING, Any, BinaryIO, Optional from core.db.queries import get_project_slug_from_hashid, update_exported_at -from core.uploader._base import Uploader +from core.uploader.base import Uploader if TYPE_CHECKING: from socket import socket diff --git a/pixl_core/src/core/uploader/base.py b/pixl_core/src/core/uploader/base.py index b33057648..2aef17273 100644 --- a/pixl_core/src/core/uploader/base.py +++ b/pixl_core/src/core/uploader/base.py @@ -19,7 +19,6 @@ from abc import ABC, abstractmethod from typing import Any, Optional -from core.uploader._ftps import FTPSUploader from core.uploader._secrets import AzureKeyVault logger = logging.getLogger(__name__) @@ -64,14 +63,3 @@ def upload_parquet_files(self, *args: Any, **kwargs: Any) -> None: If an upload strategy does not support parquet files, this method should raise a NotImplementedError. """ - - @staticmethod - def create(project_slug: str, destination: str, keyvault_alias: Optional[str]) -> Uploader: - """Create an uploader instance based on the destination.""" - choices: dict[str, type[Uploader]] = {"ftps": FTPSUploader} - try: - return choices[destination](project_slug, keyvault_alias) - - except KeyError: - error_msg = f"Destination '{destination}' is currently not supported" - raise NotImplementedError(error_msg) from None From 991b17ec72ab7a042a4719e94ae553a2a85df807 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Mon, 4 Mar 2024 10:35:46 +0000 Subject: [PATCH 113/120] Update client code to use `get_uploader` --- orthanc/orthanc-anon/plugin/pixl.py | 4 ++-- pixl_core/src/core/exports.py | 4 ++-- pixl_core/tests/conftest.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/orthanc/orthanc-anon/plugin/pixl.py b/orthanc/orthanc-anon/plugin/pixl.py index 752d287a3..e356dbe18 100644 --- a/orthanc/orthanc-anon/plugin/pixl.py +++ b/orthanc/orthanc-anon/plugin/pixl.py @@ -32,7 +32,7 @@ import requests from core.db.queries import get_project_slug_from_hashid from core.project_config import load_project_config -from core.uploader.base import Uploader +from core.uploader import get_uploader from decouple import config from pydicom import dcmread @@ -145,7 +145,7 @@ def Send(resourceId: str) -> None: project_config = load_project_config(project_slug) destination = project_config.destination.dicom - uploader = Uploader.create(project_slug, destination, project_config.project.azure_kv_alias) + uploader = get_uploader(project_slug, destination, project_config.project.azure_kv_alias) msg = f"Sending {resourceId} via '{destination}'" logger.debug(msg) zip_content = _get_study_zip_archive(resourceId) diff --git a/pixl_core/src/core/exports.py b/pixl_core/src/core/exports.py index 58f7213f1..84ef5b5ef 100644 --- a/pixl_core/src/core/exports.py +++ b/pixl_core/src/core/exports.py @@ -21,7 +21,7 @@ import slugify from core.project_config import load_project_config -from core.uploader.base import Uploader +from core.uploader import get_uploader if TYPE_CHECKING: import datetime @@ -132,7 +132,7 @@ def upload(self) -> None: logger.info(msg) else: - uploader = Uploader.create( + uploader = get_uploader( self.project_slug, destination, project_config.project.azure_kv_alias ) diff --git a/pixl_core/tests/conftest.py b/pixl_core/tests/conftest.py index 7b356e5f8..b0561c1e1 100644 --- a/pixl_core/tests/conftest.py +++ b/pixl_core/tests/conftest.py @@ -22,7 +22,7 @@ import pytest from core.db.models import Base, Extract, Image -from core.uploader.base import _FTPSUploader +from core.uploader._ftps import FTPSUploader from pytest_pixl.helpers import run_subprocess from pytest_pixl.plugin import FtpHostAddress from sqlalchemy import Engine, create_engine @@ -69,7 +69,7 @@ def run_containers() -> subprocess.CompletedProcess[bytes]: ) -class MockFTPSUploader(_FTPSUploader): +class MockFTPSUploader(FTPSUploader): """Mock FTPSUploader for testing.""" def __init__(self) -> None: From d908c56305fc6c64cede9f0080c058bec8c803c9 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Mon, 4 Mar 2024 11:08:11 +0000 Subject: [PATCH 114/120] Remove duplicate template --- config-template/project-slug.yaml | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 config-template/project-slug.yaml diff --git a/config-template/project-slug.yaml b/config-template/project-slug.yaml deleted file mode 100644 index aa18d6fa9..000000000 --- a/config-template/project-slug.yaml +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright (c) 2022 University College London Hospitals NHS Foundation Trust -# -# 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 -# -# http://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. - -project: - name: "project-slug" - modalities: ["DX", "CR"] - -tag_operation_files: ["base-tag-operations.yaml"] - -destination: - dicom: "ftps" - parquet: "ftps" From 0baef01310b268a33f66f3d8b506916b09ad6192 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Tue, 5 Mar 2024 15:52:32 +0000 Subject: [PATCH 115/120] Doc fix --- orthanc/orthanc-anon/plugin/pixl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/orthanc/orthanc-anon/plugin/pixl.py b/orthanc/orthanc-anon/plugin/pixl.py index a129b5db1..46273e87c 100644 --- a/orthanc/orthanc-anon/plugin/pixl.py +++ b/orthanc/orthanc-anon/plugin/pixl.py @@ -235,7 +235,7 @@ def OnChange(changeType, level, resource): # noqa: ARG001 """ Three ChangeTypes included in this function: - If a study is stable and if should_auto_route returns true - then SendViaFTPS is called + then Send is called - If orthanc has started then message added to Orthanc LogWarning and AzureDICOMTokenRefresh called - If orthanc has stopped and TIMER is not none then message added From 56044c839e251ea474aaf42c90014b98ce29397b Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Tue, 5 Mar 2024 15:57:15 +0000 Subject: [PATCH 116/120] Fix import --- .../test_ftpserver_fixture/test_ftpserver_login.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest-pixl/tests/samples_for_fixture_tests/test_ftpserver_fixture/test_ftpserver_login.py b/pytest-pixl/tests/samples_for_fixture_tests/test_ftpserver_fixture/test_ftpserver_login.py index 4e6369163..93c8be601 100644 --- a/pytest-pixl/tests/samples_for_fixture_tests/test_ftpserver_fixture/test_ftpserver_login.py +++ b/pytest-pixl/tests/samples_for_fixture_tests/test_ftpserver_fixture/test_ftpserver_login.py @@ -15,7 +15,7 @@ from pathlib import Path -from core.uploader.ftps import _connect_to_ftp +from core.uploader._ftps import _connect_to_ftp TEST_FILE_CONTENT = "test text" TEST_FILENAME = "testfile.txt" From c8a7ebbbb7fdc3f9dadccee78efd1db39c072b44 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Tue, 5 Mar 2024 16:07:56 +0000 Subject: [PATCH 117/120] Fix export dir for cli tests --- cli/tests/conftest.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cli/tests/conftest.py b/cli/tests/conftest.py index 3eed3dff7..1c1ef9c1a 100644 --- a/cli/tests/conftest.py +++ b/cli/tests/conftest.py @@ -47,9 +47,9 @@ @pytest.fixture(autouse=True) def export_dir(tmp_path_factory: pytest.TempPathFactory) -> Path: - """Tmp dir to for tests to extract to.""" - export_dir = tmp_path_factory.mktemp("export_base") / "exports" - export_dir.mkdir() + """Tmp dir for tests to extract to.""" + export_dir = tmp_path_factory.mktemp("export_base") / "projects" / "exports" + export_dir.mkdir(parents=True) return export_dir From 80e66fc8b84b3e579aaa1293a25f2e853ae1bc9b Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Tue, 5 Mar 2024 16:14:48 +0000 Subject: [PATCH 118/120] Fix: Delay checking for `export_dir` existence until it's needed --- pixl_core/src/core/exports.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pixl_core/src/core/exports.py b/pixl_core/src/core/exports.py index 84ef5b5ef..0b9c32792 100644 --- a/pixl_core/src/core/exports.py +++ b/pixl_core/src/core/exports.py @@ -46,11 +46,6 @@ def __init__( different view of the filesystem than the docker containers do. """ self.export_dir = export_dir - # Make sure the base export direcotry exsists - if not self.export_dir.exists(): - msg = f"Export directory {self.export_dir} does not exist" - raise FileNotFoundError(msg) - self.project_slug, self.extract_time_slug = self._get_slugs(project_name, extract_datetime) project_base = self.export_dir / self.project_slug @@ -90,6 +85,11 @@ def copy_to_exports(self, input_omop_dir: pathlib.Path) -> str: logger.info("Copying public parquet files from %s to %s", public_input, self.public_output) + # Make sure the base export direcotry exsists + if not self.export_dir.exists(): + msg = f"Export directory {self.export_dir} does not exist" + raise FileNotFoundError(msg) + # Make directory for project exports ParquetExport._mkdir(self.public_output) From 18673ef108dda8164977f44b3915891b7edb1fa0 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Tue, 5 Mar 2024 16:48:33 +0000 Subject: [PATCH 119/120] Switch back to editable installs --- .github/workflows/main.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 86410e4d2..8e1582ec6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -131,9 +131,9 @@ jobs: - name: Install Python dependencies run: | - pip install pixl_core/ - pip install pytest-pixl/ - pip install cli/[test] + pip install -e pixl_core/ + pip install -e pytest-pixl/ + pip install -e cli/[test] - name: Run tests working-directory: cli/tests From c48e1be27774c8afa6be885492c57cf5fec426fd Mon Sep 17 00:00:00 2001 From: peshence Date: Tue, 5 Mar 2024 17:58:44 +0000 Subject: [PATCH 120/120] fix orthanc raw? --- docker-compose.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 7e7252fbf..ce10ec37d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -199,6 +199,7 @@ services: ORTHANC_ANON_AE_TITLE: ${ORTHANC_ANON_AE_TITLE} ORTHANC_ANON_DICOM_PORT: "4242" ORTHANC_ANON_HOSTNAME: "orthanc-anon" + PROJECT_CONFIGS_DIR: /${PROJECT_CONFIGS_DIR:-/projects/configs} ports: - "${ORTHANC_RAW_DICOM_PORT}:4242" - "${ORTHANC_RAW_WEB_PORT}:8042" @@ -206,6 +207,7 @@ services: - type: volume source: orthanc-raw-data target: /var/lib/orthanc/db + - ${PWD}/projects/configs:/${PROJECT_CONFIGS_DIR:-/projects/configs}:ro networks: - pixl-net depends_on: