From e33fd5c583f8b2ec866aab8abdc58ad1694c4a29 Mon Sep 17 00:00:00 2001 From: April Shen Date: Fri, 6 Sep 2024 16:04:37 +0100 Subject: [PATCH 1/5] simplify xlsx2json conversion --- eva_sub_cli/executables/xlsx2json.py | 65 +++++++--------------------- 1 file changed, 15 insertions(+), 50 deletions(-) diff --git a/eva_sub_cli/executables/xlsx2json.py b/eva_sub_cli/executables/xlsx2json.py index 179c0d3..c97c6f3 100644 --- a/eva_sub_cli/executables/xlsx2json.py +++ b/eva_sub_cli/executables/xlsx2json.py @@ -53,12 +53,13 @@ def __init__(self, xlsx_filename, conf_filename): self.workbook = load_workbook(xlsx_filename, read_only=True) except Exception as e: self.add_error(f'Error loading {xlsx_filename}: {e}') + self.file_loaded = False return - self.worksheets = None + self.worksheets = self.valid_worksheets() self._active_worksheet = None self.row_offset = {} self.headers = {} - self.valid = None + self.file_loaded = True self.errors = [] @property @@ -77,7 +78,7 @@ def active_worksheet(self, worksheet): def valid_worksheets(self): """ - Get the list of the names of worksheets which have all the configured required headers + Get the list of the names of worksheets which have at least the expected header row. :return: list of valid worksheet names in the Excel file :rtype: list """ @@ -97,32 +98,13 @@ def valid_worksheets(self): header_row = self.xlsx_conf[title].get(HEADERS_KEY_ROW, 1) if worksheet.max_row < header_row + 1: continue - # Check required headers are present + # Store headers and worksheet title self.headers[title] = [cell.value if cell.value is None else cell.value.strip() for cell in worksheet[header_row]] - required_headers = self.xlsx_conf[title].get(REQUIRED_HEADERS_KEY_NAME, []) - if set(required_headers) <= set(self.headers[title]): # issubset - self.worksheets.append(title) - else: - missing_headers = set(required_headers) - set(self.headers[title]) - for header in missing_headers: - self.add_error(f'Worksheet {title} is missing required header {header}', - sheet=title, column=header) + self.worksheets.append(title) return self.worksheets - def is_valid(self): - """ - Check that is all the worksheets contain required headers - :return: True if all the worksheets contain required headers. False otherwise - :rtype: bool - """ - if self.valid is None: - self.valid = True - self.valid_worksheets() - - return self.valid - @staticmethod def cast_value(value, type_name): # Do not cast None values @@ -219,16 +201,17 @@ def get_biosample_object(self, data): scientific_name = self.xlsx_conf[SAMPLE][OPTIONAL_HEADERS_KEY_NAME][SCIENTIFIC_NAME_KEY] # BioSample expects any of organism or species field - data[SPECIES] = data[scientific_name] + if scientific_name in data: + data[SPECIES] = data[scientific_name] # BioSample name goes in its own attribute, not part of characteristics - biosample_name = data.pop(sample_name) - # For all characteristics, BioSample expects value in arrays of objects - data = {k: [{'text': self.serialize(v)}] for k, v in data.items()} + biosample_name = data.pop(sample_name, None) + # For all characteristics, BioSample expects value in arrays of objects biosample_object = { - "name": biosample_name, - "characteristics": data + 'characteristics': {k: [{'text': self.serialize(v)}] for k, v in data.items()} } + if biosample_name is not None: + biosample_object['name'] = biosample_name return biosample_object @@ -263,20 +246,6 @@ def get_sample_json_data(self): json_value.pop(analysis_alias) json_value.pop(sample_name_in_vcf) - # Check for headers that are required only in this case - sample_name = self.xlsx_conf[SAMPLE][OPTIONAL_HEADERS_KEY_NAME][SAMPLE_NAME_KEY] - scientific_name = self.xlsx_conf[SAMPLE][OPTIONAL_HEADERS_KEY_NAME][SCIENTIFIC_NAME_KEY] - if sample_name not in json_value: - self.add_error(f'If BioSample Accession is not provided, the {SAMPLE} worksheet should have ' - f'{SAMPLE_NAME_KEY} populated', - sheet=SAMPLE, row=row_num, column=SAMPLE_NAME_KEY) - return None - if scientific_name not in json_value: - self.add_error(f'If BioSample Accession is not provided, the {SAMPLE} worksheet should have ' - f'{SCIENTIFIC_NAME_KEY} populated', - sheet=SAMPLE, row=row_num, column=SCIENTIFIC_NAME_KEY) - return None - biosample_obj = self.get_biosample_object(json_value) sample_data.update(bioSampleObject=biosample_obj) sample_json[json_key].append(sample_data) @@ -284,9 +253,8 @@ def get_sample_json_data(self): return sample_json def json(self, output_json_file): - # First check that all sheets present have the required headers; - # also guards against the case where conversion fails in init - if not self.is_valid(): + # If the file could not be loaded at all, return without generating JSON. + if not self.file_loaded: return json_data = {} for title in self.xlsx_conf[WORKSHEETS_KEY_NAME]: @@ -295,8 +263,6 @@ def json(self, output_json_file): json_data.update(self.get_project_json_data()) elif title == SAMPLE: sample_data = self.get_sample_json_data() - if sample_data is None: # missing conditionally required headers - return json_data.update(sample_data) else: json_data[self.xlsx_conf[WORKSHEETS_KEY_NAME][title]] = [] @@ -324,7 +290,6 @@ def add_error(self, message, sheet='', row='', column=''): """Adds a conversion error using the same structure as other validation errors, and marks the spreadsheet as invalid.""" self.errors.append({'sheet': sheet, 'row': row, 'column': column, 'description': message}) - self.valid = False def save_errors(self, errors_yaml_file): with open(errors_yaml_file, 'w') as open_file: From 99589a0876a3b4685717ee001d375e1335b08ee1 Mon Sep 17 00:00:00 2001 From: April Shen Date: Tue, 10 Sep 2024 09:14:19 +0100 Subject: [PATCH 2/5] fix tests --- eva_sub_cli/executables/xlsx2json.py | 13 +++------- eva_sub_cli/validators/docker_validator.py | 3 +-- .../resources/EVA_Submission_test_fails.xlsx | Bin 37594 -> 50212 bytes tests/test_xlsx2json.py | 24 ++++++++++++------ 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/eva_sub_cli/executables/xlsx2json.py b/eva_sub_cli/executables/xlsx2json.py index c97c6f3..642c5d3 100644 --- a/eva_sub_cli/executables/xlsx2json.py +++ b/eva_sub_cli/executables/xlsx2json.py @@ -55,12 +55,13 @@ def __init__(self, xlsx_filename, conf_filename): self.add_error(f'Error loading {xlsx_filename}: {e}') self.file_loaded = False return - self.worksheets = self.valid_worksheets() + self.worksheets = [] self._active_worksheet = None self.row_offset = {} self.headers = {} self.file_loaded = True self.errors = [] + self.valid_worksheets() @property def active_worksheet(self): @@ -78,14 +79,8 @@ def active_worksheet(self, worksheet): def valid_worksheets(self): """ - Get the list of the names of worksheets which have at least the expected header row. - :return: list of valid worksheet names in the Excel file - :rtype: list + Get the list of the names of worksheets which have the expected title and header row. """ - if self.worksheets is not None: - return self.worksheets - - self.worksheets = [] sheet_titles = self.workbook.sheetnames for title in self.xlsx_conf[WORKSHEETS_KEY_NAME]: @@ -103,8 +98,6 @@ def valid_worksheets(self): for cell in worksheet[header_row]] self.worksheets.append(title) - return self.worksheets - @staticmethod def cast_value(value, type_name): # Do not cast None values diff --git a/eva_sub_cli/validators/docker_validator.py b/eva_sub_cli/validators/docker_validator.py index 6b01e49..c9e370b 100644 --- a/eva_sub_cli/validators/docker_validator.py +++ b/eva_sub_cli/validators/docker_validator.py @@ -1,4 +1,3 @@ -import argparse import csv import os import re @@ -12,7 +11,7 @@ logger = logging_config.get_logger(__name__) container_image = 'ebivariation/eva-sub-cli' -container_tag = 'v0.0.1.dev16' +container_tag = 'v0.0.1.dev17' container_validation_dir = '/opt/vcf_validation' container_validation_output_dir = 'vcf_validation_output' diff --git a/tests/resources/EVA_Submission_test_fails.xlsx b/tests/resources/EVA_Submission_test_fails.xlsx index 2c06d0dd8b874ce9dc02d902d3673e3ece4f5ae5..716160c36c711783c13fa4c656b4ed2cbc363e97 100644 GIT binary patch literal 50212 zcmeFYRa9Kjwk?VWcL?q-g;Th@LvSa!ySoG{+=7!paQEPayAuc&+=IJclD+r2=e5)J zzU{q__jT)IRUd22-sftw%{InV6?teFYzTOGc!)?51|5k1_CtEZMlR-dt}K8*&&s53 zrCwI#@KfJthFh*x5%{k~Z3;4Nm>obxV*#+`m+m zpN8Bs{cQ#zC=%gfpyA2>Vivg@rt>=n)7erp|7RdfdzhlVEti8VDUpv5$EFrVGP%c) zoI-(LF;6=tig`Gq_b+q*meIA-6jXd0h> z;KtiA!&oidd=)AEWfV^GaELjK%D-(Uw6_vo zb}XKbF18;X9c=+#_I9!Aimpqn=pDCOR&F+EeWZRc>FKCkH&h(fS;3sVqc?n-l=399 zFjmE{9k$S=;+V>4#Tc^3ZDSD0M;Gy&`mV1V^it)%_s^d ze(T)C(jdr_E@;I3f*j9a4T{3GxfHxdDD96)M^7;u3jsR6lm7f3dnBJWyu0^dJ+61) zseWNO{9MiYMP7EakKu=0WRPx$kc9Zxv|>jc&1=ckx;`Sb=39LW8!Q#3>8i$GoTI+E zi%>{Bhw<+Q>*d$cKB4UY%(_d_PGdR(A?{B}YELxQDkg+~GNDlpIzJ?)*=}IBvGI7q zT-4tmcjq*cG{YZ%Pt~bg^UFw`CN`WE zpg!G~Bbl31uNU0Am2I|%nm=*NBMF{}C@W20m;1OQC8NSk`X}eFsyn=2;r|rj$pSWD zKHw5OAZjBrGL7B}HkGF!Q@6D~+Q%~#f6*{^k4iW(4$LKvc!mlyzYCwxU$B?xo;M_1 z_CrfnqSNon^a!CPxVfyvcTcI{Lfb#ROcX{Or0m+8>Lt{)+9YR_W#tH{xrJ;b*15E?zX->BGkctM&QZ*ny`;-Dh(rV*se=nX+X)&)-*Hb8>#QapWA_ zKvFZ*ix7cVk6x}7BZnfQwC)quQH03$wzspNT#~Z;j0aBh1hpOzL#BdXBsABq%@FS7 zt@vMjC<1*lyNHL?@N_!uTc+M&d83KlKVL;S;~g8EeTO7VQb7&iNd&4kJnN|T?7(?x z-k~}K$MZ1ad1;1`gf?P2KpQWT<6h`X9$h2%*Ap4##y#f%Z}U}jdvZt>&5mV~hK|t) z7cH*Lqs)Mz-0VA^>!Dy5^G}&x3Ciu#3wzDExA&FyuNRDIs;F|sTR2x7tr6^B6$Upv z56XA)$2hYC5Df5Sx_#IZ_bv_n`pcNn63jS8-Eo6MqZmhBR|LtiHezwY;2D#QDsuPf ztQgGh(RscFEh777N3phvCapC5La>2pUQ%JC^Vs(&dCvM_xRr^g(rnmw-=0bl z=b+gq#TmQg@_FE~zCPM^x`r|aJ@&&1=o2B{gMxiY{e(3;iBxZ}?15yVa%ZTN2HErd zPdmx^m40p&*TxjdhjXCSX<$C8k`~UvjHZgHiHZJ4Ux3!LXrvN7dh_1sO=gG)lK(hwjG5 z8um^tBJc)-1DX|PY5SlO4_@k1kx>?ab^9eU}o+K(Xc9O zx(l~)f`ld<=S}8Qcc#Y7G~=Hqcu0G+=1rKQ5p|;RwaBEiw$-)K zC+ej4J?9l5_(TvW^weKV71bFz302H5uN$Yk7uddZeBG_dY-}&JjpzmRAFnI zRJ=2xM-~2P*q8yx#xQBB?Kecp#ioq1boR#Ue$qNytWN&gNB#0Y66^b@)Vy?}!LC=p z7^Wnox&u99`#kh>`0B0Y+K+umz~HSw?-19$At_e-cncYTegLG$$L)dIL@S!Yi*^RG zAM(y>qnmCu_3-Lx*^&&-2`6|32&Q-gDGqu)cqTEdQh1ax^ri|%d)D86m+R}y?u`({ z(%LHI#tg$A*mPO=;+gn-R3dctK@LvJ=}x#;uwdXIO34`!s8*UD5&Ka4(8^t1O4wAs z8?q;=M*#(2mrk9+-&-+}MlVuc%)f=v$YCIi(pbjGwZNwf(`{k_ zJPRG#pCLYYL;MbLc%2_-#{yaFv~vo5tVJa+LPGN`l{`E~OU`j()1;l3EBGA*s-4Gt z)OcTMByab%`NZ77_A_rE98@hM1_#VXEedYzNO$MBzGY5*!TOBgPz@xK_XFy&80Vlu zP<_n-RFh3@g~+jsrSJJ?!^&+sa0s)~s{35o>-r-@j$(zm7O^y}UEzQ#u!j1meT2(uCvw0M>-!j;*Fy`qU^}S-Z3H zc*Dtt!wj319(}<@wBo$~_5Aq+H9l0-70Iso@%+2Sl+7xn_kuwtw-r3shIOUMKtZ1K zTA@2)L*o97+!qVPxt~aDP>p;G6h9aPEWAI}u;s+oPU}e!Tjs}=Wu=`i&2rA~aw#?W zcp`E%rhYzcg(Nslh9YTe_?_?hYZ}ICfnV2r>cof4xL5Pqf3ht+wvkloizZ@Y+4=F2 zL}eKLmh$CnYde-4*_FqSA!fW8=tW8YgdiKHtb6=Ny`YNo5>HBL-QY3K{$ zaR|iS=(DOtdh$VD^6&w{&UQFwOFS7^bV{{vEDG6*!qMa9J!!z@n%Ua?D1853VNu{a z5|W7=2~YjCNpLPPN$B-QDdEJrA=!9UfsHX8_PaY-6OVv z)I#E`+O_d>1@72xIt?h)Kk=j>yjx@tOzQJoqZ?I~MUxY1zK^@l2MX1r%<8R!SO#zI zpRX5TKR8k!ETrC*v<;#_OkV~5$*G3-kOuhgARyYp{!>oH``;r&S1WUKH&>Rwe{6r! zYroF0%Q6po$D~$`S5EYIYZh60`S~PTTz|DW^fgqkeLZbDEc;NmvyasSTE{7=TEuVx7kWY z_7z>Rnz5XV0txE|irqRuPGf~w;o^P40R#zD%Ri(<@9WVjW4t}MP7?apf`4k zTJ2QQKU&F8;ToB|P!BKNb3Ma|Oxr0+X}(;TZDS>OZ-B` z$OIG7q$JNNAi|7Dro8Utf#t;5xeba>wl#pc@`OI%{s-{$s z{h3Kc#E*0^)}%fi;{ld)gC(lIi(WVq3Zb+xt7`VpV62~AP|Q^D#?BlN_X9r0S9H*6kjt5SsD%Flghxyx)A&cmC4<>5JI}FVNC)D zzp=Vx)20j?w~zr7PrYm(tp5&|uVyYd6^f{I#v4n+Oucp{wazP(sZh35UOG?P&A190 ziCFs(EC(6qb|6$N?>P0aRX6k{u3%_p-t55UQ_can^uNBuo%DdY!_pUYEXX)|s?^M0 zvc*v5#;M_gRh?8Ao;{!7jiNJ`V2+|wI+V!s4Ae<@d@pdf0arQPvZwdfM@^SCfMX>MFxo8c@|1=YF=n#vY$)DW3c$a_9K5aIniQ*l(e-M08nw z-MX;La%&vddhC29=wiaD65GJM#RPOY_EB9@rrJRUlOkd(^r zMPVD725xM(5xdj7UlB_=xMhp(B_g*fo3VPOQVdl_9hLsRY7qkRDSf?_LF}Ma+g}RI z?phK0eyFxHH{ekn{AwENdp5To(Eago+10wUuhg|vk8q_>^%%7Jl6T+g0d4d=+rVCS z?(b6SgVL!4ZRDR>8e`*^r(a;ThR|X+^cf$ z-19ObHm6{aNbJ$|m|ohrUsO@z?m!fFYhj;2Jl^%U{Jds4b+AAJcF0d%LsH|mAqPGY z|H{t$-7IM9ad+k4b${{v-e{cLDiF*4-O$?aThHf+-k095SlS!omal30N_)2pXjczO z**P(TV60C9Xq!2{!j8guE4A_Uc@f5eBk!i~$GJJ=WCRM@cpd>*uYDWbRI2lLH?-So z#Oc+}F?jT7W1^qn>+L->vqO?*5-k~=ywio<;T*E45D3v&Ys7A4hmE@E@{D~nk zSh+E}K^?}VxztRRU~o@0iVjV%r_cu}e8@F3F=MDK$!SXz+hhpB-$135u;Kj^jX=c| zoS6Nc8{Nc{0_Zbx+?iY^{Z~l`pA0%%n?Y4WiTA$FX^VQ1sWO5Z7nBh4@D8&0TzZ3S z*>S{oo;cen`1vQW;qBXz)=;jPl|v!vq%-4L(&xm;<8GY9($~8Ocpnm`iUd`t^1`%+ zcdo%zPDlZ2b<;V{Uyy2BU;}W(TZ3H(gmL;^M-a4Q%WSrcKDtu=)Ix_+()8RUg^K2a zlhlG`%}!ovNPuI0AC@?V-oCf{l^waAscA>81{X4!NMlSJbzQk+pc8H+IYCbl-x$%_ ze+7NOF54twvC(sV-47kES}X;G%%(ygwKZ`IY<`}yxy~-AEXf93S9GArljl|pZw_$F z1CYzBC2n@-n0g?B-4zA0bTB~zzPIj6j zM0x0~*QAyi(fUaJZpKoO{~U4AN}+MS=SYCow#PWMV73vm9gSt_QiwRTXz97}ke$k% zHdv=Hmd;jJNF265{VdrbXA-jmLh-I}<=Ljb5x-8?H=XJ@N5}oeDBk**O>m=j?ezK) zKrW>Dsz4T6WJ=8)>>|)%$|8SMPI(WB)b%9nvJOww|IS1&@P?_^KqkUzfc7igr4Jub zOhMz0c6EzTEe$d1vLuF+BecvKf6!bah<_!i{)#MUi|gQQlk@e(_wqDvU6^k@u$_ju zzxW>QSJvx#`m47e_|;31PY(4?I%hO!<5ani?nVPYTWjS44XqfeFa4)JM_*p1yj%*V z&4wch3=KK1EH?X%wGn1LIm}^+x(W{DImyQeTiZGc5r_edWt%d{R0{j|mGm?`Uj$fT z5rhP^di+`+Jh)zcotw{$f=!<{ZmC|}(9oz-V?=IEv8llXWS^;~Bq+|%cnEF4Xc8aN zhV02ZG^(#NpQdAG!q+LexMRu%4LdW(QxZ68H+-p2K(WucR>urCRTjUTbuz>sT@4z@ z8TDlCDz~`HTUe%tv6Ftr5!x=*7A4d_+Y#ZfMN zjq}HQTJ#}^1%CLBiR}@^h&iPTz{=obr~)cBM{$~0&#ok8uo{%eQJsqMJ| zeq|+|U@^=IivS{t&Ip|W*PbSh*T=2H-k-PM#F(U0I0P=0wI4|Lx)kE|f15?sUO)s5 zXBaVPtc8CalhY^1KO^8sr~h7(5F%ZRV$RGuGM*nDE{h%8gNU)K7p8ZPi)@HZ)hnNS z%s7?;i;+$NyD^$HcM)@w(uKhCeji>>aBwT^djkivp@3JgR-%H_FcOYgyN#G{%cv|* z%aUbk2Xzf%)N1Do1enXtL7YyN7OOxwkKoL)hY?;w+i!<=yHG^^V|w{CuCVcS2lwS+ zVjy&{H($1Luf@4v=G!cxcopk=`hDK!vPPL;Os^3PX(7jr*>F5#q@!}8KrjB5W61Vx zm`M#otz%5!#a75H3mJ@h8vi*Eu73hfw24~6D<0{G7(;_M7~2wYPmA-YBOiR>RK}g% z1kVeY%4HyiK67?%A|>0-m~6cZI^VNE0EmNCYXB*3KB|yDd>44m*vIZ+_jnF7Q5h`%#ud1B-K3h|h|OLEv~* z%W(^Dn8&rij8SaqWHFs#r4fvA#{L=B&SJi#Qskc!YRu^qI3g;}yI=5x;MW`O__Oyj zYJePtG|Em%gbQAlsX61k;f>7Kd-bydT7^a(O}!Q7L{}$1a_w6!r6iNUTHc@ayR$&d z6)S`lm>S)Xdsbz**|RUEc78&)_sG@$xc))~wt41T93K7-RsJUregg|aE!_uUPd42Ua<1*)|J1%7TY;M5ch}nGg`C1IFzau{uDi|fZV7Ip= z9gGg~!Ctov7Jg6$HAoLD2{QW{(-2b=rr&YV`x>{Zg-=AWD0AU{=~v=2T}C^20_i~| zi4;R*^Cogh7H7PovFMO7=8qViW0l!NyFV5?rU1ka$*2HOSJ0vp&Yu4J;UAEC=32R9 zE49REVaN9iFP98ktEymIyKaiMLj+ul&<#YIV?Mc7i;Y&x+_+FQFmG#7HNixd9*k4C z%6wv`c!XGuX`u(|iw0+yST6}4v&L*W6G_YR$t77q+^TlB9OfY|r@0%67r;CQl1)14 z_4ZTb6pc01jq8y)kStpa+;aBm;@xDEE}BLy5_rHD0^seY{7A;0GLfj!gK_P|Rhu6B zE=Z|W?LB{Y4+hOfSxm(8&e`*f7(LHX_Rd`f5R~=e_fAphy7GGruh%}yWWB!oGg#By_JebM3tane zdjtP|`|IBg*8Z`|k+bGGtNA(nbg%mC;%EXam zV~0@I%frdN@}gUvBj`9^dtv#)?xAo+`PKQwvAEg&(z%Oa)MF;cxJ@{#YRTjFa(FnP zlL6TOBHO{(yvjJ*x`=Im+t4Dkc$!(KO%z~NFEmXjr^GGUm_sso=6Y#4p~~|4M|*q8 z9x%&gnM8Re7jV?_diK?zF$$L60hxxm?qq`a% z`&FZL+NYx@jRwvJhN;KZZM{dwZ4Zi`>n}UWm$S)jq(Z_5>IO)Uqe9jrJBwGPPfz+B z2ezoxr{xzOulwg_U1g4Wo*eBu?;jtrNKjv9)IpzrH2sci8XsTnZoWRQ_j%5@mAw7s z^VHKdQe}DjF7wdm$N0H*W~204oLv>E-@Ui+$^vW6-C zy>0Xnw9iOJZMM#V`&F~owWBPf$~&!(!M%1Qq(@4D`YXep1$lWM#H*w)tG6Bozi;9J zYflzy^I2`y#MP5!x=bU?L^rIvMRzNT^^f&SBrhw?Mh{1`o)2CG2>y;QOXpV(0gcAvRQx7$^Ueq&mkyyKc{?MwGV|Yd*MI6Q%e(| zpWKheMZ3yCnpW1!^=tgWQSV#a9!-m1R}enk%mxFCh-xM(pL0bN2^ucKiNx>KC@)T+ zqWu)S%H_Ts$s=o3A}h}%bx9Q5X~o`P=9Lp;zyki|NldT?8h@xw&lipo3$N-JTPliM zxw8{-h)dG;Uds|1!Yjxh+o3{l@7Y%(ruR12RwK*=UIhr=ULy->0#wG=0pE!hWU!q` z)lP-J8NY-ohvJvvukgPwv2Z%okF%wcb0TFw6*7DCJo<#&z}-&3{&9caB8XC3iCAev z<4u2E@63(0-L!fu|0T1#!nnR5MZJuhG;a~Z*4g6 z9lvZ^eQ$w~q>xKn?-Qx zh%xHq3HK&(qtJd!{U(9?5g!BYH(l88iauX?HmsWW2^)cmAru&O^lGQ$ULH!>dF-!O2dMLi5go!UQDgPAX`-fQE>H!h-zdXDi z#jeQzR1@3#nKQxmrT&i^6^+|}EBEo{y~v4M9Au8WjHL<{+gI1$N_%)yyd*RD*s==y zL~XOYaBguCZg7&ASbT$j3vQ9;h$i-hP1N$|n2PLq{Xc2XicDWz7542;+_?YCSKl(b z2$8cjz4idbHyAdtt7HlN56O@JN2BRaU3gzF@Ay>%A2A_v?Elmz^$&SV!nbdX!r;r| z&q0}541fE?f9d^`u`*71hm7{0r*K%BtdT&s9~gAHl`cQza-mBUOiBoHfZqUOT{ z(It$_ZBC@au+k*>%WTHQ!>m)H`%#_T$v_A}EecYdRLDRuV)jJ$<=`L~ONwbHey^v1 ztdN0c#Prmp3c^7!k=&e!hN+<-uaH^QNrcj*`fYpr#)169najbU|I2Bur-1##QS;RN z_Lp-w5sh6#LH$q8w=kSienvHLbV?0{yQL(eb|R+cx4$B4J<(%1IQYhra@vV4^%T$* zG8sHO6Vb?jxI-&CiIAFa+zft3_P<S1z3m(WfX=u$UZs2!Kr1e>1S0_>Ife90` zEJYxYm*BEjTPE>)al#l(SOgvt8s_a)LPpROn(0@e&D;kzEcAI3COhc>5#4t(o2&#O zm(ck&plQ#BWRIb5YxC)9BKg~O|V0i ztOP-ur26D!DLq!LQGpDfg8C2&rm0^Nv7k1oVs?@u19QWID&S4OQf`vsl9R#rXy-@X z(Tc*$c(dZZr+8sQgQ6gcF%OOm3=a+RgEJK+-6WkNBlGLlj*hr1W`UQ{WW}8(f0;&v z`iK>?;}{v}5E7&SW4b}GNg7Q?b_UjV3BRKyftTT6#RZYSIKx5ZBgU8;Mh0pI2N6M; z2IFp$N|2G6fwgr%-4!Fl%b>I3N|L`|KtXLn#_T9W1_}j1+^%UyhTV<6LiGihB4BJf z-XPW7#P$nEb~Ukx3xaQYbL3 znCyk59b#}*TRY@V{saof!&DZ2)3FArW;V8;D6%UTAR-7%Ga-99X@RI-(H0551MNV4 zb2dGI-gJybni`0;-r78kMM58p-GPhjq5*)hfV)wkE;;}h8)y*?>S6$Zae%vTfC&J` z1zLbWT>t?1J#hC8umZq%K#Leq7drrq58QnNoB%KZ&>|Mp#SH)x0(aj44**OAw1@+B z@d3cZz}+_>005H!E#g64LI5x+aQ6*}0KjBGiv&=Y7ywKT+aqcVxq!QGzzzWB23ll+x*Pyt zAaM5$I03*sK#OcpmkR*Q3*3DJZU8VJ&>{!aKH(8ljdK-ZjY^{xGZfRp_E!{QabJi54w)aAD=5$n#?bcbrpnC74^mk zS*|GGFYJ`(w^X$TFYMIu>$!Qo=Nvrp_nVn-bFiKrj%Yppg%lC-Of_1>0#A`4^UXP* z;JS<1XnwUZlw~z`LGi%?xQNqM*HYH$QF-iG>)^Aqc>cQHQnBQD{bv$K9L^`u5eV%u z`S_vT0kweTaQV1Rqs4!DriPx-mDx1* zsH5Y)cZ;ELIWX&FuMOaMbnJ=J@l(Tc_IuuvKW!Ir#zI?J7yC^0$}4sp?0&mrH|wYmLHb+-R+U; zBL~AOi*IHx9?b0Y)W~ruNAPo(s0*!1Af^us&d zOY%>8Hy?vN&<}%)!@V>r)Nk5NMh&GXx~Y}tto!OQD~We9TyJP5m>!60kfZh#1E9iP z;TP%@qmpS8Nl?%dwF6o`G)SYU=T`nuK8Z~#}8RObG?=wtB zP^E^^T`;-%yI`^-Z1hv-e1>GiO5F5KnT@;Htijm)b@TJgN}fulJ|0oRGEV5btxd8u zsEauZzOUGJ7y6IsrgH9InsBUF37ybU?QJK$n0b}i50PoNK{tHB*#evRq48i#KsvtHDgyY?erY?!yR~P&fK^Ye!_dp zpF224?y`DIU#$pWj}GmZ7zr#%X`)Oy_+LcC88f$H7p4Nzg<8-+VQ ze|--Zgk&U-?r16QxoO8`TI#x3xTV2|`bFz0VC^ZOo4cg`9_2-Bu(BMv_57Xx78>7K z1j>^rq453y=}oLK(^&);ci;+`#XW>C@aH(`)i|l!x4^k|w50=5&#egmtqA*udN_YQ zYNxgXY5&>yB`d?wPVxTJBGOw$VTS?~f-7Q%UWbSJ&V$J%E73RCPEOPH$}eT+W|W2B z%YYnDf`y~kpVv!HuyL1r>_Y96M>OmD-Xie zfI^}|7kTTn__A8kBk{*I(!3Wd~WRw*XJcle)ae$N0->&(|!sMy0XD+@rRl*k6695a~ zJZ<&+dntNay9uB|hU(Oh*)Is2te$5-lp}X|V5HN@Ma#(X2BP%Qi%48uBTNceIk1|9 zCVn1m`K_if#Tlv%&E#@?+9%@pBx#Sk^zp0AoS{?na>%4i1p-fz1)}G;5=MV6ouRP< zSMqo?<4=}dHm=YPO?Uyv-`-Wi@cwJWJ;8ZSB?fy*A6Av@T=1e?nQ+bG#}DXjx0+iT z>1ct~ewA=q%QG}2pUz1xiS>%o1;xS=EJevgT(LMCT>&W~FyY5IlGB&4psr#GjrZi5 zOe)6{25T16hvYGZAHTw=2bYG@w9?487aZm5 zVx9Fr@P_R`c$BYJKf0G!THG8^?^wy3cS~@4J_YZQWlDzB{I))M@@nVJ;we$#p-czG zOymeMXDfq*E%?dvKw)a1z#^JsITEE$J?0;!^PBr zT++_k&vi1fC8h9@T_|Kd&xNd#{1zR;!!%8R3^p^&ssgi03-c z1j&ewBy`1z`P=Xo1`MclXpe4`UZ%lQc%SvHNg7fj!J+VvWPO+BH)*^2YEE)0$Cc;l#Uj@$-&p3^D-~(UJmVplLyZ-_3mo*> zQ#a&+4|S8T1TL67{t}9g4pfvfu9QvZUg@lIbv2cPb1`6pWP182LG5bEo*veFsoIP2 zZ9C1Tr+a5wgr!{!V58dKP{Xt~&Af9KbY8>^eaX#p3-8YcEroPh*|NjKqI&=u5qoRf z(Op`Tu&Ijo^TtJYuq$CkX&Gx`$L;}~v-4w46)pGL?O%G3e9ojcjIYikdl8*~hVqao zgps@3wqDQc_yD{#sQCewe>`T{)Ud`fh> zYO?V0MuJnazso^2GAiVRdxl6BsqA%k4WH8CQq#~g`ah{6_Gp`{weLX3^ zk3zjj=|3RN%0$NVNzQ(gl*ekmiKH>H-5@q3#Xi~20i&O!MxEF zcCxd0nF{F`^x=o%-YpwUn~#vXIw<^ZUj_XpX0Boq0EtQVjgs~Ut9DJe0pfcjUE1Au zXJ5a0z?^jsQIF2?HN$3uW#B#>n}qrD#W)%u#3YXEDahO~uz$M89B^RJJ8fr9jh1wL zug?$qWqRKSk)B09$$=uVq-o+vR=tbTgn_se*`hGEQi3DZDSYnFf)^JGM*5mJAXf51 z-JIb+O5_2LJp%1pcvCZzc`~M;-|_YNuqgvXRNpv1^94)h+*GWdO9DQX79YlnCx^c! zLGB}*uS2i%m0w1p5yFRvcC08pN_&{(Rkd#qXrO#K7tmYS7X2_&^+SP#Dsd=dBBr{a zzG4Lr>I-ilTTMHh7m0O_Bg z?bs{a?)Vn8*)ab<1Z}RrK|4#)b(tGI{PY=z=$9!5>9=n&-xN0I6fLv)KG2RUIASB{ zRM3aOlGEQ*vxul+Dl)P2e)FxBHIDMHML!Fg`FOs&T!oA(#`UQpjl3ngM?^hi40LFM zA8_w?X@d(Ft5hz)Y)+lFk!;liEH}cHE+xsTMCW5gQ6Fbtu`%e%Rmiks^ssiNwpGo@ zj8$(_&(gbQwYH(2Y4^9%QnJFPPBze0H(BVhy{5Du8ZR2^^UXN+%%3r!luIe)zR0py z1s2bD?g-BrR?QR~9Li3LY(7@)u8?df`LpP~p#^GZC2!~@p{vyicN{+Q{P0mx7u1F5YzMa>TAw;dNaX4dp!vkCFr)-q=Jk^xs`+EVq~f8QKZ{(Vbz+y3Yx6?kxyx&V z-sr`O7=Zhrb51k#oc2fQlQ9)b?+9E2r~Sy%!*^VBYkmT_jK=3x9vLq@0r{4;$(fn3 zD2eIym-w4==Tek?`v6cUMQ3%>SK z5|qrqHu4NZsWEPWcA?mD#m5Y`&{gRki4EJtqht1LgK?g$fjzto@_RBnaP0Qoh^9bR zEf3ChVwypUW4~z1Zm{Nf%^2sJyTe7#qm2p0QbVA<-DraYT<_;<1m73J1R?$y^Yk z+w#Lo;aZHsWmn14YivN>mMP^bm$)fmR^Czk%?sUnJk^uUz|#4C>k>0&sgf2Fb$qam zxkqkWNp=njg;3ITMxZ6UF09f!#jZ->M^SF9VEmTdg8^((O#xj6R83RIOVm1Y%dDux zfO+NG>scj2LaVHq#Fq&gf=pIzq29DfhDM7Wxe zyUwovStJJs*%cS>wfBZ~wA8icITZJw}XXHhUWZFd5`jc9AW;)P5QqVfc_rp`+KLpBe7NqnhiPpR!A}> zXL||q>!FJ|#5Yu?5`m!9JHXc{VrocYM`c_p2KW6wtNT~sz@)+6%>26^HR$9-JuaDG zvqf0l7(d~T6~Uq22K+Tvv7^L8@v3~!5`qK0QQyk)>+~-S6EglBuCh}P+PFF0xhA3= zgyFshFYMcH-PqEWX1uMH*u|$Y%{AV+q5ZT?c?hKjykGrkQ}Om!zN+BbNzuU9p1*_& zmS&1w#(x(Z!ARY?BJ?$1K*?PQ|3oI|Z{U8bTqaUNw56DzrE277^YHh^v6Anr{LEVe ztV8~%Ci*w?Ip9Br2Hh@yhX($LD5tfNuw+U2vN@ck%M(=Y#Y$NjzpOCoeyYGAiFsrf z{}~#<0#=Ch^*>_31V6v;3(&T%e|j11%oK$Gkyd9Yxy=w~k^gHbzv5JDnv}HKsBpUC zhqu9YYDGVT52pRq^F!NCfqLF@4!Vy8T*k2a{z?dhkNb#V_EhUJd<;ttI_QtioEy@qqGq!LjK3jW!HJi&;_}CWxD`7$S?9=hg^<6!7{PF%}#p>{&t?ByLk>xEfgDvGUreB{MSu1sk z!w1%Ow)AwK?F38=E6-!Q!kiatMN@D2O*egOe-x9u2Y50v)K2GrPv71B!pJdtJo}=g zS&V^LJC2sx#GQWsjju@(`7&D?a)i?M73zHr*k^zr2*jdqRIjmF6HF->($ zmJCsRRp2p+r(UJ_+Me>o)V=w4+veV!lsx-nVqnwg+;9!5gG-w`9AmzYgDz>Gu_9mR z329E_>y>;`uGSc;z>A-L*3n508%WTr$7|z*&i*QcT(z1MR=`D~PEy_{_wQ^OMvH_- zKQ>| zbNdae*^f^kw%I4!=mVJe?NI~9VG?81Hb%sh?uPo1u!)#7g_!hmNrp|-U=|#e9!8GbNPim%KcSnXVD##? zVI#we5+yC}M3s!~`}{#m;?Vy&lrr5ca)DO#ovG6<0v~#IH>)}8@Jc!~UKodRD^xc* zGK{w?JJ1Aj)7O>E?Bs-68^e@C4c2*e0y#w`8In z$VGphaGL`}(X4opw7XO5$VDIEpq<50CGn8^T;sm{)1OO8*JDhU>k5N1VI0kx{(TQV znl0UH0)vw$l)lHWd^y7MY3S*n|0&%SJuRdPU}{m>qVvQrl}WwW&gmcLJvKM z$^XUPTL8tib!~$L3j_@W*Wd~6E+M!Rv~g+N-66O`0>LdvaBZM*8h3}rH3V(kouB)@ z_0|7X&D?itroO4Enz~h|&pv(5Q@c+;dv)*HXFY4J+M|MxkRhENsIIC_u9RC9miV4C zXB+!5W4Gv^{dO)~E&X1%<%*#F#}z}%1=rAWk1rYZyCO%;RQ5}OxCuBo8Prr_wtZ|Lei4<83pZ?RnNa zHawYBU_uAYw-KO2S8@#wz?p^!9^$W?z>b*~pcpGWYod%F`Yhtb|L1vX7gL?h5Kot&9PHhni@ zk<6@T7|}?NUV%)f3X^aeY7A8^!!LuC>m`FI6c@V?&eJ49p*YzHR?pO;)DI*QMp5r3 zQcwr~uq^sq{PO%RW+UU)UnG9oHTL5)XuHVs11eIAkY@e2BBc6pK(==g#hF5P6}J3$ z&(T_3l*~`gXyTEJgmFhdErOokDJc|p{f?7XNg-5?G-mlMFVwxy&UtP<77%32dx+s} zP~$lfjUrP#e1t-Jy>h;8J3m3*w-LEsU+yc&u>oU08UB#a;@Eei|7|y!4PBG=tm~#@ zj#{Zo3}qewjSOI80CS>%6uxdS$7v9K$?Zi~BmGF~_cd)h3Vz5ok%f<$T7plCUn3o1 z-KnOw>Z|jbnU3-scCSKqsnjo7MX->?Yht)X#kmFN>@jhPPro{@88;&V{+3Yh)jW*x zSeD+GxCFE@!+P2NOSdEiVx2Ygy+pNYM>51&JeGN`GSils?+#$9QNK>t_5JHg9@AR0 zpR=JjKf73QN(zqMZC1?lr$2HBCFmJ5^QqWFFM9B;YdG=bT*}Uv4UFHQp=Xq0bS?d9 z#o7%fXZSc;HJhBLpgBaa{pGaorx`5q8$;ItLwTpdq~JtIK-UOFokX9aq7DO>%jT2r z@cFa&EZV>wyG)0zhk~pv_$poO4Tg766`66W4vY9hIXEG$+}W$c zj`+l41e)?}Rmh5voeHl0E(^~QdImqCp9W`D&sI-v!xgFQADsbQGS%AX9y+ZZxIl+Li#Be1Uw6|#N;6_ zBq#&6`(16DItA0KS|mIX+FNut^dHgrlgAYTomU33?Ssqy7;7q52KCRKp0BJy0(f~~ zy^nsLiGBg!M=xE3(uOl0XnhI66QH%%+YzPkAqMUGhAZJIe|~eQZq9b|d&MhPs7cL7 z>?owcCgynaWKBG-Zhj*Bw`Y=s=MEesUgSL9$JTBM;v(<4Uz=`_8x_gsfx5PU$*`ah_$HlYLZC zurBB)38H`7WaZqWdB2+pX1$=mRuYE5_)lh__Q(K!fiN`hK=+%~a_=GDyY90ycQP`Q zkLcb#bGF$=JfY~HUK~ZA&I$KhhzR5;;V~>esor>x))%b|DKcU-|H=? zqJ1zMHqyGEQeZ19SXNvH5vZmy1Xjc{>O9H+YPX=Y_~K^Jsh5b70#H_c!)U$LbU1t5 z;gcX>18adOh0JN-0VT~TcAr+`S(S+vQlcO5kT)La!La(+2hl0~0vDxYhI)+;pwu zR1<#|IP#aiQH}Ni$w^tj@-|Thnq9$11BXMmY&&*UiI2&WXWp>xn?HN3x+ChX5nhFF zPQ7hDjLeFzKFl$ienc(~^=eDf)VOu_n3E_OXxc0O^6q3*eJ@k%Bwxv0u>>hz@uNhN zMa%tocH8`D6BAgwU_Z~h?ecu16Tb}Ywh3SOdhk3aVAcD4Xn?osfQwaekUEvDk6nDc zcQ#`~5+b-?TjqbAnw4o7+gIgY##`n7c9XTb@>;+ri@>%c#rApO=|!25Dc3DkhR1fp zPS9Gc{cB#LdNCrE8JGn6h*uu4Vpf>_XNpo9qnvF#(1$586b>$ngt5jWP=g-d%VVm@RD1mjGvoC!|aK^C#J>1(75ow!-4NcVT@8ob}j_rytuE)9r%pD_0skc^9O)a0>!zkHm(kwx|uVWs*;^bQlTdTsM{(url{ zF6n3hhRM(?iB$To+<=5&^ zqgEMv^3~^Nurh1YW)c1u@)d5KQkbpLPpc7YW3~6cQRB@Uo8sIR>F)phIi@Dzmi!3f zz&F2AX8%q`?zRt*e=qA&fm!hCyPBI77Na#Ly~=kp1Q|42_pNpSgK(_?vP9|lr-(%? zM{Gh-c>hkeL|VuA$BQOSsDs60bnbnImU6T@JYDARrWn;fN%$FE+LUC4Q}y{Bk+fsT zOqJ(B?mD78KfeD2JmY(+YQw+CzS~?6>fWrF@Ucj=_S6CoHok6BbeDW5jrl6d8p|_N zw}qMYg{I7r`}R-#?PoCchFFk!Btyvd!uFAZx<24dcm;VJh8H=SUwO(MbCtMt3SDMYgZ7%mY&L9>WG{V%o zh63tp+--&shw{0bq`&ZcGevR3mbOuMJ*vtdf4XOjYuKVb*u9rLIKcLg9KlyJ>)fV7 zz}>0Cu{O$_Aj^?|7{hvV&Sf!_Gc9E=HF|Ahf$Pq-pL?+n=>M%hCy{_UDN&oc*}&Z1WDq@qz>O3&IaPH<4Gb>U5Fq75Xlxbg{wi zVXX_|hOd%Q;%3UpW}?^~{E+@|`Ti`|ruL;qy;jT@e#1Bxc+jzjd=#Zy?Zoj~HSD|^ z1RMJ$_Cb2Jd#5C-6O(4v#G$S8inJ%e{H$tK+IQE_o6d_vVtTg zQ*|2tvjW<*J|7@F5oo>1(ZbnI+;J(NsB*N0?T}!#C}}%e4Q(lJpGnFBlh90ZBt2HK z(0kgLtQrsgDBI=n>wCfV_d4CxXREXE+iu4_4jg^tvM^KmHl<%oCQddRmZU8f6N?o) zHdA~_RM8kdcip$_Z9Z{rmNJTF3Fap`9=#Ap6FPh3YA`(gZGc|aHa5-%xAgkHoz*Xi`UXZ90eNm*PNFLcNZ zNXQl*a+3RvNB?*#^ikKlxV0Z$VwX?_$q}YKi5xJDM!ZfBchDn;=<>#7{z~ulNts2h z?192bDuY5l;p@xyIEeb#;WY2(wKaR%s=vinG9rKk`nx1_lK-I35O`0LLh@Vx^ZS*xT!tjXV;P4p)c{V9}G;F`K8q z_n>vesI;5zEuCeesg;;*-VjNHI3?HIFg=ER6jO39!tR-Rw|ynJ*jm zOTzsJh-^#L;_>r)*UM2DE0TcXbKo%KTW?IWbqF^JK+lKV(7-kRj==wJa~gIeZ@ldz zc(S$YeDfuvt=kYQz-t4iCDX65xf%~wXi88))Aq!e%N-y%)*;_bTJg0Th}WrJwmcT5 zok)8hnKJZn#duiRAFdDVfWJaK}iOYuzbG@`WfdXyaDz5XnTel2?K;x4%f@ej*y zWA7sP=h@SnrNq?DSyZuBMH%>DS`Ncj!_vCAG}+L495}aWZ*6)*Q>$&4+sSS1)LtyG zaZ;E{5aOKyed{+mY30aaX}}1zV~u6Tj1teMAN`}h%q5EY0`tiF^D@eOHrlZ((d7Ygi2ZIeH2t3kq_gl#r&_y7%#pQn0r_pVYC!{d_>}sY+Oxz; zjO=NvGt%U+)6;Kva)JQf#7x4?0y(X`4q$Z!pExCqm_3L-pd$di>$m|AkH;XHOY`yz zl3?f6B58v-G#qEU)R;mIsoeF=JN3RDop%fYN5f=TGZ4RB-J%2%LiHvnsOP)tO#Sxo z=qK;`M9k;Cy9-L*P}9+1bs?@!t*?E0^w>!&&&fTdjO?L_Iucxc;0coZ0sQ#}Dz0=n zCNWxVtP+8Px;g#YGX~*h!tcScaE!lrqS$s)@;6t7jUeQ|BXz#JcVYAxepW)Csu|ID zE=T{6!)J}$rGzh-j7F@_O*m&Q2!>m(-_$OLt)k9b>AFg&4Rhd~j>O5kj{D+b9x7W%Nw&ch(36ra2Be&Ay>o*Y^l{F`!%MCin&dL6@*A@UFU;WYp_tt zL3VFFl&d^Pyy*iT?t;=OYPKY1-^EypNGC&^m^`uBV1a!fdx)3=*G!roGze25 zVIyPX5WF0)!y%l7^Se`%3m>-Rn-Mm@Yr@vY{@<#-2jr`r)Tfvfvh>=bpIqGMura7K zb3NU!yHt=tVjn+$4^n06M3AsYe91E~%1_l!SxW&Qm^*>&Lz9lnzOEY8Iec~EdQvdi zb?$fOnZFaD_bsqru7w!7M@cGO7DY&mkqM%u$dOMA=1q?XB6Gib%{MJkZD98i$)3lv z27$=RtX8zH^^p0dfW75U=*OB2lD_Wb_4Df#nu}((UlDAy($Tb)`adFhvGs8Imhv~q zrq^dd`F50Nv3rwMG`p4=F1#9tsv~7BKp8Gh>!d!m&16zCRx(g*^o%x8yF zfk|gjnQi( z21>b+F2cD40a{eG?mJ_%aQbpiikuuiKH9@gtb3cxG_PJ1DQdQagSxJ8?$(x27^+T3 zlIseA;#L3#!j3X5lc3U~{#^0RfXT~mYVI!SM^`(cF% zft|?nE0KH6d?`~M&6Dl^Q9vh+)raa&ub;yg+!xQ5aJ(lj6%Lg0Zw(rsF@2%E4s&6C z93$6Rf#lH~m>-??eR}43N^}p~WoyvyE_-VUuIjWucy&z_EUIy(l=3**clcd|);Hqu zelFXo!!uf9UAaQ45ogkdQgN3cw#cT+MJh62U?^=Xv#9J<0 zHk^h40@TlyD|qADUpb5A0u*scAA49@(&D)YKN1TB`xGZL`9P+1j8J)#9h;~89YIny zRjJH&wMEfv`Qh4TEejLyT&lZ^lH=ptU@{2_lK&y{wK~hlQ%p4p7)=5mA5h zLL7Z=^DU(#0GN~3>iM;^t|$ZWF+Z0r4_#)_Qwi(#<%rdyGZd!Mq5b%o8+N2V1b6N6 z*esUWaG(O8H|<5`SV^w-K_+HzazD6h{<*n`{8MK#AyJw^8FJ2=k!=*CKuh9+JKL~1 zCgpN!nUBWu#d8y7<}H}vu5s0WR%ZcyAm|jS-Pf3wwbZT>t-P`12OS8D23MEWL34V- z^8~vD%Qg6RgQ7FW2UeK&R$ai+++n*k?u&KOx_oJdQ+rCxm8Q+BSb(jNpu`d_@7D+i zx;m!cVMVry;A-^VMJJwQB5`;XU)cc~e}g zRvW=CKK3akcAt54lsx3gPgKV{MvYE2X)qiR=wQdi#W>IA(SpFZQdkQ6*AN#~$ZlnH z1yPTAFR?N?ra$RI!^p#2R^1TiqBS`XgRbp?kq!=OyVMcY(*+a#-+ zK8_uEF3M|);SC=2?{QfS3_QUo<=`r~dSC>Vrg>VoY6xKEtEaFZRr(goq5r6zY|n9T zTcF1*!x{FC7_GsI1E1LfPYHX}nTWvwOPvEvrn?byZ-S(}Hmqms!wqf=?@z0bNFwEf z{ILVt#Mi6u+iQ@~mZX%Ua#d)q;EepE2nJ9HX(p|WkLAvERgI8GzsuAH?)0j=l;Ip~ zBYRtGFQ&Vs!xqjv)`uG*8wRFpRmFPn4OB;XEv zI6HYE|45s!AS;j7%<#vx5=a{Mh9vSrL@xgHh`~`eb~v>s~ue|;QL75o7xZs7hde_-&%0YRiCLN4w+xvO~*SPda z{k(zsP^LF5Vs9-CMzXPY(VeK6c6N2Q35(|Pbuz{cQm*?3n8f%4E26V@%o#G;Tp%)& zghWmr$JS$Dze;-&j}>ZQ4(l37 z+5Y;d)`Cyk8>Zw&LUycb(e^o#4`qF*hAh9$J}f6jQYLWL`10=nVoQ`tGUU~k?C2vN z$BcjrB>JLGtUf%)M?&iM_9a$pVeYI)cALM@&0D?H4j7|Nl}3n1o``TL zT-^DfA3QCO)0Nf{eqnI*cxJMbe-C4=W1$J()o2RfN|LaYs+-*QloDo^dbvr36&C!jt#Yn_Uu#>4P}M{-=VJ?FTIN`cx~U0J4#s`$`NWx z6Zy)hAp#FwIc9)+#kaMpn7h#ayV##wl=cvNqAS&UUYe2u=nWrFP1D}0DLk;W3skCq zGUpniithz~%$(4lB75kM*7tTUQJ_%K6$u|1aizV$jzq(%cHvj`^>@nBA{Iv?2Ab9K z{e3+ZH+8K!T&e=uLkA5p{7W|Bi0fSRYYzdP!vPkaJ{>>=&9aI z3vJCvzk*B7{KEH}$QejusZMbU7>p{k>%A4{Dy$e&~=MHj*42 zAd}zJlx$07OD1u}aDKI=m{2n(7r-O7rwroMI+dbyefiE|Y^#wrM6bT5YSb24+Ez5f z^bKd(fXy^e`$lgH4_X2%k*l5;e&Zsdm2j6At-0Q**5;ST8DjjY$LdXP15*i4l?Qzu z*MTQ#w4UUmdl`?XVqR_$1#F`sOn=c%{)l_NSAJA08!S3S^Uvaij*1~=$7c*;Q+IlG6l37rrjL4aQKL^63M`J7x z=yE#?XPEhV?QhDdX3v#4`Fe(mDVv0#;@FZXpY}kg6TpV!cQZc+PI$S#bVfo<;NFoG zPL!yB?IHfOEYV!(`cpZc2C;DcTSebt78Snm11EZme-T)?zR1&&YTOQ2uU$H z9J^LOn0!1^V4mgT#Q4QR>6>>r??p4PIW>)f4?>Zlc@>lgn|4to({%X_V-om^(I>nt z_!GyNi=Zd^Bi^{>{Z;Z#=03PBw1V)2w&uevGixN8&+w%lGLnHqXkjVNgBjy4WsUtW zj+YtaBbl0PRUmy$wZnc0(>fX9FhH(Srwz9^0W5tSqGbIYRh#t*a|@^EJ0WpvqGW<@ zir&N(rGUgIoRt&tip5e;p`Z}L(Uia!t&mT4mxOUYp zMTv_;N}&z3bW-Zw1ZzV$zGN4ZGJLp1+2Z1Jo5bH`UGGH(;N(TF_Ry(3D-LxF)Dy7v zaHeUD?9?Tht}qd2Lr-6l7=Qo5$c*5`NWB`-8rkU`n6nHgu)e>R1WYHVwDIz`Hg(GytSSWDvtcLYKm{%FA%oq z`j;O)E&VC*rvA-L`cX)*QzkJhs=l0{X{*-|)$;`wJjBXqaQHZ8>7038K@ZIjYN)3erxf`yfCsEu(;?edyj&DNcala zJ|om&OacGwOuQ7{T7G&sA}Pb;9>KgoH4=llvhdSxN(EbEge^(i1%9Xm()cr~amHAq zHJ2*q`+FIuENqSW>OLK!*+5I=bq^)QG2wPfXWEnRF6k(SC7B01dLK*&n%7Ht)T@45 zzta7TK;{HcKR^%e#18QQfp{@Z_-I{zvwm_+C2*6_3k{}_b0pQvnS>h^y3DTXZcmm3 zQdoV^ldq7EOi)H{Gj!_7N?@_X;r<;JTeF9QN&>G#Q_=wNB$JO=;)ik*<>pHX`BLZZ zb+hEF1BsFn8)Gx|Q6yVAxe_G1P9t?cx*|R7q~Cd-GayiSOX)MhCHm}2X@_eW54j@B z1@HSTb;UVkqllgS`f)|msQk%x6KS>YO+Fa&%xm$>yA>P4q(5bev>|u^Z4~Y)95iE2 zs#|XEN#3C9q-AZ<^mE%T?$d=9BzHwsj3dkUCj0!_1N&p4=v+6nP#C#O6XluRi0B|3 ztJ6rdASjk>lg!z>SX7BWYur@Qm9o#8t~mRUa2bcHg3^v-yYdH@CayJ(M~p+51xGQ3 z6*@UK^pSARTS}pongL8deNB!Xb1aaY%lu`N`1oytm}pEhS7XS5wO^_+J=-mY-T{>~ z=lESyPlL6U{6$1+QG8?-$bW*HANkgi_w6hC%X9+nDoaubvgj7(gWyX}tuN}HTahTF z%VKxm$0Y`CZw6`=1UiLE%F=fg0(uy&q>Um6CM>0-Nvoa{RGzd{a$eshuJ)W3jnMmo ze#$z(_1`H#X~+H&vU)OM2q~$tQMlaxcMSNF;u6scDI8oz>VI;NzI}0z0v$|MoE;op zSWO+A&HmEI_H43cX-zfzf|jQ~_-33G7?xyFvd^i&P3 zw0GqK!SD)yLJD&AVkc&GB-wd<(P%lD`@do8X*C09e|ZPvZRXe_@F^ycvvh(o0by-M z_B=BKc(w1Y3fAfBUm@IEN>GwxhR^2e ztF1gBLPm44VH&0{*}>XYsvfe<@`kK23gz zsrmV#Yz^(DzJtZIS|{+LTzg~uPt&WjI#TpGae&8UIdZ@|y`yGcL5l!n4*$%VA1Y_z z%&9dl*GRMQXM=-Q%fs^6Ngzzr&&xOf#R*5WAcA(d2xiw6s& zd3}FeM)RkpQj>Vp@d)~Di?U@GZWgsdj!W^&yTISgZ>_cR6feogRsRU7VZiEwaAMad z5=6eSmSJeKc1Q;P)Qh}Y%|PmlJO0Fxf~bpB1l8oAC|EIG0S9%LZMPwAIVBe&g>?p7 z|BM{Pix}(a?R+L!wDeydw=0`OB&WrbnP$iOvA$G4mF<@FO{|s)t4|}r!c?F$4J&J+ z{-)-{i#W(aHhUz&7KC3)v48BPN)SJ({}VIk2IERH=U3AhTNUTVccwbPc0$L z<&ECzKy-3g$@lgO5tMe6AP2i?(Z5(Etvnh;d0`RsAJyG2p}hZ_MN>ByR|mVlh^$Xg zR1RUo7CU~5D)az{;_0JQI~ntm3#ATtSN#&Bew10F#n}vKkO0KfrMHa&>5gAsOYM=b zVn(zpftcNRVP9oSiCJ1m$zh+X13Lt$NH`bvLj*#D_cd5E%SgGFX`(C#ZQp>#{J5RJz@7Rtpk=_ks_lZ-W= zbfQMzdNXBU|I70N{kyywm34Gx<1qXc(O#J31DjxcvnHO?LDv*a@=ju~LoU9#noR`F z$7K8S)6r*9h?eia@}m23>FfU`!03SgPnP)?r@EgV>|M?5T@BT}9L-$x|KfC1Q(o;w z)Y5*>NLYy(qFDhAyjtHZ&Z^1IlEIIljhCHTeDXoCJXDtuG}BQc0^JL{57Lmtr7(Hm z|K!3Ga`y3G7w9nk#Gp)l?d5%pv2waglUYe)*}v^f)|AG6M*N=4qNDmM6w#hKQU#sl zzT^vylTi1EpZ?hBDol;9?1j`E8}m=_afHTWT^j0^k}5-6#uSsZWF%RVIf~eMcw(X$ zN_#&HxFI-p4kim8Pp3J2H;Wr9o+*Mb%4FN4s%WZf9?Vg%(#(7w`r7*<)n=6INHa1c zXRAfs{Z)*3%RHx6R5vo&qEf5%-rV11$!10(VS+FsVrN9$ml@9}j}xxj8KUPG_Tl-n zVTJ}~v)yIF1w~Q{T5G1blTelOA7sg+W z_y<&JDJom!pI$uXw^6JK-F`g2#2?;p<;ol>gyy%6Lrsp41av z{hnOCb@uW)S?~OP(b`PNa%GVMt@Gcq<$(~gz>3bVvG7yQt@(5ydnRWcSNEr#y^caF zOBar8&%%BqV`F_q(9?s9{k(J*-mHGBHyuk9n&O=`p7k!C zw`%#q!nX_(eY)ohPR(MzR<RP*k!&N(I}?S&vpcISl*_ zb{em5Hk!|mx7%t@fq+rN5vj}OObq(yQvXBen|5z~=73f`NV_lReI=$ItG56NRDR3Z0u@WnR2vXyifG`LJ_@NoL1t-@8)5*6WFci zg|AS>{;sosJ;oE;y(?FSPz8Pds=kNeO?|1wp#U2NM6{~b*C318p;-@et*w#{x(6aY zL_goR<}Z=ha?NH6f zD=o(Q_82IsA*PncI8SujYIU4{InRQStVfQ+>Z0z0Y1>ETsl&Vqf%f|wZ@cXB2F143 zrL4(-kZN>drnCnsB z{u8g(wUNb5KNjqRe)%&beR=T2*~#KjsS1C1p_iv>6h13nI^TMc|G;}BdP5XO3KQ+0 zzRZc<+JVAXyeK4IN}u#MrsuE<7OY@e35IdqVi8T*g}dr&SzXoMpMAbN{MK$da1yQu zoR1@H-hn>6`kdb0);>1cJ=@{6R?+WQz0jI5C8~!>A(}ZZa=8tQZ6NZi9d|%(Z4V^& zvRzztk*;{{pl|HAQP!y$TEb%6US4E^$G<2c{#e)ykz6_1G$lFfeodjp??6K4K$4av zW!5k`2>#x-#Bk+7BD}!k%!}&V7JHL1Fs=Cqz<8#}cxK0V){5s<6C08-Fs%7UgYitC z@yw6$>@(w8GoIIU)WdvKtDU63wWPnjr2m4{Z^b`zni-ux$pmb};}&x_-FFMfyr(ez zRxpL_!n5l?uho5C1AiW0%DsZeT4WTmWIg@a))#z5g;lBLfs*$!)-{p~Jz*Pb+qRWSh5{PXf{O#6+%#~!bJmb{mgD+b2($%tx~ zCRr;Q9v7p~kq;+~1zl$_$r?V>AFBAIpf4@~PwHUKnJaCXpL~wd7c(p-R)vFi4kx1B zcg+MZO3Cgj$rcyL1U(?1kNq2DH;?!;h<#v=OMQo#y|I2SGm`^7eaub=IvS?;JOS5s zC0=6=B%xVSu}M$fOAM=4e@L{>>hfb{jTgLA3DH_`#=6rIQb1eKm&6@G@VFx^P=W~^r3Z@*dgU2*u z7f4Cn>p`)5q@-?O>*Vpxms5XU(`QwrFNp5hLPwrpm60B!2RZJetu{Y0KFw9w>YECp zx4&GP#|m~3)aB;sS@+UnTx)#&TEC(8a`eE^9c*pB=JomB{A%r`NAqXDx#e*4t0fVw#}VX=N#u{n`xDBH86oWE1a_uVVcgha+S`iwoorU9J!F{ zp5wf1&{i5(gD>~a=@dhlYW=MRQWc(4P}TEbU9F71?T0J-jJ;;X(^0BxP+5Lo>| z;x3DRTIN~z@-i8{2en`%9#_qjTzPSi0>!8;w;pwU)`Y$N?piE4@$}f}!14s-l%ETn zm66nxyC#hlYqPTrwBId?{yZ&ks_ZV_(v(a8kZYCIOfQ(bvOJ-7HBqP3p7^gGWIZgu z2sY5Tn8qVci{Qoxy+il;n0i%X^ry*a!KD*Cj>I^QG@OkF_G8iZV$r_Rt3Gt;9CGOd z?F8B7plN2KB?}YS-s@GL=~W*z87=JuCG7;6jUz3I5R?lMF!`{|Uo{zR{hxKkcctg} z($%0#XU$GfV)$26tXK4m?WTyG2=vND!gFqWkxL2Hws!$bQEL;GfCm$qh?Adoj0jK)!%d;!W4Ky{! zH8uYW&Rw0vJupN(KST^P!>~2O0D+LfAY=dlK^G8QULIIc{z-GTLvxmr(Z-0;=3CN# zhsWBQ#ezUmV2~65K&=bNFE9UIQLd^vJD@qsz-R+xwE2<54NBr38X{g8A~rMo?{HjQ zKw^1$WJS5G=4_AVEDfWL38PIy5_e4!_ux?a{7^d3Owra%5d@+GgXjQ&{|+C~oMmFP zF=w<%PU5aj;vODKUl>X^GaI!v8wG*P!60)0fKwMxTV9@4Q4Y|YMSs3G{jW2j73Gqe zvt63ARE##pj5gns>Z_CL2ZnU!hjf5u%(iCCAP_zngbx6q=>nq5%m0FZ#?*FRM&Pw< z%FU9Z2qX3z<+!UJ-n9vx3@4H@%#Ri+XSrL{zAy$(&K2ZgWkj{(zw=8~x!_LS?Ka7) z@{^!I#_`j)v!eINiOL6LR)Xt-TKZcjcMr*bOOZ7ANRTKl9=)=yGivECn>0Ek&z8Ec z^Wh>Mo#YbXtQO&1OeZ4DAW|Ggw;hg21|k;%rMlS>hS~G8QUh~SCz(b%SVk<7%#D!E zx$Zcs?>H7ci3q)j6r0g)n`7|xkm>ZKNLUc4S@Q8x0*O*4(| zbLhAbrMeIuHld@x!1a;o^rbRb5b{~_HBtfrDU+4M9o55@3uZ=(W_5=gY)2eA9z>}g zM29Wt=&dpM2FP>GzlVigj6T_B!W=8vFb(b7$R~$M%M5#VRhi&NS z?J@X<$aIEM8LSBTtohKCz=f2_qv4K|Var=HqdT*@M-H|p4xIp^)BvJG2s(O4489RE zoskp?8v-?3K3-}dQR*b-NC)LklZ2iu4xk+~7Ec^w8P8z!etI8kai(P1|_dQS|#2{N6D6bU;5HG6(i zYG7LGq|!)-%82C$bEA*ub$BnSYdLkIiBhA94tvqj`(p4-k?BmOGT0IF+4G^PfeWdV z_9GpRBbNN;Mgrz_WSnf|oI3GDsqsXI{pjceG5A1aI-nE@2Ld%mK3-ZNQQBnONXNGk zOIdRxIrBQ&+G^U`#RR~70>EyN$#$?D48#WmHFY`AbUDhH(?Xfkpvfa$$s^YC=Em~o z_5UOAt85C6{D8rO>^0{6fX0LNoDcZ{MG9KFcNT+3w{JP_v<8lDN7?VB`;TrT+3$G! zj&3E{@92AvZeOwA5%e70wz1v4?*9Me&i~)#<^M;$cP5Wm$!{CUZ`aYfRMWaFCh*TE z@Y@ZN+74!dftX;Rk}k(Ew}r;Qui5cSay&JJ<>al7oQ;x*S-#997I| z;mm1s$s^s#BR2Be#`4?swCB~d=Zgtm^9f#dga5$)Vdn3QfT<&y$x7$27b~7!Rdm2G6TLc116IHf&atI-x>jx?O+}l_!bOQ)divG zf{K{|AP zUrj5&m_R$9Kx;RMV>_4t2BLz2vbrEdT~GltAcz?-mCW=H{2yli&ImYc2SH#U5f}*2 z1)=ML%9#OS%z)WsrtV}W8~J2o`Q&=q;cD99#RRkY1T(wAf8hTx^LIv2-jQ5xDX(cH zuUSXSSWU~gn1DB*fM+-O#&$3Y3`7D0C3QjYx}ZE}Kp-<ZA!QNjoV%b-{I5k!M6dl47ogQ~QdD@x^KrbcSc{3Hv zUwnt%d*RYP!?zm3B~V|H42Xe$wtD>4K0l!Q!y7tdF}twE%QN~H$e`eXeXS#B#^Eu{^`Ea8_oZlXDT!`_*9)+{Vxum<(} z<7)>g?tnIbU=^+}r$Hr)X?3Dj8`hXbPTWrI@a96%5*heSc6sHaWW6@}n%3NTO_XSc zfdO;IX@G5V2Pyi#bKQe40K$=_sO581zB;9qnCrD1aI?OB>Nc|)x#+#(PVh&bbt2>3 zT2*=D#Du$MdP7pUf}p_$ER+MS;+fk!oJ?F@@ci;P_Zvcjdz+_&sdWo=5z*nk2dM$p z4Co6zEPd#~9=lkWS$kyn!JG4fgRki;*92X!jC|;y0Y&!<=E&o-+!fbNPse4^(N$9U zeM00=w^9LpEbVw*zXhv@Uebr(-VTqQOE)`N#c_W8sakDi&F)p^RlF4^h6`c#qvY!$Gk*CLoUc(iOwfe|f@89Z8CLR-B>4VYjhE4zW`{Ml#&*f}17C{A$enw`yYBuhf}{J3?^wtl%004eEk`U< zm+Q|@Dcz9y;Jbjb`4oYw(;7sTRWKdY+>z|-4f)Ag?A-qtZ>+PCE(KO2I|G#M5+;u^{m!La*U;X zT_xgrnC}K`p&2jCDA%-kY;uYA8`Ry^aycneLvy`wZ{xAA=i$kCZt&TUJ{n5dUf+7u z64KT`U1#lkqF`x!TMRkU8|$CmDjOd4$a;|?5G${-+9&vlQp9J^8R803Rh4bI?eEmy z-ZVff(@CLACK|a8=>g^2!>3mFp?A7-W`3uvVdvMI-s`e%n%A>yRStc(+cx`mPQsfo z=}V7Ak6N=w+N-;TkCWEz-uN+teHRtcvsYTrsZX{%y6%uVvRb8weUJx#IliMW@bXVFw0(wWUu%W8D=R-yE&DwLFTjZJ?JiPob>{3 z4Su-`XEc1DUfA-i7ch5o%K&(RvcE2k(<=9!_a3V7ZfNCC4)!lWXgwbsd5DGeJyaYL z1><4i8RWbh8T}rw%X0kFM@7ryL;9c5{?e^*$i_D~Q^COn$-)2M*RAmUrCaG;(f>=g zvVD60_>R=RUvE@qKxhY;eIc{&sO!Jv)Iesc?9sE@NddiU_>(7Hv}fh9XTSvq<;5!xF)x^VZqsHo`qv76vI zyNJH!@$9#)S9_DTu&^IwG3)7c;o1AlzV^}U+PZ!*N8xqj{L6*?abw&GgaaDHnUHC|)86ccC*a@ zGH>smk#aB1%2VY~$Rj#@>bcB6KF8`m+GD`lAPQ-ExZdkXnI3#>6frzrGaNm=SV$}j z4rmbeY7$wi0tc*SDjp9HZ{6B+x4AbxSY19^Kd+@|RUHS+`|{c~`rV$^(#Jda-z+57 zUe<+#H#`Dcyl*cqrq2c&%xX@$m9*|3+}gYw?ss8{D`lfCtc4%i`Tx3O&-&Bh!@Glw z%U-ope&X=40x>)jeqi2uXqtNE% zR_`0JhsK2m-+5QQqu(b|`dNL(k*q18qvNG(hRN z_pmS{X;rW4jJK?z?0oNarTO_Oc<*G6{`zKYcRX8xvAN~$yc)bSJiH?9uolhR^0<9D zT35N2qXnz2g(8t&S&z&O2PjNuJ{cJ5J+>`DL|a9}liEcT?BaCOm;IEJc-4|=zKn?~ zN-wf9Srz5zSKIa!KI9DXy#ls)d4SrQRSmHG{0yJi7NxHr^1bwq{gn|b>DUCj_j<54 z{k$9D3YDiX_OkuGdF}k~%jf9h*T}a$YsYgYMEpH+#ydp(>&9JNGygiC6xloyFFboqnfVuYJbNca`WfvZ=DizA z{fwAWri@4XXe*s^q$_++h*wt8`5M=TF1h)gaVaYSqoD4rxD>LOzGUH^;U}-d_{zdrV~B#Mn0=r zeuxsnGh2-kH&y*p&v35yZZVn)ODc^Ze**ZHpXT zn-{mO0Y*Obf2wu#;`TMbD1!b^wJ2WPCI+!Tq5qfdn3}x{biqh z_RZP*uCu<~p+6?@eeWM$l~>e6r#`0GEgVgM0VHVpnEu#efv;Qs#uHJ>S~U^lds^#u z08QVhCOY&n1F`GgN>@!>eENRY<{wch*Sd*N&}f1%>B)iEq_@(&3O6dElnt>-0$;n< z?M$SyMoo0-$zr!1XnIKD#ymaQA4>^*113_pIk+wFamqgbyCi>OW|Xo{P3-l315MA4 zT0P%u*^T`6sYrPw&zhX z6Q{O8Jtb|8+|MqHnK_QfvPaia1}?4A_+DB?s(fs%bYvTkDydgjFrp>Q+sDt&-EXz{+&4Ew-V@e*}9S7>@ixv4d0wdIe^qW|F98R`lv;>@2j)KG*(xRm(g6U8x2XQ|O(7Rko_*Da5lj`pJ~`YdHB?3hg@1 z>p`hT9J+^=CL>rcD&-^YS{OfsNVRaBAfW z2=q1Vbtg^!H5`41$?gj)yu*y{OVteL9_?d%GhDS-NHh=AdBXMp9VrmfLUg=eaj2Jd z&~G@U#}VvFj`SlW`wEPoK%lFzmnltt6^=GBFZYKPnwV|k@(Rf`z;Sgylfk zD)TU0`@i?cY2DL{PVdw{{cq&Vf3KzeC#>O@uxfvAT=}mFy??^K`Q8ZOR&u#Nc2{Pg zWCp3yua{g9%-bKY8&|r7`xsW@O20NTJrHXM#9U;CQl;O5;79y1-g{p8cuyAH9gk;o zF*di7c!E!<_|&4GsGxvPt#HQ^*;b5gtt0~Z)Pi8%{`hZM#*Eds1lZ8;S5UJ9&m)x)*_z{0pXe=>D>?(-u*+LvO5Q}-#1(t|piGHer zhCurwchOy$uA3!tk?G>uMoOlmv5lolumc;VN^T1Qk`}NPgM={4K!9`x?8zWAe1b=% z2in2ryxiY@i6vTq`GIfVv8z_ae9)%?!Li`jc}9z|HF@0z(dM6B7=nz@_^uM9FJMy! zsUg3Y1bGbDok5Pc!%Bh-=S-j-B#SJ6J9r4t6lh=JF6xr$%q-DjnJ$rS1P?`A*v2yD z0?ZG66aIbawcyzKMvJCd_Re3pOMiA@_*Xvlu)8RMt-#odN@5M4S`f_JAJYZd8Xk3p zB?3=u>1;&{k!K(l1pm+<-xp*dWy%O#8PP_b2(;e_v_B;KX$n5$Q=oddz;b}fjOOlW zlZi=|1Coh(>`+Q3hS;H{N>7X(Dpi7X85C`vl?13TK;8_hx5a!4lo}v`24&m=%ny81 zO@;f^ue=*hEI4-F(eiz<_A?7Z&STgslHg9lJ>vzBh&K$Mn0>lV{CW2&^hBNs`*!5X zz1vky9S6%i5<_0So!RXxh%UIC9g!eEd07*s){l=&9+&CZZyzYE85pE&SmKviD<(R3 zy0vb~-~?=y10o$qw@}bG(9qx*s$Ey2PuAQLS4bo}3=0T%B%HRGVl^#X)p5yoi!nbE z&Rdh&Tx+iBK4v^q_^C$U-dtiX(q9?Z_jH&mgcD;$CroLuy0zE4s?((JwDbVw@U%K}(~Z zXbi|7YCSlX-#AzjRMTbZ5D5Fn?mimKu?~4E$UTx)&`wA`aUtZ?_^OKLS3`pxNtysb zg&^@Z9c1la zv$Z<0MpiIvjbrO9F`OeebQ(1}p5kCe+bdm_9_%xR92%qE*EN^QE8RIYK|Gu)6Y8mdj=KeSE9v2g3m z`gb(rU7b5Cf+p^Dy4lZ;Hg_j%sW@5T-TDlY_sPgW!e~QMZFY7cx{1AQduwq?Q%RSZ zt8(p1@(T?fi7!&bwMKC~o+`WTk$mipb03F&6=}Y6Kjk#ymlZ7Ten-3aI@c@0E3@I^ zh{LXEsPY^qo)709eDmXO$n0>rV!Dl9lsZXY@iH`tDD5NdEY$Nut1#D~5rA5IRa z?v6DU;L- ze&Xn8%u5PRL0+W0Dp~VU#eTt;W!pUsrxN@%<~K(G`RS%>jYwhd{ZXW`xfR(Ux;bXK zb*qajdH8P8nyjLt!h#jvJAUIS3Kj`OUz=+^b>oN5b~p30EOk*AQ-&V!X=*M#+_CZ* z2-=%5d9sv7UkR;*mMmEU9b5m**3{GMPWJ+yyK8_64-lr!84ubzPPKBJtN6DS`(At> zD7h{I?^$YV0QuUX{m9H_kaLyM5!#N&LGwe+A0cS@EM*JeiUi7uNrL^mr@9@^RsA4$ z2hw{dXE2c+!bOPyh6IzCZOj4KDa>=uP&OR4mFoZvW=YjtW1a#msLrP3bWX1-Ti*0w zKu`nVEu3x4vumBgJnzgHZgr@Z3s8K3A7-OuJm(Z;oih~tV8K)8z$&?{n~U;a{!USR zouO<>nRx9wSd%soQLAUIQc&nzln0?Ywc=Awb{e!Z8!*qF9B2Jpz$qv{XJ*apj5-@- z$tzA#rdl`84Z)dBKqtGXPLHo%pF(*sqnWpn(>cv#%bjc)Iz1lQG==kn2BLEOe)Sv!p-Rs95+LIn$mPGz)Zf! z*#O76PWe0M0?a&kKYKY3#5-NiDU!&!C^P?LXQTY~W2Y!nNA`dJ8Ux%V5cEs%$^dtr JL^t{Ee*rG~#-RWJ literal 37594 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 diff --git a/tests/test_xlsx2json.py b/tests/test_xlsx2json.py index 16c9006..a251fb2 100644 --- a/tests/test_xlsx2json.py +++ b/tests/test_xlsx2json.py @@ -1,5 +1,6 @@ import json import os +from copy import deepcopy from unittest import TestCase import jsonschema @@ -63,7 +64,7 @@ def test_create_xls_template(self): create_xls_template_from_yaml(metadata_file, self.conf_filename) assert os.path.exists(metadata_file) - def test_json_conversion_fails(self): + def test_json_conversion_succeeds_with_invalid_metadata(self): xls_filename = os.path.join(self.resource_dir, 'EVA_Submission_test_fails.xlsx') self.parser = XlsxParser(xls_filename, self.conf_filename) output_json = os.path.join(self.resource_dir, 'EVA_Submission_test_output.json') @@ -71,15 +72,22 @@ def test_json_conversion_fails(self): self.parser.json(output_json) self.parser.save_errors(errors_yaml) - assert not os.path.exists(output_json) + # confirm no errors with open(errors_yaml) as open_file: errors_data = yaml.safe_load(open_file) - assert errors_data == [{ - 'sheet': 'Project', - 'row': '', - 'column': 'Tax ID', - 'description': 'Worksheet Project is missing required header Tax ID' - }] + assert errors_data == [] + + # json file exists but missing fields + assert os.path.exists(output_json) + with open(output_json) as open_file: + json_data = json.load(open_file) + assert sorted(json_data.keys()) == ['analysis', 'files', 'project', 'sample', 'submitterDetails'] + # required field taxId is missing + assert 'taxId' not in json_data['project'] + # novel sample is missing scientific name in characteristics and sample name + novel_sample = json_data['sample'][3]['bioSampleObject'] + assert 'name' not in novel_sample + assert 'species' not in novel_sample['characteristics'] def get_expected_json(self): return { From 6616329fac8f02f45395e83fec994aabfd25399c Mon Sep 17 00:00:00 2001 From: April Shen Date: Tue, 10 Sep 2024 14:55:20 +0100 Subject: [PATCH 3/5] update tests --- eva_sub_cli/executables/xlsx2json.py | 2 +- .../validators/validation_results_parsers.py | 8 +++++ eva_sub_cli/validators/validator.py | 3 +- .../metadata_conversion_errors.yml | 6 ++-- tests/test_validator.py | 31 ++++++++++++++----- tests/test_xlsx2json.py | 1 - 6 files changed, 37 insertions(+), 14 deletions(-) diff --git a/eva_sub_cli/executables/xlsx2json.py b/eva_sub_cli/executables/xlsx2json.py index 642c5d3..e3c40b3 100644 --- a/eva_sub_cli/executables/xlsx2json.py +++ b/eva_sub_cli/executables/xlsx2json.py @@ -52,7 +52,7 @@ def __init__(self, xlsx_filename, conf_filename): try: self.workbook = load_workbook(xlsx_filename, read_only=True) except Exception as e: - self.add_error(f'Error loading {xlsx_filename}: {e}') + self.add_error(f'Error loading {xlsx_filename}: {repr(e)}') self.file_loaded = False return self.worksheets = [] diff --git a/eva_sub_cli/validators/validation_results_parsers.py b/eva_sub_cli/validators/validation_results_parsers.py index 321abc4..ed92ad0 100644 --- a/eva_sub_cli/validators/validation_results_parsers.py +++ b/eva_sub_cli/validators/validation_results_parsers.py @@ -164,6 +164,9 @@ def convert_metadata_attribute(sheet, json_attribute, xls2json_conf): attributes_dict = {} attributes_dict.update(xls2json_conf[sheet].get('required', {})) attributes_dict.update(xls2json_conf[sheet].get('optional', {})) + attributes_dict['Scientific Name'] = 'species' + attributes_dict['BioSample Name'] = 'name' + for attribute in attributes_dict: if attributes_dict[attribute] == json_attribute: return attribute @@ -185,7 +188,12 @@ def parse_metadata_property(property_str): def parse_sample_metadata_property(property_str): + # Check characteristics match = re.match(r'/sample/(\d+)/bioSampleObject/characteristics/(\w+)', property_str) if match: return 'sample', match.group(1), match.group(2) + # Check name + match = re.match(r'/sample/(\d+)/bioSampleObject/name', property_str) + if match: + return 'sample', match.group(1), 'name' return None, None, None diff --git a/eva_sub_cli/validators/validator.py b/eva_sub_cli/validators/validator.py index f3e591d..79e23b2 100755 --- a/eva_sub_cli/validators/validator.py +++ b/eva_sub_cli/validators/validator.py @@ -1,7 +1,6 @@ #!/usr/bin/env python import csv import datetime -import glob import json import logging import os @@ -345,7 +344,7 @@ def _convert_biovalidator_validation_to_spreadsheet(self): sheet = convert_metadata_sheet(sheet_json, xls2json_conf) row = convert_metadata_row(sheet, row_json, xls2json_conf) column = convert_metadata_attribute(sheet, attribute_json, xls2json_conf) - if row_json is None and attribute_json is None: + if row_json is None and attribute_json is None and sheet is not None: new_description = f'Sheet "{sheet}" is missing' elif row_json is None: if 'have required' not in error['description']: diff --git a/tests/resources/validation_reports/validation_output/other_validations/metadata_conversion_errors.yml b/tests/resources/validation_reports/validation_output/other_validations/metadata_conversion_errors.yml index fd5af64..13b09b9 100644 --- a/tests/resources/validation_reports/validation_output/other_validations/metadata_conversion_errors.yml +++ b/tests/resources/validation_reports/validation_output/other_validations/metadata_conversion_errors.yml @@ -1,4 +1,4 @@ -- column: Tax ID - description: Worksheet Project is missing required header Tax ID +- column: '' + description: 'Error loading problem.xlsx: Exception()' row: '' - sheet: Project + sheet: '' diff --git a/tests/test_validator.py b/tests/test_validator.py index a9031e3..19f0ea9 100644 --- a/tests/test_validator.py +++ b/tests/test_validator.py @@ -80,8 +80,8 @@ def test__collect_validation_workflow_results_with_metadata_xlsx(self): ], 'spreadsheet_errors': [ # NB. Wouldn't normally get conversion error + validation errors together, but it is supported. - {'sheet': 'Project', 'row': '', 'column': 'Tax ID', - 'description': 'Worksheet Project is missing required header Tax ID'}, + {'sheet': '', 'row': '', 'column': '', + 'description': 'Error loading problem.xlsx: Exception()'}, {'sheet': 'Files', 'row': '', 'column': '', 'description': 'Sheet "Files" is missing'}, {'sheet': 'Project', 'row': 2, 'column': 'Project Title', 'description': 'Column "Project Title" is not populated'}, @@ -170,8 +170,8 @@ def test__collect_validation_workflow_results_with_metadata_json(self): 'description': 'alias_1,alias_2 present in Samples not in Analysis'}, ], 'spreadsheet_errors': [ - {'sheet': 'Project', 'row': '', 'column': 'Tax ID', - 'description': 'Worksheet Project is missing required header Tax ID'} + {'sheet': '', 'row': '', 'column': '', + 'description': 'Error loading problem.xlsx: Exception()'} ] } } @@ -223,6 +223,19 @@ def test_convert_biovalidator_validation_to_spreadsheet(self): {'property': '/sample/0/bioSampleObject', 'description': "should have required property 'bioSampleObject'"}, {'property': '/sample/0', 'description': 'should match exactly one schema in oneOf'}, + # Missing BioSamples attributes + {'property': '/sample/3/bioSampleObject/name', + 'description': "must have required property 'name'"}, + {'property': '/sample/3/bioSampleObject/characteristics/organism', + 'description': "must have required property 'organism'"}, + {'property': '/sample/3/bioSampleObject/characteristics/Organism', + 'description': "must have required property 'Organism'"}, + {'property': '/sample/3/bioSampleObject/characteristics/species', + 'description': "must have required property 'species'"}, + {'property': '/sample/3/bioSampleObject/characteristics/Species', + 'description': "must have required property 'Species'"}, + {'property': '/sample/3/bioSampleObject/characteristics', + 'description': 'must match a schema in anyOf'}, # Semantic checks {'property': '/project/childProjects/1', 'description': 'PRJEBNA does not exist or is private'}, {'property': '/sample/2/bioSampleObject/characteristics/taxId', @@ -248,6 +261,10 @@ def test_convert_biovalidator_validation_to_spreadsheet(self): 'description': 'Column "Reference" is not populated'}, {'sheet': 'Sample', 'row': 3, 'column': 'Sample Accession', 'description': 'Column "Sample Accession" is not populated'}, + {'sheet': 'Sample', 'row': 6, 'column': 'BioSample Name', + 'description': 'Column "BioSample Name" is not populated'}, + {'sheet': 'Sample', 'row': 6, 'column': 'Scientific Name', + 'description': 'Column "Scientific Name" is not populated'}, {'sheet': 'Project', 'row': 2, 'column': 'Child Project(s)', 'description': 'PRJEBNA does not exist or is private'}, {'sheet': 'Sample', 'row': 5, 'column': 'Tax Id', 'description': '1234 is not a valid taxonomy code'}, @@ -261,8 +278,8 @@ def test_collect_conversion_errors(self): self.validator.results['metadata_check'] = {} self.validator._load_spreadsheet_conversion_errors() assert self.validator.results['metadata_check']['spreadsheet_errors'] == [{ - 'column': 'Tax ID', - 'description': 'Worksheet Project is missing required header Tax ID', + 'column': '', + 'description': 'Error loading problem.xlsx: Exception()', 'row': '', - 'sheet': 'Project' + 'sheet': '' }] diff --git a/tests/test_xlsx2json.py b/tests/test_xlsx2json.py index a251fb2..b9c2e45 100644 --- a/tests/test_xlsx2json.py +++ b/tests/test_xlsx2json.py @@ -1,6 +1,5 @@ import json import os -from copy import deepcopy from unittest import TestCase import jsonschema From f7d293e2efaa043ee801df4f17f808193e4256f7 Mon Sep 17 00:00:00 2001 From: April Shen Date: Tue, 10 Sep 2024 15:35:53 +0100 Subject: [PATCH 4/5] fix biovalidator output parsing --- .../validators/validation_results_parsers.py | 4 ++ .../other_validations/metadata_validation.txt | 15 +++++++ tests/test_validator.py | 44 +++++++++++++++++-- 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/eva_sub_cli/validators/validation_results_parsers.py b/eva_sub_cli/validators/validation_results_parsers.py index ed92ad0..274b7a6 100644 --- a/eva_sub_cli/validators/validation_results_parsers.py +++ b/eva_sub_cli/validators/validation_results_parsers.py @@ -134,6 +134,10 @@ def clean_read(ifile): if line.startswith('Validation failed with following error(s):'): collect = True else: + while line and not line.startswith('/'): + # Sometimes there are multiple (possibly redundant) errors listed under a single property, + # we only report the first + line = clean_read(open_file) line2 = clean_read(open_file) if line is None or line2 is None: break # EOF diff --git a/tests/resources/validation_reports/validation_output/other_validations/metadata_validation.txt b/tests/resources/validation_reports/validation_output/other_validations/metadata_validation.txt index 3b805ec..0bc227e 100644 --- a/tests/resources/validation_reports/validation_output/other_validations/metadata_validation.txt +++ b/tests/resources/validation_reports/validation_output/other_validations/metadata_validation.txt @@ -26,4 +26,19 @@ should have required property 'bioSampleObject' /sample/0 should match exactly one schema in oneOf +/sample/3/bioSampleObject/name + must have required property 'name' + must have required property 'name' + must have required property 'name' +/sample/3/bioSampleObject/characteristics/organism + must have required property 'organism' + must have required property 'organism' +/sample/3/bioSampleObject/characteristics/Organism + must have required property 'Organism' +/sample/3/bioSampleObject/characteristics/species + must have required property 'species' +/sample/3/bioSampleObject/characteristics/Species + must have required property 'Species' +/sample/3/bioSampleObject/characteristics + must match a schema in anyOf  diff --git a/tests/test_validator.py b/tests/test_validator.py index 19f0ea9..6a65a87 100644 --- a/tests/test_validator.py +++ b/tests/test_validator.py @@ -71,6 +71,17 @@ def test__collect_validation_workflow_results_with_metadata_xlsx(self): {'property': '/sample/0/bioSampleAccession', 'description': "should have required property 'bioSampleAccession'"}, {'property': '/sample/0/bioSampleObject', 'description': "should have required property 'bioSampleObject'"}, {'property': '/sample/0', 'description': 'should match exactly one schema in oneOf'}, + {'property': '/sample/3/bioSampleObject/name', 'description': "must have required property 'name'"}, + {'property': '/sample/3/bioSampleObject/characteristics/organism', + 'description': "must have required property 'organism'"}, + {'property': '/sample/3/bioSampleObject/characteristics/Organism', + 'description': "must have required property 'Organism'"}, + {'property': '/sample/3/bioSampleObject/characteristics/species', + 'description': "must have required property 'species'"}, + {'property': '/sample/3/bioSampleObject/characteristics/Species', + 'description': "must have required property 'Species'"}, + {'property': '/sample/3/bioSampleObject/characteristics', + 'description': 'must match a schema in anyOf'}, {'property': '/project/childProjects/1', 'description': 'PRJEBNA does not exist or is private'}, {'property': '/sample/2/bioSampleObject/characteristics/taxId', 'description': '1234 is not a valid taxonomy code'}, @@ -95,6 +106,10 @@ def test__collect_validation_workflow_results_with_metadata_xlsx(self): 'description': 'Column "Reference" is not populated'}, {'sheet': 'Sample', 'row': 3, 'column': 'Sample Accession', 'description': 'Column "Sample Accession" is not populated'}, + {'sheet': 'Sample', 'row': 6, 'column': 'BioSample Name', + 'description': 'Column "BioSample Name" is not populated'}, + {'sheet': 'Sample', 'row': 6, 'column': 'Scientific Name', + 'description': 'Column "Scientific Name" is not populated'}, {'sheet': 'Project', 'row': 2, 'column': 'Child Project(s)', 'description': 'PRJEBNA does not exist or is private'}, {'sheet': 'Sample', 'row': 5, 'column': 'Tax Id', @@ -162,6 +177,17 @@ def test__collect_validation_workflow_results_with_metadata_json(self): {'property': '/sample/0/bioSampleObject', 'description': "should have required property 'bioSampleObject'"}, {'property': '/sample/0', 'description': 'should match exactly one schema in oneOf'}, + {'property': '/sample/3/bioSampleObject/name', 'description': "must have required property 'name'"}, + {'property': '/sample/3/bioSampleObject/characteristics/organism', + 'description': "must have required property 'organism'"}, + {'property': '/sample/3/bioSampleObject/characteristics/Organism', + 'description': "must have required property 'Organism'"}, + {'property': '/sample/3/bioSampleObject/characteristics/species', + 'description': "must have required property 'species'"}, + {'property': '/sample/3/bioSampleObject/characteristics/Species', + 'description': "must have required property 'Species'"}, + {'property': '/sample/3/bioSampleObject/characteristics', + 'description': 'must match a schema in anyOf'}, {'property': '/project/childProjects/1', 'description': 'PRJEBNA does not exist or is private'}, {'property': '/sample/2/bioSampleObject/characteristics/taxId', 'description': '1234 is not a valid taxonomy code'}, @@ -201,10 +227,22 @@ def test_parse_biovalidator_validation_results(self): {'property': '/project/taxId', 'description': "must have required property 'taxId'"}, {'property': '/project/holdDate', 'description': 'must match format "date"'}, {'property': '/analysis/0/description', 'description': "should have required property 'description'"}, - {'property': '/analysis/0/referenceGenome', 'description': "should have required property 'referenceGenome'"}, - {'property': '/sample/0/bioSampleAccession', 'description': "should have required property 'bioSampleAccession'"}, + {'property': '/analysis/0/referenceGenome', + 'description': "should have required property 'referenceGenome'"}, + {'property': '/sample/0/bioSampleAccession', + 'description': "should have required property 'bioSampleAccession'"}, {'property': '/sample/0/bioSampleObject', 'description': "should have required property 'bioSampleObject'"}, - {'property': '/sample/0', 'description': 'should match exactly one schema in oneOf'} + {'property': '/sample/0', 'description': 'should match exactly one schema in oneOf'}, + {'property': '/sample/3/bioSampleObject/name', 'description': "must have required property 'name'"}, + {'property': '/sample/3/bioSampleObject/characteristics/organism', + 'description': "must have required property 'organism'"}, + {'property': '/sample/3/bioSampleObject/characteristics/Organism', + 'description': "must have required property 'Organism'"}, + {'property': '/sample/3/bioSampleObject/characteristics/species', + 'description': "must have required property 'species'"}, + {'property': '/sample/3/bioSampleObject/characteristics/Species', + 'description': "must have required property 'Species'"}, + {'property': '/sample/3/bioSampleObject/characteristics', 'description': 'must match a schema in anyOf'} ] def test_convert_biovalidator_validation_to_spreadsheet(self): From 71de02a88409adb40bfbe518c3cb5f44d3fb46e5 Mon Sep 17 00:00:00 2001 From: April Shen Date: Wed, 11 Sep 2024 09:31:29 +0100 Subject: [PATCH 5/5] add scientific name check and fix tests --- eva_sub_cli/semantic_metadata.py | 37 ++- .../metadata_semantic_check.yml | 2 + tests/test_semantic_metadata.py | 45 ++- tests/test_validator.py | 286 +++++++----------- 4 files changed, 189 insertions(+), 181 deletions(-) diff --git a/eva_sub_cli/semantic_metadata.py b/eva_sub_cli/semantic_metadata.py index ced4365..8417144 100644 --- a/eva_sub_cli/semantic_metadata.py +++ b/eva_sub_cli/semantic_metadata.py @@ -1,4 +1,3 @@ -import re import json from copy import deepcopy @@ -6,7 +5,7 @@ from retry import retry from ebi_eva_common_pyutils.biosamples_communicators import NoAuthHALCommunicator -from ebi_eva_common_pyutils.ena_utils import download_xml_from_ena +from ebi_eva_common_pyutils.ena_utils import download_xml_from_ena, get_scientific_name_and_common_name from ebi_eva_common_pyutils.logger import AppLogger from eva_sub_cli.date_utils import check_date @@ -22,9 +21,11 @@ BIOSAMPLE_ACCESSION_KEY = 'bioSampleAccession' CHARACTERISTICS_KEY = 'characteristics' TAX_ID_KEY = 'taxId' +SCI_NAME_KEYS = ['species', 'Species', 'organism', 'Organism'] ANALYSIS_ALIAS_KEY = 'analysisAlias' ANALYSIS_RUNS_KEY = 'runAccessions' + def cast_list(l, type_to_cast=str): for e in l: yield type_to_cast(e) @@ -36,7 +37,7 @@ def __init__(self, metadata, sample_checklist='ERC000011'): self.sample_checklist = sample_checklist self.metadata = metadata self.errors = [] - # Caches whether taxonomy code is valid or not + # Caches whether taxonomy code is valid or not, and maps to scientific name if valid self.taxonomy_valid = {} self.communicator = NoAuthHALCommunicator(bsd_url='https://www.ebi.ac.uk/biosamples') @@ -47,8 +48,9 @@ def write_result_yaml(self, output_path): def check_all(self): self.check_all_project_accessions() self.check_all_taxonomy_codes() + self.check_all_scientific_names() self.check_existing_biosamples() - self.check_all_analysis_run_accessions + self.check_all_analysis_run_accessions() self.check_analysis_alias_coherence() def check_all_project_accessions(self): @@ -68,15 +70,34 @@ def check_all_taxonomy_codes(self): self.check_taxonomy_code(project[TAX_ID_KEY], f'/{PROJECT_KEY}/{TAX_ID_KEY}') # Check sample taxonomies for novel samples for idx, sample in enumerate(self.metadata[SAMPLE_KEY]): - if BIOSAMPLE_OBJECT_KEY in sample: + if BIOSAMPLE_OBJECT_KEY in sample and TAX_ID_KEY in sample[BIOSAMPLE_OBJECT_KEY][CHARACTERISTICS_KEY]: self.check_taxonomy_code(sample[BIOSAMPLE_OBJECT_KEY][CHARACTERISTICS_KEY][TAX_ID_KEY][0]['text'], f'/{SAMPLE_KEY}/{idx}/{BIOSAMPLE_OBJECT_KEY}/{CHARACTERISTICS_KEY}/{TAX_ID_KEY}') + def check_all_scientific_names(self): + """Check that all scientific names are consistent with taxonomy codes.""" + for idx, sample in enumerate(self.metadata[SAMPLE_KEY]): + if BIOSAMPLE_OBJECT_KEY in sample and TAX_ID_KEY in sample[BIOSAMPLE_OBJECT_KEY][CHARACTERISTICS_KEY]: + characteristics = sample[BIOSAMPLE_OBJECT_KEY][CHARACTERISTICS_KEY] + # Get the scientific name from the taxonomy (if valid) + tax_code = int(characteristics[TAX_ID_KEY][0]['text']) + sci_name_from_tax = self.taxonomy_valid[tax_code] + if not sci_name_from_tax: + continue + # Check if scientific name in sample matches + for sci_name_key in SCI_NAME_KEYS: + if sci_name_key in characteristics: + sci_name = characteristics[sci_name_key][0]['text'] + if sci_name_from_tax.lower() != sci_name.lower(): + self.add_error( + f'/{SAMPLE_KEY}/{idx}/{BIOSAMPLE_OBJECT_KEY}/{CHARACTERISTICS_KEY}/{sci_name_key}', + f'Species {sci_name} does not match taxonomy {tax_code} ({sci_name_from_tax})') + def check_all_analysis_run_accessions(self): """Check that the Run accession are valid and exist in ENA""" for idx, analysis in enumerate(self.metadata[ANALYSIS_KEY]): json_path = f'/{ANALYSIS_KEY}/{idx}/{ANALYSIS_RUNS_KEY}' - if analysis[ANALYSIS_RUNS_KEY]: + if ANALYSIS_RUNS_KEY in analysis and analysis[ANALYSIS_RUNS_KEY]: for run_acc in analysis[ANALYSIS_RUNS_KEY]: self.check_accession_in_ena(run_acc, 'Run', json_path) @@ -98,8 +119,8 @@ def check_taxonomy_code(self, taxonomy_code, json_path): self.add_error(json_path, f'{taxonomy_code} is not a valid taxonomy code') else: try: - download_xml_from_ena(f'https://www.ebi.ac.uk/ena/browser/api/xml/{taxonomy_code}') - self.taxonomy_valid[taxonomy_code] = True + sci_name, _ = get_scientific_name_and_common_name(taxonomy_code) + self.taxonomy_valid[taxonomy_code] = sci_name except Exception: self.add_error(json_path, f'{taxonomy_code} is not a valid taxonomy code') self.taxonomy_valid[taxonomy_code] = False diff --git a/tests/resources/validation_reports/validation_output/other_validations/metadata_semantic_check.yml b/tests/resources/validation_reports/validation_output/other_validations/metadata_semantic_check.yml index c0c130c..39ed5d0 100644 --- a/tests/resources/validation_reports/validation_output/other_validations/metadata_semantic_check.yml +++ b/tests/resources/validation_reports/validation_output/other_validations/metadata_semantic_check.yml @@ -2,6 +2,8 @@ property: /project/childProjects/1 - description: 1234 is not a valid taxonomy code property: /sample/2/bioSampleObject/characteristics/taxId +- description: Species sheep sapiens does not match taxonomy 9606 (Homo sapiens) + property: /sample/1/bioSampleObject/characteristics/Organism - description: alias1 present in Analysis not in Samples property: /sample/analysisAlias - description: alias_1,alias_2 present in Samples not in Analysis diff --git a/tests/test_semantic_metadata.py b/tests/test_semantic_metadata.py index fec6214..35d37c0 100644 --- a/tests/test_semantic_metadata.py +++ b/tests/test_semantic_metadata.py @@ -74,9 +74,9 @@ def test_check_all_taxonomy_codes(self): ] } checker = SemanticMetadataChecker(metadata) - with patch('eva_sub_cli.semantic_metadata.download_xml_from_ena') as m_ena_download: + with patch('eva_sub_cli.semantic_metadata.get_scientific_name_and_common_name') as m_get_sci_name: # Mock should only be called once per taxonomy code - m_ena_download.side_effect = [True, Exception('problem downloading')] + m_get_sci_name.side_effect = [('Homo sapiens', 'human'), Exception('problem downloading')] checker.check_all_taxonomy_codes() self.assertEqual(checker.errors, [ { @@ -85,6 +85,47 @@ def test_check_all_taxonomy_codes(self): } ]) + def test_check_all_scientific_names(self): + metadata = { + "sample": [ + { + "bioSampleObject": { + "characteristics": { + "taxId": [{"text": "9606"}], + "Organism": [{"text": "homo sapiens"}] + } + } + }, + { + "bioSampleObject": { + "characteristics": { + "taxId": [{"text": "9606"}], + "Organism": [{"text": "sheep sapiens"}] + } + } + }, + { + "bioSampleObject": { + "characteristics": { + "taxId": [{"text": "1234"}] + } + } + } + ] + } + checker = SemanticMetadataChecker(metadata) + checker.taxonomy_valid = { + 1234: False, + 9606: "Homo sapiens" + } + checker.check_all_scientific_names() + self.assertEqual(checker.errors, [ + { + 'property': '/sample/1/bioSampleObject/characteristics/Organism', + 'description': 'Species sheep sapiens does not match taxonomy 9606 (Homo sapiens)' + } + ]) + def test_check_existing_biosamples_with_checklist(self): checker = SemanticMetadataChecker(metadata) with patch.object(SemanticMetadataChecker, '_get_biosample', diff --git a/tests/test_validator.py b/tests/test_validator.py index 6a65a87..0cfaf73 100644 --- a/tests/test_validator.py +++ b/tests/test_validator.py @@ -1,10 +1,81 @@ import os.path +from copy import deepcopy from unittest import TestCase from eva_sub_cli.validators.validator import Validator, VALIDATION_OUTPUT_DIR from tests.test_utils import create_mapping_file +expected_validation_results = { + 'shallow_validation': {'requested': False}, + 'vcf_check': { + 'input_passed.vcf': {'valid': True, 'error_list': [], 'error_count': 0, 'warning_count': 0, + 'critical_count': 0, 'critical_list': []} + }, + 'assembly_check': { + 'input_passed.vcf': {'error_list': [], 'mismatch_list': [], 'nb_mismatch': 0, 'nb_error': 0, + 'match': 247, 'total': 247} + }, + 'sample_check': { + 'overall_differences': False, + 'results_per_analysis': { + 'AA': { + 'difference': False, + 'more_metadata_submitted_files': [], + 'more_per_submitted_files_metadata': {}, + 'more_submitted_files_metadata': [] + } + } + }, + 'fasta_check': { + 'input_passed.fa': {'all_insdc': False, 'sequences': [ + {'sequence_name': 1, 'insdc': True, 'sequence_md5': '6681ac2f62509cfc220d78751b8dc524'}, + {'sequence_name': 2, 'insdc': False, 'sequence_md5': 'd2b3f22704d944f92a6bc45b6603ea2d'} + ]}, + }, + 'metadata_check': { + 'json_errors': [ + {'property': '/files', 'description': "should have required property 'files'"}, + {'property': '/project/title', 'description': "should have required property 'title'"}, + {'property': '/project/taxId', 'description': "must have required property 'taxId'"}, + {'property': '/project/holdDate', 'description': 'must match format "date"'}, + {'property': '/analysis/0/description', + 'description': "should have required property 'description'"}, + {'property': '/analysis/0/referenceGenome', + 'description': "should have required property 'referenceGenome'"}, + {'property': '/sample/0/bioSampleAccession', + 'description': "should have required property 'bioSampleAccession'"}, + {'property': '/sample/0/bioSampleObject', + 'description': "should have required property 'bioSampleObject'"}, + {'property': '/sample/0', 'description': 'should match exactly one schema in oneOf'}, + {'property': '/sample/3/bioSampleObject/name', 'description': "must have required property 'name'"}, + {'property': '/sample/3/bioSampleObject/characteristics/organism', + 'description': "must have required property 'organism'"}, + {'property': '/sample/3/bioSampleObject/characteristics/Organism', + 'description': "must have required property 'Organism'"}, + {'property': '/sample/3/bioSampleObject/characteristics/species', + 'description': "must have required property 'species'"}, + {'property': '/sample/3/bioSampleObject/characteristics/Species', + 'description': "must have required property 'Species'"}, + {'property': '/sample/3/bioSampleObject/characteristics', + 'description': 'must match a schema in anyOf'}, + {'property': '/project/childProjects/1', 'description': 'PRJEBNA does not exist or is private'}, + {'property': '/sample/2/bioSampleObject/characteristics/taxId', + 'description': '1234 is not a valid taxonomy code'}, + {'property': '/sample/1/bioSampleObject/characteristics/Organism', + 'description': 'Species sheep sapiens does not match taxonomy 9606 (Homo sapiens)'}, + {'property': '/sample/analysisAlias', 'description': 'alias1 present in Analysis not in Samples'}, + {'property': '/sample/analysisAlias', + 'description': 'alias_1,alias_2 present in Samples not in Analysis'}, + ], + 'spreadsheet_errors': [ + {'sheet': '', 'row': '', 'column': '', + 'description': 'Error loading problem.xlsx: Exception()'} + ] + } +} + + class TestValidator(TestCase): resource_dir = os.path.join(os.path.dirname(__file__), 'resources') vcf_files = os.path.join(resource_dir, 'vcf_files') @@ -34,184 +105,57 @@ def tearDown(self) -> None: if os.path.exists(f): os.remove(f) - def test__collect_validation_workflow_results_with_metadata_xlsx(self): - expected_results = { - 'shallow_validation': {'requested': False}, - 'vcf_check': { - 'input_passed.vcf': {'valid': True, 'error_list': [], 'error_count': 0, 'warning_count': 0, 'critical_count': 0, 'critical_list': []} - }, - 'assembly_check': { - 'input_passed.vcf': {'error_list': [], 'mismatch_list': [], 'nb_mismatch': 0, 'nb_error': 0, 'match': 247, 'total': 247} - }, - 'sample_check': { - 'overall_differences': False, - 'results_per_analysis': { - 'AA': { - 'difference': False, - 'more_metadata_submitted_files': [], - 'more_per_submitted_files_metadata': {}, - 'more_submitted_files_metadata': [] - } - } - }, - 'fasta_check': { - 'input_passed.fa': {'all_insdc': False, 'sequences': [ - {'sequence_name': 1, 'insdc': True, 'sequence_md5': '6681ac2f62509cfc220d78751b8dc524'}, - {'sequence_name': 2, 'insdc': False, 'sequence_md5': 'd2b3f22704d944f92a6bc45b6603ea2d'} - ]}, - }, - 'metadata_check': { - 'json_errors': [ - {'property': '/files', 'description': "should have required property 'files'"}, - {'property': '/project/title', 'description': "should have required property 'title'"}, - {'property': '/project/taxId', 'description': "must have required property 'taxId'"}, - {'property': '/project/holdDate', 'description': 'must match format "date"'}, - {'property': '/analysis/0/description', 'description': "should have required property 'description'"}, - {'property': '/analysis/0/referenceGenome', 'description': "should have required property 'referenceGenome'"}, - {'property': '/sample/0/bioSampleAccession', 'description': "should have required property 'bioSampleAccession'"}, - {'property': '/sample/0/bioSampleObject', 'description': "should have required property 'bioSampleObject'"}, - {'property': '/sample/0', 'description': 'should match exactly one schema in oneOf'}, - {'property': '/sample/3/bioSampleObject/name', 'description': "must have required property 'name'"}, - {'property': '/sample/3/bioSampleObject/characteristics/organism', - 'description': "must have required property 'organism'"}, - {'property': '/sample/3/bioSampleObject/characteristics/Organism', - 'description': "must have required property 'Organism'"}, - {'property': '/sample/3/bioSampleObject/characteristics/species', - 'description': "must have required property 'species'"}, - {'property': '/sample/3/bioSampleObject/characteristics/Species', - 'description': "must have required property 'Species'"}, - {'property': '/sample/3/bioSampleObject/characteristics', - 'description': 'must match a schema in anyOf'}, - {'property': '/project/childProjects/1', 'description': 'PRJEBNA does not exist or is private'}, - {'property': '/sample/2/bioSampleObject/characteristics/taxId', - 'description': '1234 is not a valid taxonomy code'}, - {'property': '/sample/analysisAlias', 'description': 'alias1 present in Analysis not in Samples'}, - {'property': '/sample/analysisAlias', - 'description': 'alias_1,alias_2 present in Samples not in Analysis'}, - ], - 'spreadsheet_errors': [ - # NB. Wouldn't normally get conversion error + validation errors together, but it is supported. - {'sheet': '', 'row': '', 'column': '', - 'description': 'Error loading problem.xlsx: Exception()'}, - {'sheet': 'Files', 'row': '', 'column': '', 'description': 'Sheet "Files" is missing'}, - {'sheet': 'Project', 'row': 2, 'column': 'Project Title', - 'description': 'Column "Project Title" is not populated'}, - {'sheet': 'Project', 'row': 2, 'column': 'Tax ID', - 'description': 'Column "Tax ID" is not populated'}, - {'sheet': 'Project', 'row': 2, 'column': 'Hold Date', - 'description': 'must match format "date"'}, - {'sheet': 'Analysis', 'row': 2, 'column': 'Description', - 'description': 'Column "Description" is not populated'}, - {'sheet': 'Analysis', 'row': 2, 'column': 'Reference', - 'description': 'Column "Reference" is not populated'}, - {'sheet': 'Sample', 'row': 3, 'column': 'Sample Accession', - 'description': 'Column "Sample Accession" is not populated'}, - {'sheet': 'Sample', 'row': 6, 'column': 'BioSample Name', - 'description': 'Column "BioSample Name" is not populated'}, - {'sheet': 'Sample', 'row': 6, 'column': 'Scientific Name', - 'description': 'Column "Scientific Name" is not populated'}, - {'sheet': 'Project', 'row': 2, 'column': 'Child Project(s)', - 'description': 'PRJEBNA does not exist or is private'}, - {'sheet': 'Sample', 'row': 5, 'column': 'Tax Id', - 'description': '1234 is not a valid taxonomy code'}, - {'sheet': 'Sample', 'row': '', 'column': 'Analysis Alias', - 'description': 'alias1 present in Analysis not in Samples'}, - {'sheet': 'Sample', 'row': '', 'column': 'Analysis Alias', - 'description': 'alias_1,alias_2 present in Samples not in Analysis'} - ] - } - } - - self.validator._collect_validation_workflow_results() + def run_collect_results(self, validator_to_run): + validator_to_run._collect_validation_workflow_results() # Drop report paths from comparison (test will fail if missing) - del self.validator.results['metadata_check']['json_report_path'] - del self.validator.results['metadata_check']['spreadsheet_report_path'] - del self.validator.results['sample_check']['report_path'] - for file in self.validator.results['vcf_check'].values(): + del validator_to_run.results['metadata_check']['json_report_path'] + if 'spreadsheet_report_path' in validator_to_run.results['metadata_check']: + del validator_to_run.results['metadata_check']['spreadsheet_report_path'] + del validator_to_run.results['sample_check']['report_path'] + for file in validator_to_run.results['vcf_check'].values(): del file['report_path'] - for file in self.validator.results['assembly_check'].values(): + for file in validator_to_run.results['assembly_check'].values(): del file['report_path'] - assert self.validator.results == expected_results - def test__collect_validation_workflow_results_with_metadata_json(self): - expected_results = { - 'shallow_validation': {'requested': False}, - 'vcf_check': { - 'input_passed.vcf': {'valid': True, 'error_list': [], 'error_count': 0, 'warning_count': 0, - 'critical_count': 0, 'critical_list': []} - }, - 'assembly_check': { - 'input_passed.vcf': {'error_list': [], 'mismatch_list': [], 'nb_mismatch': 0, 'nb_error': 0, - 'match': 247, 'total': 247} - }, - 'sample_check': { - 'overall_differences': False, - 'results_per_analysis': { - 'AA': { - 'difference': False, - 'more_metadata_submitted_files': [], - 'more_per_submitted_files_metadata': {}, - 'more_submitted_files_metadata': [] - } - } - }, - 'fasta_check': { - 'input_passed.fa': {'all_insdc': False, 'sequences': [ - {'sequence_name': 1, 'insdc': True, 'sequence_md5': '6681ac2f62509cfc220d78751b8dc524'}, - {'sequence_name': 2, 'insdc': False, 'sequence_md5': 'd2b3f22704d944f92a6bc45b6603ea2d'} - ]}, - }, - 'metadata_check': { - 'json_errors': [ - {'property': '/files', 'description': "should have required property 'files'"}, - {'property': '/project/title', 'description': "should have required property 'title'"}, - {'property': '/project/taxId', 'description': "must have required property 'taxId'"}, - {'property': '/project/holdDate', 'description': 'must match format "date"'}, - {'property': '/analysis/0/description', - 'description': "should have required property 'description'"}, - {'property': '/analysis/0/referenceGenome', - 'description': "should have required property 'referenceGenome'"}, - {'property': '/sample/0/bioSampleAccession', - 'description': "should have required property 'bioSampleAccession'"}, - {'property': '/sample/0/bioSampleObject', - 'description': "should have required property 'bioSampleObject'"}, - {'property': '/sample/0', 'description': 'should match exactly one schema in oneOf'}, - {'property': '/sample/3/bioSampleObject/name', 'description': "must have required property 'name'"}, - {'property': '/sample/3/bioSampleObject/characteristics/organism', - 'description': "must have required property 'organism'"}, - {'property': '/sample/3/bioSampleObject/characteristics/Organism', - 'description': "must have required property 'Organism'"}, - {'property': '/sample/3/bioSampleObject/characteristics/species', - 'description': "must have required property 'species'"}, - {'property': '/sample/3/bioSampleObject/characteristics/Species', - 'description': "must have required property 'Species'"}, - {'property': '/sample/3/bioSampleObject/characteristics', - 'description': 'must match a schema in anyOf'}, - {'property': '/project/childProjects/1', 'description': 'PRJEBNA does not exist or is private'}, - {'property': '/sample/2/bioSampleObject/characteristics/taxId', - 'description': '1234 is not a valid taxonomy code'}, - {'property': '/sample/analysisAlias', 'description': 'alias1 present in Analysis not in Samples'}, - {'property': '/sample/analysisAlias', - 'description': 'alias_1,alias_2 present in Samples not in Analysis'}, - ], - 'spreadsheet_errors': [ - {'sheet': '', 'row': '', 'column': '', - 'description': 'Error loading problem.xlsx: Exception()'} - ] - } - } + self.run_collect_results(self.validator_json) + assert self.validator_json.results == expected_validation_results - self.validator_json._collect_validation_workflow_results() - # Drop report paths from comparison (test will fail if missing) - del self.validator_json.results['metadata_check']['json_report_path'] - del self.validator_json.results['sample_check']['report_path'] - for file in self.validator_json.results['vcf_check'].values(): - del file['report_path'] - for file in self.validator_json.results['assembly_check'].values(): - del file['report_path'] + def test__collect_validation_workflow_results_with_metadata_xlsx(self): + expected_results = deepcopy(expected_validation_results) + expected_results['metadata_check']['spreadsheet_errors'] = [ + # NB. Wouldn't normally get conversion error + validation errors together, but it is supported. + {'sheet': '', 'row': '', 'column': '', + 'description': 'Error loading problem.xlsx: Exception()'}, + {'sheet': 'Files', 'row': '', 'column': '', 'description': 'Sheet "Files" is missing'}, + {'sheet': 'Project', 'row': 2, 'column': 'Project Title', + 'description': 'Column "Project Title" is not populated'}, + {'sheet': 'Project', 'row': 2, 'column': 'Tax ID', + 'description': 'Column "Tax ID" is not populated'}, + {'sheet': 'Project', 'row': 2, 'column': 'Hold Date', + 'description': 'must match format "date"'}, + {'sheet': 'Analysis', 'row': 2, 'column': 'Description', + 'description': 'Column "Description" is not populated'}, + {'sheet': 'Analysis', 'row': 2, 'column': 'Reference', + 'description': 'Column "Reference" is not populated'}, + {'sheet': 'Sample', 'row': 3, 'column': 'Sample Accession', + 'description': 'Column "Sample Accession" is not populated'}, + {'sheet': 'Sample', 'row': 6, 'column': 'BioSample Name', + 'description': 'Column "BioSample Name" is not populated'}, + {'sheet': 'Sample', 'row': 6, 'column': 'Scientific Name', + 'description': 'Column "Scientific Name" is not populated'}, + {'sheet': 'Project', 'row': 2, 'column': 'Child Project(s)', + 'description': 'PRJEBNA does not exist or is private'}, + {'sheet': 'Sample', 'row': 5, 'column': 'Tax Id', + 'description': '1234 is not a valid taxonomy code'}, + {'sheet': 'Sample', 'row': '', 'column': 'Analysis Alias', + 'description': 'alias1 present in Analysis not in Samples'}, + {'sheet': 'Sample', 'row': '', 'column': 'Analysis Alias', + 'description': 'alias_1,alias_2 present in Samples not in Analysis'} + ] - assert self.validator_json.results == expected_results + self.run_collect_results(self.validator) + assert self.validator.results == expected_results def test_create_report(self): self.validator._collect_validation_workflow_results()