From ff07496e5aedb456bddf5c98c722d752f5284c39 Mon Sep 17 00:00:00 2001 From: gordonblackadder <171737385+gblackadder@users.noreply.github.com> Date: Fri, 6 Dec 2024 22:22:37 +0100 Subject: [PATCH] pydantic v2 (#9) * output the models in pydantic v2 * add type and template info to models * move json->python config to yaml * test yaml file * update excel read and write to use versions * shade metadata page even if no metadata --- README.md | 10 +- excel_sheets/Document_metadata.xlsx | Bin 32547 -> 32554 bytes excel_sheets/Geospatial_metadata.xlsx | Bin 52553 -> 52564 bytes excel_sheets/Image_metadata.xlsx | Bin 33154 -> 33167 bytes excel_sheets/Indicator_metadata.xlsx | Bin 51226 -> 51208 bytes excel_sheets/Indicators_db_metadata.xlsx | Bin 15735 -> 15742 bytes excel_sheets/Microdata_metadata.xlsx | Bin 49097 -> 49108 bytes excel_sheets/Resource_metadata.xlsx | Bin 8433 -> 8443 bytes excel_sheets/Script_metadata.xlsx | Bin 34124 -> 34133 bytes excel_sheets/Table_metadata.xlsx | Bin 32726 -> 32740 bytes excel_sheets/Video_metadata.xlsx | Bin 15705 -> 15715 bytes json_to_python_config.yaml | 59 ++++++ pydantic_schemas/document_schema.py | 17 +- .../generators/generate_excel_files.py | 4 +- .../generators/generate_pydantic_schemas.py | 49 +++-- pydantic_schemas/geospatial_schema.py | 13 +- pydantic_schemas/image_schema.py | 184 +++++++++--------- pydantic_schemas/indicator_schema.py | 11 +- pydantic_schemas/indicators_db_schema.py | 17 +- pydantic_schemas/metadata_manager.py | 124 +++++++----- pydantic_schemas/microdata_schema.py | 17 +- pydantic_schemas/resource_schema.py | 3 + pydantic_schemas/script_schema.py | 17 +- pydantic_schemas/table_schema.py | 17 +- pydantic_schemas/tests/test_generators.py | 71 +++++++ .../tests/test_metadata_manager.py | 17 +- .../tests/test_pydantic_to_excel.py | 132 ++++++++++--- pydantic_schemas/utils/pydantic_to_excel.py | 79 ++++++-- pydantic_schemas/utils/schema_base_model.py | 29 ++- pydantic_schemas/video_schema.py | 11 +- 30 files changed, 626 insertions(+), 255 deletions(-) create mode 100644 json_to_python_config.yaml create mode 100644 pydantic_schemas/tests/test_generators.py diff --git a/README.md b/README.md index efdf8e7..0189f2c 100644 --- a/README.md +++ b/README.md @@ -97,19 +97,21 @@ microdata_metadata.study_desc.title_statement.idno = "project_idno" ## Updating Schemas -First create a branch from the main branch. +First create a branch from the main branch. Branch names should follow the pattern 'schema/\/\'. Then make the change you want to the json schema in the schemas folder. -Then in pyproject.toml update the version number, changing either the major, minor or patch number as appropriate. +Then in pyproject.toml update the version number, changing either the major, minor or patch number as appropriate given the conventions below. + +After, update the version number of the **specific schema you updated** in the json_to_python_config.yaml file to match the version number in pyproject.toml. Next update the pydantic schemas so that they match the latest json schemas by running - `python pydantic_schemas/generators/generate_pydantic_schemas.py` + python pydantic_schemas/generators/generate_pydantic_schemas.py Finally update the Excel sheets by running - `python -m pydantic_schemas.generators.generate_excel_files` + python -m pydantic_schemas.generators.generate_excel_files ## Versioning conventions for schemas diff --git a/excel_sheets/Document_metadata.xlsx b/excel_sheets/Document_metadata.xlsx index 2020cc3de0f06e5452fec42491ab209910ca5bb9..fc3b5bb4a9778ea60d1f7c4b34bcd32b83b6b13e 100644 GIT binary patch delta 2120 zcmZuyYgAKL7QVbG5CR5#02)Cg$RojJK~PMnPIX2QaF``!iA1p=$Rilu+|1kwf*@qB0x7vDRY_)H;B?Rmla<Ba8^zHXbIt1{22IemfnTj@h`wLRzJBbW&#gDZ zuQg)Wx#Y=m$4aTv<7?lg1O}|#yOlk1AS9kh~%(MLS5yUe!qW?5`oV}@) z=~X9g%@FXGp2{ZLdL(dj_bZ|SZkXf&N-Y}?$mFKmj5#iR#>HIJ83D>t|YijAwbps?C zEl-z6|`pTBnqa{U#kT0z7D3w zF__NcR%;KYO1h_nTFvNKZ}*IFxt$)Le@@$#UN3B9_hVl1r}dLu9#(Hf7IoB0_bJ-) zRPWw);a+{)q8HV`E1J;)q|72T5qVd(Ov2u6J%7lre#_mrqCw>HUPju~cgLA@Q5yX3 zydTh{P^q%KAD!4YIa)fK!rzCeBkoH-iY@&i7yEosp`63sW)~h2w39-+v^|QsdxzTF zX+@t6BRgcbZx1YzC-3?)$PZ#D_4UeT#l!(sX5PxXYsfCwPVxv_dd_7wIhN2wjz2;# zawc|wp|if&4^JMo_6o``433)^4jL{{>R#=bz{XX6{x{lSq)ub|y7 z%VVMQ-MlI^NsK-{tG8sje~*2 z@YvSd>7pg>UhvJIetIZ7xJ@K;M1@rp4)M3u;2C}U)pcmwm?wITJlHg zwBtQ(@``5N;@^034OhSPw%?b{=OVPFGGSATHaVZ}EYM%l(nd&CFqkyrrDM=#oKCu!hA##XF%y6z;_D&{AE`)U@ zO$jgExFY$JSvPp*DZyb$xw58%^0qkjq09`Olc#iF3NZ#V$C-A;w?Zq!&ouk7{tz`7~v-jN24vPzpPv&m_g8t zBLty-r$fai41h4)j2<>KU15nqpG-|k&nDnkzyEI|KYIE?u3=n%Y>+~)k$I30K?UiA zd@K+W{B6w+2x{{y!q B*fanD delta 2045 zcmZWqX;_n27XA_pVTTk13rfURkYN`Df$#+#g@-C)wU$9ZtP09v!V(gYBp=vn9#I>Z z>mvvVDkF?CiVgy@gjmssahO0N4@8pyp|`faWJ~`H#;kr zqN37uW42r0ek-7F^GQ+2BDx0(Mw(8lU}l=zA?$6&i>YaYuoNE;Ls83@ls`3ud~F3q z$OQMlNqRn!F0MXutttb%YsRwikAJP3wRivX_ClL0&$s+5jQWjzh+F6B_s#`^{7pwG z*YZm?_PBqCJee83=z8OoQ=@Ocf-&gj;z@onDw&y2Jlb(4e#?a4-^1)ZD0}w8{^NT` zy(ynDZ6-37Xei#6C@YH5)1=yW*|{`s?(xX@{<`zgdl`+R0Vj?aGnH^FQ(kpppqwf? zcAQ{rZ;WUjte}6_hnO326<=5xLJe~$e?tphM_w+B5ILq0r&C+BFrR2sVd;aFSkV6j7hr$mzGzNVruh{=y@%YpY`emoaKM0g~ ziBzOX?W05^A*3dyr7)pQI@4QTuI?00X0#yv@D9=UbP|o-EuEW@cJnmJLigxeYzT=o z+1yMfIhKee@-9WKtrSK&$W?8;bD|&G>eP$l`hU>NmGcad5x(s^7Q&()g8~b6^>b}W%MD{n*=&NTa^JfN}9)A z24*|ulKCS$$|r9J!><g1)Zj4B(aK53~|%UKm5^Y7W>I@T0J2zL-x=dR*~ zB29fam#NN-gHNE?rCh=ZO>ek6X%u(4B;0)!--Kk;;v?g!-E0|6!mb;d@3xGhsYX7S zQew_6kGdQsvk@X7(fIPwjG^#GMh>jv%{*+ciG66%JEy4$CP;8($77Xa9Z2EIjBAtH zErfbkW|}kGUtcR_!aJhbUcHE%GkCKmU74~pOC4*wH#g)5qRd>KoNjRncY8l>Dev4B zR8_OG;>U|hSYEB_O((LxK*wKrxC`)2T?0@a2hL<}f7~MYhuu`MmuDEP>VU6s6v4|3 zg!6NgHMicagdL>u8PC%q$GHdg^pcsuIMbatU}fgw|HDXSsMA4&CAgHsfN}NO@jdMt zq4ndq2LoNi7SDjgQ)9z;<%5+#IqNS%yV}GtOmcl~6ad9$F7A7cq{cRAIc>wuvbM#0 zElT#`KBZ__!>1_fUB;A|i@)r@d8tji)0&pYldYN$b>L6O-uTfOG_OT4RLnkB+Qvv=mS31xjN@G)`4?f%~SXa)b>sS{b% z#oS=FY7;nV`y55U?fUibzXKtMp=iIMh9pDO{wcsDR? zX^!b09z5Rz`1bCg2WO5Ia#)t5W)SdkbOUqd76!p>)xbPG2$LweLP`aXlH9?4i#5(o zL82!pKIx|8V4^v`tzr;r5r}cl35)&-pc35y|EviXSxJ5BQrQP?;|L(dDj0JaF)fvX z`XmpXhQWU#nE%aZ4ia#|MxSqxUYepr?O!WA2wm}ij5V!5tX5&o${WJ~nI!>OlXKVE zX?>DG5t(4PJm*w_f&h7~{vwlz4F<#w$t9hck-BIN=+79)}9wBWC(o5hmMp!~}wZl6A3$(las>J-IL|+Cb-kchpHez@t^6^Z+?mtp~{GVo?Tb zV;Au9f)w3UR^+UK;vxm zkO2d?Syz6IfdAvmKnNNwK!3u0=cIoa!T6OM&u~Bw$Lb+A1BcgfKYs7iyvG5T;i6AD z!*C!_vmhuZ%`1zNo0gd6nR7Pn;64=E#^Bebs}pJt7Z*NTtJ)R~L2E1KH_J!yK`7H+ zFBH#oK(&BerlbB=#&kf(<^pE!Mz>GJk}8F>ZYT&+Vj#%+Hz^3pN5ujS)7h{&PY3=R Dh5)mR diff --git a/excel_sheets/Geospatial_metadata.xlsx b/excel_sheets/Geospatial_metadata.xlsx index 77d68d890cbeb27ec95a6c7139c7bbfa703dffe0..352ffe4201ea4be0090f530544c72ceacacc8f50 100644 GIT binary patch delta 2098 zcmZuydsvcb7XJVtHPJ{N-89LZRm&8!(!79ewKuug=m7~wFaI->vqK{%cKh8lU`G&LcS5J^29<(3vl`RU5D z7@oVyg^0T&MJ*`-W{&N5OG-^KE);1rrD_{JGBWn_vnub9f)q;b+Q-@+C!=1UFJum+ z0KEHEex-Go==b+|b=z@hqaWss3ilmyth2StO!|Iii=DkQtiE;G@Laj&_Rpg%HdvpJ z%FE&Z@#Neilj8D1gFUF~w@*q8>W2~{jY{^zo`J1_rmYCeUoGh0?dkb43I zH1^gY*boP&8H-k9b|MaOGq_@E@K?{{cYJq8@kN|rs{hqje;0NUys!7kBU?RYU3bPD zx}7Q($$AHK?~S`@LdrbZ^SKdqlhZPxa8k3sB3v3?$!L!z6u*+4>6ZBq(i&b7;|ut_ z=|4;eh0?`Eq3}%kKo~78l_F8_*oyn1u3kYk9Jr78xR)+tu%m@E7*0=AZfO#9M$|>t zEKW_Hny%fO@0Ky(e^fgz98b5qHW6JBT3tYon;lo*9jLcV*1TKD8}1zujHxE2!*m{F zwmgn&M=G~_9TAQXKU5Jj$>xDg3Rw9s=k}Q7G^RRMDW2^KHx(9wTkWvIc5DlBZW<4C7h6xSx5q=`LL{&=eWftQ( z+Dy*C986xCoy{GV;=B^mnxslxMFf5`_d-BhF@Ccg?$hws!dsKOT}kz#YB8ZYn;`Zd zlzcZ#=$e+Oici#fFKMrbB#Ag~^Nb0Sbr3rvEJ?tB_dK?a&(nCnCMT<$NK-=fEEsqn z?BPply$3(eX3m{84Q`;({=sGA8SNr^XQ}*sF&(E?Kkm01n^p`h^4sYX7p-nSU)cqW zOg7;En&r63_*v5!Q&BpR)D&In;OR?~2qKCl@t9IfoiSk5xNJurxzRQ9&9TIiE1kg( z#l<<93y*2m8FJPX>)a^sUg5%7vN!xv(rG7NZx^Gp2I@S?(N7p?AaZjge&B@<^C z`x=J>hA+imC&k1EmyRhSz6_F=!Kz(7GJJ3oi~sKabsuw<_OxTXBv9YT_LoM-hlJyS z_2=NMx2ghh2QulsHW!(vad(FEWv5xO;+bIPLMM<%!cC<+a@{TA!RH!hd}hAN#1xYEt-UDE)FEUjN>aj#LHGbR#0`X5>0JR>A4$0WX zssagSUMt)OMqhVDx&2ZMi#g2kO6uJY?C~z3aWxhlG=(LjLEO_#x-?@$?P!^#C~eO? zxU$X_tZOhe=zLq02mEz7+_5GG9U9l8K*IGyeH4L|`vdm|BH|M0TJ47{>IRZkeuyof zMtxkz*l8;pkYbb{oBvDa?Jy>{rN-r-~_`iK0Z2T2lHx_L(Ep#hE8Cf}9w> zc$`RJ2&i$nZX{;DMx|hQb3!k@aUyaQn%N>V|9xy(hj?#iD8*mSZRS5vR?2$`;c_^j zUqKD*h5uQ_XfD(CrI6Vq+QQusKU><(2`g6i8!EfUDLX>dQ(!<_@c-?}#9MS>QM!{^ zF=M~}Z+gY*0nJ3O; z`wLHf*vS_*h}Yo!i}mY1InI!^j>3L@S>NIf0b;$S?pmYG4(vxDEl?lm4@|wVL9Vq} zc5n7rrnyi2A;iR@r}AtFvMxh^Qgq5*m$e4O3G`EeOo0v3@`1h&Q-IS0`T?E1;ors( zk2O=IlgP=4DAu%bP5|#M>@Ua3W zzHii}e1?P|9Wi})SqKat9BuoZeovWgHV8S$YqQgT)wbCnFExY0X50VS2BO_Y_v5c^ Lcf`DHT`u)Aq65?@ delta 2057 zcmZ8iYgE%m9{oeYLm<2YS_~T0N<dw+LkKHT54V%Vc% z7~-fu4DJ9ykU3IW~ZM*%G&mtztw|$BM7ns$9f#-cdoSucI>jc9C@avHLVzN z=@<8PwiN-sr|k1XuV;iF1bzj_IE@w9P_v7uu4SEi@Y{39JF!UO9gkC+85jCguF_vH(k5kqCi}W4-H)&c{)L&!n3(wzWSMT6RI}xZ8@7Ut~LHu>@(NEf4j@RR#eE#g2 zD?Y*@DEoLv=7bODGuOZDI~2z9pS#~ef8o7nYg4eJBd$|W%?3N%P3Wj_lCs8k z*XOyHYY9QHQ7rf%xlhQ1$y3N=lDRD{7;QR@QE+-`=M}&&WjDxTmRP zdct#&hViy0T(K}I5NOBZSRt~=`XIrQOPNRMvd5Qm`2mpsh@lvaKn4U z#>C}?3HAQMdjTwQ&CpDL^@@7zWDNZu?2MGm-l@2`6Z5C(a(TbJYvozP$PZ<~h3INZ zVNF$tr;S*C_-6)>M!6o-N+b@9)vI;V*ct{Ui}p&|#bZX0G#1YEh|vlu_t;QcZRRvL zrzwY;z4(F_L9F34X`(!Eq<-<#8uZYjc7aDGDRE3Rx36QK8|R5DB+|Gc2`ZkuPPxcW zlIIT^Z`@74(5Wsf3?6>4Wi75Sc!3nJlLV5A+0#FxM`nl6WX|{vG*h_yC#Fm4?20S4_fk`jfMzc=4S4o%1Y(-GM}}OpHkn%T$=6GG?@h_xvXfgb+}j{5Ib8xT{$Qa zV$S6#9}MVdmsMXn+XGvxonIa@|1QZFfC9l)Kdx6HEot}M77P}N%&`A=iB*$3=JA1S>!SErQ_po zmMaFNnQy|>s?$6?sY3hXf>KgueEFati$uFeFuQ2g`3lNiN05m#id8$&wnvToxH~T`?asg z<@X-rUjw4$HmCKak0qB}F~}0sRdwNfw=VbNQ-_aEY3YPvAb!xh2HdjuFdLOrXZW3k zfL)81sYvV;K5PGz4c=7PmbgL)1qcMegsphDsTAC*!-A$3D-(ChS!ycitHFXYTZGAf z{?(U&1+}(brqt%W?B1=w7U5-5MfIb$f?ceg;BPzx94>sl9VGEQ&2V&tqBDgJ@@lbQ zrq#wcPyui(te#^DPn1)Pz$W{Bz!6CRZ3wstbNM4rn5i_tyv(A>uowJA-M*;FKrD&6EA^66c)5`5FnjPrJtsoo_BxAoR)fz4auaf!%Ave ziiklGjLSEOm%2Q&vMLADHNqU)!jzzQC`d=39f~B2vRMRfl1O_{Q!yc|h-r*S;4Mb7 zXtI;p^|In{-BOy3AiX)X-g}91rK0Zbq@V#;$XNFnf8!4QIe)HB`7(WK?kRhUN73D! z594QCcrqV1OPXC-zFo(W&0U>uk;?dESz-Sz5i)XAZ)V1**TZ$hR1GFvm8w(*B@EP; zJY__dNXO-7?CdluP1``AC#&m8Jj`ep@6&i>ZFhK#fa!ygrP;JO|Br;^-8|!O1wnD? zs|m^St2$|AAlz<1S{fG3?PS>M0w5nw7y$LdVj}}k)TJ>1Mctu)0E$P1H6I!hAoO^k zS9gX;Hie*43@l zmTzZ(4)9UN(oF^^g>UDw>c;!>t!~dNM1dJpp1ANm69BH zib9EV&q#ThK10Q=CtLyUb74R`BBU%?@0{U_o)Fxu;8@Us%2jPk%Xfvc(b(JJMC D8VS+6 diff --git a/excel_sheets/Image_metadata.xlsx b/excel_sheets/Image_metadata.xlsx index 99809cc2e8c24cbc18442c747bf628b2bbc8319d..a9c67e3435b6c743084411c8154e45dc4c245fa9 100644 GIT binary patch delta 2091 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 IC_CW)0sooKMF0Q* diff --git a/excel_sheets/Indicator_metadata.xlsx b/excel_sheets/Indicator_metadata.xlsx index 1e8011d97b55c7e15af5378f4c44d9c816feb730..db906d52e88707a6a7e08767ee6e40c9c122e617 100644 GIT binary patch delta 16296 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 delta 16466 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 delta 6227 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 U|A*i65_G`N%}jc;i+JUK0NQrjAOHXW diff --git a/excel_sheets/Microdata_metadata.xlsx b/excel_sheets/Microdata_metadata.xlsx index b6d47d551eb21f8ff68810ceaff971c9f8a0e51e..d562faaca3f37dd909e47677edaaf06184b2e595 100644 GIT binary patch delta 2462 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 delta 2372 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 Vf4UX5f1T_>h>JDXC^l6({SQVZYJmU% diff --git a/excel_sheets/Script_metadata.xlsx b/excel_sheets/Script_metadata.xlsx index 2b725d51a9117ad328e2c5907367c2b7aeae13c4..ae31c90823e31b0342a30fa55b60433cb58ca8e0 100644 GIT binary patch delta 2232 zcmaJ@dsI^C7C+)M#n;t9Q^T5+Ynte&7^YIqy0^)u*IKv8%(3!G5}4#8U&m)op+ovj z>J3FlOMA@Zn3*XFm1A5}G%Irykj&Ilfy&Sj(+hNKO|4n?tZ$vY_wW1d-?z{Hf>vj#Oz>jWB@X@?i@b7*?q_H|C zm5*L_ysUR*FU|ZaHjJ($2KFqHi)#fdn+++IY=<5?5soQgDUY1EM&)Mson@F;?~uru zgFNR--(FS?AIo9RL^jQHQxA(_zW_<-p2is2ka%m95R>5@)VOcDr>U)EsjYnWIt&ZN zMSLb)@x27rzkz-id4v2@e|4FpSrS-5#f@{7eyRJfzn&bVd>`1~znGhJr6X#o7fi@H z(7tupq@Aop#jE(YvxS)PE*XE2$K%@bw;Z@@2}-|m=3zVo^1rIZZTXI4E({ed)?8{# zoK=pdDAKMCx22`FigVthW8in8vsQvq4qUmFGd!7=dO$^J3F*ZMt?J3qbHY&~yGaOS zFQ#QnGsOR#0`3`5OGaiV|npS_Vb+|1RiOb}<|hTXVo zS6KT&x2tC1#}xSuH&f_tdxa7!|N4oeUU(FV#_U*LpkDfbhjj5-t=Ft)oHf6~ApBUm z4P%N}(4k<3dvbV{^2Jd;#S@RFVN`8O!feRYR1Mj-#Zj7)cJ4T!M7XY@F0)1`e2}%{ z;~qY~aM`&YS_W)5HZC~z&NIZnFg<5CZP5d`A-z|`+I0V}YX9yQ@!Y8zFHxdWMQVgv z<}(&j&w<-E>ri*rWo`C&*s`f}$?vBH+}{b$ji0Z5z5+Bvx>|3}v-;G#u=2UC7Wzd3 zaJ2XuX&XBlXYeEHG1^-f0!5ZzgM+K|^?f_hR_6el;=Sz2y%FSF#yGe7WFMn_cKKcp<2*RxZy9dwF$=R1eg@g!?GWKpap$- zh|4G4xVLDMrV{;Rl|>W;UO5sF$^VD0E~D;GWoY#Aa04r=u_iT~D>`$$pDX(II_JVg zPOEBIB&T9q2gT&KPJ$AKWJcVl+CaX_T}V=$cCWzs3R02=9tha2b>qGKsJ1d3FRA|D z0Zp#y+!$6ulKe3ziV)IEa!8zu*t3^WmmgUwqT#BJ{s?5h@HFGq$r6Atpg`$xnpB~AUTUzY~C{LX4+tDsjRrofRcGHUiv0GY&84Zy)o(#bylJu&XBob$)eyAOY2NvVL8R}g!{QUO5XMN99SABaoGFKN8wGQTR22$%(QLRk0 z&W#f!U5x`Q(#N%cGX1QM7AS(>Yk{K7kPm=7Ys0cn;}UpfQjLf)6eyTCjLtCc3?*dZ9JwP~?AmRbl_ zLw1KXkcYQsGVj`^gRs{9G|_|4=qI>5HHIKR@|UvZVB~+YCb@RO*G@oCG6hFJeu5H7 z|0|*9EmNm&tE)#LAjst7SEy~K4l*5r!r~(-bey~9{Y3=;&b8A~09NiwZN0a0?GS}9 ksDkZXzaR!ju?Hqy)qfl+eF(Dp?1Vz#nCEQpGD`#g7e6ivR{#J2 delta 2106 zcmY*ad03L!9{#{R;lU-B(vcRIrc5+LGZ!qcGf%0hPq)%cDNRjK$tB6T3GP;Q)A~|N zO}d&o<-%>Kv~U@7A+HOQjv6X5YA&gWxZr}w1#HIgJm-1N@4VmpzUO@BkKfyK7utRo z3ioi4livvdfHI(Z!}|gJSF&XBMov1v=1Rae2mq){)~kd7k4VDxC{zF;GSKX1SZLOR zhBJ-kuvBc||7vf5@Z>@>9W|58%*XO0+D1d#6(L1UKeGqG>;%11(nQpgW5BJ#{K%?^hxws@ZgEx$m7q5%7rh(8}^5kooYN_ z`XThHNimvsPfItr?@w}%l=Cv=_k;>HGFisu|%j~0L!jjzfuTJZP zQLy0Kl~Bj3Tm*`{CLH(ajy4fHlp9zh-Rp0#yUCrMRzyy2TJyNC@OQiLF&8??D|&!F zO%2W~y+NzyAQ%68Jntz5OR+c2&M2ig%+RijzB&oXgB95sZq*JfJB*!aFNTnJPB_nB z99g`ehN>q`3mRCZ2={cVTURuzTM*A>(;8!Ea}XV)K`e)Y@;nsMeQX3%*va8#urS70 zD^y<5fC;^7;aylgZ`NxW>CPllUwWg7>{-Kft~+LTv3EtxM1NhjxVAtLeY2nafKf#z zKlS5~<=>*_7W}&pa9?^25jhS8;n?=Lg+PAI)QF%t)Q{8Cvyn{X;%H+2mtyi6bC&z;B#?C649us0apMJ?DDU%+hTZ@5grL@a@%U^@WWN0V-VF`@3`1%HkDv zU4A(c!6^tQ28bd3$hx>6+Zb$5YH(LY)%!t%zPQt~y=|qlP5AfCBEbqGm@Rm2dz}6+ z`-U)bB5OlFLo_`&cACN*ZpzzeVlzv}zYH(Um|$|3>J%qt(4b?7xZ`hg73rRENfCYS))Zn4sLz2{vUiV5@FZ*;0gu9t?()&?Xc9KM)?jk)6j=@mR)y9y;N2Q&4G$*3ELA?e^K2E{ zJX%MBwJ=*1>EmGY5%g*h0!Qz7f*ZZ5cwg{I=P2livyr!I)v*NQ;AmJ+3m*ncA5XXG z0OG;D-&r7J#MG?$S)Rff<|YQ;#6Ob`umgb>}sB2oTpoQKMm6HBr8)-p}1vf!4(h7VUssz~u{znZh<6U1w-4Ah>@xWP} zvx305ee*8hAv_6ujjL7>mqmQmWNvYkK`Nf8bZjl7YztF@*3^Q3=%XbT($BAzE^kT5 zCHoJ-v^^_IxV6S8}ptm&%hbtSCPspVV5#f=Y7xD(=V32h@WPv#oxu?@Obh7ev6b(G*w}+hm9dw~bHXx2MqXJlOY3u=O_m3A zenT@(k$XqKgsc{EyP7A?bz=zRcAw$Enu5i-excBwH*}gldmq&`b+{K>#f+j$@3xx0RGVgl{EdW5ucC<`X!mj{Oju<2f9dt0WS-NzjHMF zWoGY_HvK!3^q^q46ndDvX|HrjoPv@AfEh{d!nc$_Jmdvvo~$psm3y)tL=MC!@4h7c z8N{P2AMui)wveb0+Y~c#0B|!DMI=Px{RsrTG`KxnAkdw1e+9lb^rZ9PM~TQu9sqW4 b6DNbi$=}P4gQw_0lELp&j1~V#kW&5+yA91$ diff --git a/excel_sheets/Table_metadata.xlsx b/excel_sheets/Table_metadata.xlsx index 16cc3fc6a170b4fe420dce5360ecd7d11db0d3ff..3fbb790f2932e91f26a7db246a4f856d4679e628 100644 GIT binary patch delta 2326 zcmai0X;4#H8hwClhKLA)NF=nhU^PJl5|A~Bc3ZfB-LxW#paKbdSTrD69%3UpAT(Yj zv}_6u*tVh=5ynUYBH%QFER7ms$S4A`B}Rk@A!L$JZq!W8t5hsr&1=LA`gF)jgC@`UW{jbo0c*(@m)5+xEsZSegr_bClU)-r1qxh$=IpqnA&&8Yv}nk zyvi}S!!c)!K<41z9Eo>y#&bL<`R$C#61?$uPVy}vPrXfeKYb28)Fy1@ayV>M>-NA027qB{#X;M-XINHj+`fyY&pY&DzU&Vs zd3t6bWr}*czn)6D$3G=A35P#S8wRv9Sa69qtACVA36we)(E=Q-gL*oIwHdHmRPFGK ze{uP3;#$5K$i!_z`=oz%5BhBuHMN<_V_^?Rv`-(OH#Z#m>%6QiK(s^r^7S0QnKhX! z+4r&@2Sh=(-(0OPU7mGJ813qp(Rc}^6?GeaIVd956*V+?R(VxzsoDxCi0|Az&lz4T z%sLjk=iSJa*F9zDCHOW;m}qV(8*#m$wU^Le^%OrWvBYz1TDObngW@u2@pP2E3!9EJ z^~=`-;vkzfvARa;Q#d`Vfp=I_2ArSO=43q}9%(zumWA=&02IP~ZB}#d5lq!Q*4Uqv z-5e>UqzzBqk7;RnH9ApUm!^DE0T*Fsdl*xt9}+LisIH?YVh9q+Bp;s5w{01EH4(yb z!ne|+Xfy{~OOxLZ<@kbS0HbHOw(;*5sEVN>eZQWWc9H1vi~G(7ctU1nH3>V~jwZa) zBB+Dh!#3!H>gk>|CXw+E0{ZCJT(kIoM+PU~la zGtSF3*JT8r3``F^1-1m+7+sC?`gBK`lXP6tGMqJtPp|>lP;Ep=4637{D`hr~0D&v| z_Di#B;x}u^tY(gS0d|}%IE>U@+S0eV@Fcy9YF7^8kq&xFh9~Ezei5L6@WmsUG2lV1+tSQmE! zy9C?ixBiiK$Ou!m;IcT`3pAi{I!X>ob~XeoH^(6q1mb0Zp>MeFF3=EX^Ti2#@6&WX z_SIeJ>+5y}c!oOYnnn%8Y0bKee*vj0a5~q|HJQGVJT&}YdS8>5D#&j|k>iqweqRwH zJ{$N%DF56od;V3VvR-*YrC}2K0l0~D_~ifp&=qDcd}&tI8?JUFZ;B%yBR*#Ea;y3n zyfb7cqvQTbf&wYOeVY?Yn#KN|n;@pJT~lCmdoM#C8{AbsIam=O5}MxQ`bqwtn|XfS zWO7g<#4#($dBa#?MP9H1D*>cNa}BSZB-S0sVgU21dP=`oSa$vCgxLLHJ$JTbf>MaNbdxtX$8VqZ&l?jOU*!YxR2?j1 z?B!LNPwR7VOetU61x~7Qm0xprcB2y z6|-*bh_fmnIGp^jor-{nQUwI_QQ!eR4F!R@F2WF@+PEI`*HCW2#iSt>Ku^A;rUJ}T zWGcXna&Q4SL3PztHd942sHZxjm0!OF7FsUXf}lcc2r^Uz^~WbwtsktQt+i0re2ysy zTJ^C&v8J9x>wr$9l^}v(b5C z%L7%4uJi>(Uj<|pp@gtF<=C)TR@+;izEJ~$#uY;b@=I%KZlIcuQptAG&D8S1IDPd( z6W9z7Cqqzj^u{DoLexku+0x+!l6(AcnQ$vAk Lu$5LaRay5R3^pz{ delta 2056 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 CdJ^jZ delta 5265 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 diff --git a/json_to_python_config.yaml b/json_to_python_config.yaml new file mode 100644 index 0000000..b1740b8 --- /dev/null +++ b/json_to_python_config.yaml @@ -0,0 +1,59 @@ +document: + version: 0.1.0 + json_file: document-schema.json + python_file: document_schema.py + model_name: ScriptSchemaDraft + +geospatial: + version: 0.1.0 + json_file: geospatial-schema.json + python_file: geospatial_schema.py + model_name: GeospatialSchema + +image: + version: 0.1.0 + json_file: image-schema.json + python_file: image_schema.py + model_name: ImageDataTypeSchema + +microdata: + version: 0.1.0 + json_file: microdata-schema.json + python_file: microdata_schema.py + model_name: DdiSchema + +resource: + version: 0.1.0 + json_file: resource-schema.json + python_file: resource_schema.py + model_name: Model + +script: + version: 0.1.0 + json_file: script-schema.json + python_file: script_schema.py + model_name: ResearchProjectSchemaDraft + +table: + version: 0.1.0 + json_file: table-schema.json + python_file: table_schema.py + model_name: Model + +indicators_db: + version: 0.1.0 + json_file: timeseries-db-schema.json + python_file: indicators_db_schema.py + model_name: TimeseriesDatabaseSchema + +indicator: + version: 0.1.0 + json_file: timeseries-schema.json + python_file: indicator_schema.py + model_name: TimeseriesSchema + +video: + version: 0.1.0 + json_file: video-schema.json + python_file: video_schema.py + model_name: Model \ No newline at end of file diff --git a/pydantic_schemas/document_schema.py b/pydantic_schemas/document_schema.py index e21163f..a1f5e55 100644 --- a/pydantic_schemas/document_schema.py +++ b/pydantic_schemas/document_schema.py @@ -6,7 +6,7 @@ from enum import Enum from typing import Any, Dict, List, Optional -from pydantic import Extra, Field +from pydantic import ConfigDict, Field from .utils.schema_base_model import SchemaBaseModel @@ -23,9 +23,9 @@ class MetadataInformation(SchemaBaseModel): Document description """ - class Config: - extra = Extra.forbid - + model_config = ConfigDict( + extra="forbid", + ) title: Optional[str] = Field(None, description="Document title", title="Document title") idno: Optional[str] = Field(None, title="Unique ID number for the document") producers: Optional[List[Producer]] = Field(None, description="List of producers", title="Producers") @@ -299,9 +299,9 @@ class DocumentDescription(SchemaBaseModel): Document Description """ - class Config: - extra = Extra.forbid - + model_config = ConfigDict( + extra="forbid", + ) title_statement: TitleStatement = Field(..., description="Study title") authors: Optional[List[Author]] = Field(None, description="Authors", title="Authors") editors: Optional[List[Editor]] = Field(None, description="Editors", title="Editors") @@ -540,6 +540,9 @@ class ScriptSchemaDraft(SchemaBaseModel): Schema for Document data type """ + __metadata_type__ = "document" + __metadata_type_version__ = "0.1.0" + idno: Optional[str] = Field(None, description="Project unique identifier", title="Project unique identifier") metadata_information: Optional[MetadataInformation] = Field( None, description="Document description", title="Document metadata information" diff --git a/pydantic_schemas/generators/generate_excel_files.py b/pydantic_schemas/generators/generate_excel_files.py index a43725a..59fa9a3 100644 --- a/pydantic_schemas/generators/generate_excel_files.py +++ b/pydantic_schemas/generators/generate_excel_files.py @@ -30,8 +30,8 @@ def compare_excel_files(file1, file2): for row in ws1.iter_rows(): for cell in row: cell_address = cell.coordinate - if sheet_name == "metadata" and cell_address == "C1": - continue # Skip comparison for cell C1 in 'metadata' sheet which only contains the versioning number + # if sheet_name == "metadata" and cell_address == "C1": + # continue # Skip comparison for cell C1 in 'metadata' sheet which only contains the versioning number differences = [] if ws1[cell_address].value != ws2[cell_address].value: diff --git a/pydantic_schemas/generators/generate_pydantic_schemas.py b/pydantic_schemas/generators/generate_pydantic_schemas.py index a04344c..4e9fdc5 100644 --- a/pydantic_schemas/generators/generate_pydantic_schemas.py +++ b/pydantic_schemas/generators/generate_pydantic_schemas.py @@ -1,32 +1,33 @@ +# import importlib.metadata import os +import re from subprocess import run +import yaml + SCHEMA_DIR = "schemas" OUTPUT_DIR = os.path.join("pydantic_schemas") PYTHON_VERSION = "3.11" BASE_CLASS = ".utils.schema_base_model.SchemaBaseModel" - -INPUTS_TO_OUTPUTS = { - "document-schema.json": "document_schema.py", - "geospatial-schema.json": "geospatial_schema.py", - "image-schema.json": "image_schema.py", - "microdata-schema.json": "microdata_schema.py", - "resource-schema.json": "resource_schema.py", - "script-schema.json": "script_schema.py", - "table-schema.json": "table_schema.py", - "timeseries-db-schema.json": "indicators_db_schema.py", - "timeseries-schema.json": "indicator_schema.py", - "video-schema.json": "video_schema.py", -} +# __version__ = importlib.metadata.version("metadataschemas") if not os.path.exists(OUTPUT_DIR): os.makedirs(OUTPUT_DIR) -for input_file, output_file in INPUTS_TO_OUTPUTS.items(): - print(f"Generating pydantic schema for {input_file}") - input_path = os.path.join(SCHEMA_DIR, input_file) - output_path = os.path.join(OUTPUT_DIR, output_file).replace("-", "_") +with open("json_to_python_config.yaml", "r") as file: + data = yaml.safe_load(file) + +# for json_file, (python_file, metadata_type, schema_class_name) in INPUTS_TO_OUTPUTS.items(): +for section, details in data.items(): + json_file = details["json_file"] + python_file = details["python_file"] + model_name = details["model_name"] + version = details["version"] + + print(f"Generating pydantic schema for {json_file}") + input_path = os.path.join(SCHEMA_DIR, json_file) + output_path = os.path.join(OUTPUT_DIR, python_file).replace("-", "_") run( [ "datamodel-codegen", @@ -44,7 +45,21 @@ "--disable-timestamp", "--base-class", BASE_CLASS, + "--output-model-type", + "pydantic_v2.BaseModel", "--output", output_path, ] ) + + with open(output_path, "r") as file: + content = file.read() + + updated_content = re.sub( + f'class {model_name}\(SchemaBaseModel\):\n( """\n.*\n """)', # + lambda match: f"""class {model_name}(SchemaBaseModel):\n{match.group(1)}\n __metadata_type__ = "{section}"\n __metadata_type_version__ = "{version}" """, + content, + ) + + with open(output_path, "w") as file: + file.write(updated_content) diff --git a/pydantic_schemas/geospatial_schema.py b/pydantic_schemas/geospatial_schema.py index d04b1c7..a9da30c 100644 --- a/pydantic_schemas/geospatial_schema.py +++ b/pydantic_schemas/geospatial_schema.py @@ -6,7 +6,7 @@ from enum import Enum from typing import Any, Dict, List, Optional -from pydantic import Extra, Field, confloat +from pydantic import ConfigDict, Field, RootModel, confloat from .utils.schema_base_model import SchemaBaseModel @@ -23,9 +23,9 @@ class MetadataInformation(SchemaBaseModel): Document description """ - class Config: - extra = Extra.forbid - + model_config = ConfigDict( + extra="forbid", + ) title: Optional[str] = Field(None, description="Document title", title="Document title") idno: Optional[str] = Field(None, title="Unique ID number for the document") producers: Optional[List[Producer]] = Field(None, description="List of producers", title="Producers") @@ -1478,6 +1478,9 @@ class GeospatialSchema(SchemaBaseModel): Geospatial draft schema """ + __metadata_type__ = "geospatial" + __metadata_type_version__ = "0.1.0" + idno: Optional[str] = Field(None, description="Project unique identifier", title="Project unique identifier") metadata_information: Optional[MetadataInformation] = Field( None, description="Document description", title="Document metadata information" @@ -1512,4 +1515,4 @@ class Locale(SchemaBaseModel): ) -OperationMetadata.update_forward_refs() +OperationMetadata.model_rebuild() diff --git a/pydantic_schemas/image_schema.py b/pydantic_schemas/image_schema.py index a71d648..c7ddb01 100644 --- a/pydantic_schemas/image_schema.py +++ b/pydantic_schemas/image_schema.py @@ -3,11 +3,10 @@ from __future__ import annotations -from datetime import datetime from enum import Enum from typing import Any, Dict, List, Optional -from pydantic import AnyUrl, Extra, Field, confloat +from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field, confloat from .utils.schema_base_model import SchemaBaseModel @@ -33,9 +32,9 @@ class MetadataInformation(SchemaBaseModel): Document description """ - class Config: - extra = Extra.forbid - + model_config = ConfigDict( + extra="forbid", + ) title: Optional[str] = Field(None, description="Document title", title="Document title") idno: Optional[str] = Field(None, title="Unique ID number for the document") producers: Optional[List[Producer]] = Field(None, description="List of producers", title="Producers") @@ -93,18 +92,18 @@ class MediaFragment(SchemaBaseModel): Object defining this fragement of a media asset - if ommitted = the whole asset """ - class Config: - extra = Extra.forbid - + model_config = ConfigDict( + extra="forbid", + ) uri: AnyUrl delimitertype: Optional[Delimitertype] = None description: Optional[str] = None class ArtworkOrObject(SchemaBaseModel): - class Config: - extra = Extra.forbid - + model_config = ConfigDict( + extra="forbid", + ) title: Optional[str] = Field( None, description="A reference for the artwork or object in the image.", @@ -149,7 +148,7 @@ class Config: ), title="Style Period {Artwork or Object detail}", ) - dateCreated: Optional[datetime] = Field( + dateCreated: Optional[AwareDatetime] = Field( None, description=( "Designates the date and optionally the time the artwork or object in the image was created. This relates" @@ -218,9 +217,9 @@ class Config: class CreatorContactInfo(SchemaBaseModel): - class Config: - extra = Extra.forbid - + model_config = ConfigDict( + extra="forbid", + ) country: Optional[str] = Field( None, description="The contact information country part.", title="Country {contact info detail}" ) @@ -263,9 +262,9 @@ class Config: class CvTerm(SchemaBaseModel): - class Config: - extra = Extra.forbid - + model_config = ConfigDict( + extra="forbid", + ) cvId: Optional[AnyUrl] = Field( None, description="The globally unique identifier of the Controlled Vocabulary the term is from.", @@ -289,9 +288,9 @@ class Config: class Device(SchemaBaseModel): - class Config: - extra = Extra.forbid - + model_config = ConfigDict( + extra="forbid", + ) manufacturer: Optional[str] = Field(None, description="Name of the manufacturer of the device") modelName: Optional[str] = Field(None, description="Name of the device model") serialNumber: Optional[str] = Field(None, description="Serial number, assigned by manufacturer") @@ -302,9 +301,9 @@ class Config: class EmbdEncRightsExpr(SchemaBaseModel): - class Config: - extra = Extra.forbid - + model_config = ConfigDict( + extra="forbid", + ) encRightsExpr: str = Field( ..., description=( @@ -325,9 +324,9 @@ class Config: class Entity(SchemaBaseModel): - class Config: - extra = Extra.forbid - + model_config = ConfigDict( + extra="forbid", + ) name: Optional[str] = Field(None, description="Full name of the entity/concept", title="Name") identifiers: Optional[List[AnyUrl]] = Field( None, description="Globally unique identifier of the entity/concept", title="Identifier" @@ -335,9 +334,9 @@ class Config: class EntityWRole(SchemaBaseModel): - class Config: - extra = Extra.forbid - + model_config = ConfigDict( + extra="forbid", + ) name: Optional[str] = Field(None, description="Full name of the entity/concept", title="Name") role: Optional[List[AnyUrl]] = Field( None, description="Identifier of the role the entity has in the context of the metadata property", title="Role" @@ -348,9 +347,9 @@ class Config: class EpisodeSeason(SchemaBaseModel): - class Config: - extra = Extra.forbid - + model_config = ConfigDict( + extra="forbid", + ) name: Optional[str] = Field(None, description="Name of the episode or season of a series", title="Name") identifier: Optional[AnyUrl] = Field( None, description="Identifier of the episode or season of a series", title="Identifier" @@ -359,17 +358,17 @@ class Config: class FrameSize(SchemaBaseModel): - class Config: - extra = Extra.forbid - + model_config = ConfigDict( + extra="forbid", + ) heightPixels: Optional[int] = Field(None, description="Height of the video frame in pixels", title="Height") widthPixels: Optional[int] = Field(None, description="Width of the video frame in pixels", title="Width") class LinkedEncRightsExpr(SchemaBaseModel): - class Config: - extra = Extra.forbid - + model_config = ConfigDict( + extra="forbid", + ) linkedRightsExpr: AnyUrl = Field( ..., description="Link to a rights expression using a rights expression language.", @@ -388,9 +387,9 @@ class Config: class Location(SchemaBaseModel): - class Config: - extra = Extra.forbid - + model_config = ConfigDict( + extra="forbid", + ) name: Optional[str] = Field(None, description="Full name of the location", title="Name") identifiers: Optional[List[AnyUrl]] = Field( None, description="Globally unique identifier of the location", title="Identifier" @@ -423,9 +422,9 @@ class Config: class PersonWDetails(SchemaBaseModel): - class Config: - extra = Extra.forbid - + model_config = ConfigDict( + extra="forbid", + ) name: Optional[str] = Field(None, description="Name of the person", title="Name") description: Optional[str] = Field(None, description="A textual description of the person", title="Description") identifiers: Optional[List[AnyUrl]] = Field( @@ -437,9 +436,9 @@ class Config: class Product(SchemaBaseModel): - class Config: - extra = Extra.forbid - + model_config = ConfigDict( + extra="forbid", + ) description: Optional[str] = Field( None, description="A textual description of the product.", title="Description {Product detail}" ) @@ -452,9 +451,9 @@ class Config: class ProductWGtin(SchemaBaseModel): - class Config: - extra = Extra.forbid - + model_config = ConfigDict( + extra="forbid", + ) name: Optional[str] = Field(None, description="Name of the product.", title="Name") gtin: str = Field( ..., @@ -465,10 +464,10 @@ class Config: class PublicationEvent(SchemaBaseModel): - class Config: - extra = Extra.forbid - - date: datetime = Field( + model_config = ConfigDict( + extra="forbid", + ) + date: AwareDatetime = Field( ..., description="Date and optionally the time of publishing the video", title="Publication Date" ) name: Optional[str] = Field( @@ -480,17 +479,17 @@ class Config: class QualifiedLink(SchemaBaseModel): - class Config: - extra = Extra.forbid - + model_config = ConfigDict( + extra="forbid", + ) link: Optional[AnyUrl] = Field(None, description="URL of the link", title="Link") linkQualifier: Optional[AnyUrl] = Field(None, description="Term qualifying the use of the link", title="Qualifier") class Rating(SchemaBaseModel): - class Config: - extra = Extra.forbid - + model_config = ConfigDict( + extra="forbid", + ) ratingSourceLink: AnyUrl = Field( ..., description=( @@ -527,9 +526,9 @@ class MeasureType(Enum): class RegionWDelimiter(SchemaBaseModel): - class Config: - extra = Extra.forbid - + model_config = ConfigDict( + extra="forbid", + ) regionAreaX: Optional[float] = Field( None, description="Horizontal axis value of the upper left corner of the rectange", @@ -553,9 +552,9 @@ class Config: class RegistryEntry(SchemaBaseModel): - class Config: - extra = Extra.forbid - + model_config = ConfigDict( + extra="forbid", + ) role: Optional[AnyUrl] = Field( None, description="An identifier of the reason and/or purpose for this Registry Entry.", title="Role" ) @@ -570,21 +569,21 @@ class Config: class Series(SchemaBaseModel): - class Config: - extra = Extra.forbid - + model_config = ConfigDict( + extra="forbid", + ) name: Optional[str] = Field(None, description="Name of the series", title="Series name") identifier: Optional[AnyUrl] = Field(None, description="Identifier for the series", title="Series identifier") class TemporalCoverage(SchemaBaseModel): - class Config: - extra = Extra.forbid - - tempCoverageFrom: Optional[datetime] = Field( + model_config = ConfigDict( + extra="forbid", + ) + tempCoverageFrom: Optional[AwareDatetime] = Field( None, description="Optionally truncated date when the temporal coverage starts", title="From Date" ) - tempCoverageTo: Optional[datetime] = Field( + tempCoverageTo: Optional[AwareDatetime] = Field( None, description="Optionally truncated date when the temporal coverage ends", title="To Date" ) @@ -602,9 +601,9 @@ class VideoTime(SchemaBaseModel): Frame of the video used for this still image """ - class Config: - extra = Extra.forbid - + model_config = ConfigDict( + extra="forbid", + ) timeValue: str = Field( ..., description=( @@ -625,9 +624,9 @@ class XmpSequence(SchemaBaseModel): Reflects the structure of an rdf:Seq in XMP/XML """ - class Config: - extra = Extra.forbid - + model_config = ConfigDict( + extra="forbid", + ) Ordered: Optional[List[Dict[str, Any]]] = None @@ -766,9 +765,9 @@ class PhotoVideoMetadataIPTC(SchemaBaseModel): Container for IPTC photo/video metadata """ - class Config: - extra = Extra.forbid - + model_config = ConfigDict( + extra="forbid", + ) title: Optional[str] = Field( None, description=( @@ -798,7 +797,7 @@ class Config: ), title="Digital Image GUID", ) - dateCreated: Optional[datetime] = Field( + dateCreated: Optional[AwareDatetime] = Field( None, description=( "Designates the date and optionally the time the content of the image was created rather than the date of" @@ -1078,16 +1077,16 @@ class IptcPmdSchema(SchemaBaseModel): Overall structure of photo metadata of a single media asset - sets of metadata for the whole asset and parts of the asset -- the properties comply with the IPTC Photo Metadata Standard 2017.1(IPTC/MS/2017-07-06) """ - class Config: - extra = Extra.forbid - + model_config = ConfigDict( + extra="forbid", + ) photoVideoMetadataIPTC: PhotoVideoMetadataIPTC = Field(..., description="Container for IPTC photo/video metadata") class LinkedImage(SchemaBaseModel): - class Config: - extra = Extra.forbid - + model_config = ConfigDict( + extra="forbid", + ) link: AnyUrl = Field(..., description="Link URL locating the image resource") mediaType: Optional[str] = Field(None, description="IANA Media (MIME) Type") widthPixels: Optional[int] = Field(None, description="Width of the image in pixels") @@ -1113,6 +1112,9 @@ class ImageDataTypeSchema(SchemaBaseModel): Uses IPTC JSON schema. See for more details - http://www.iptc.org/std/photometadata/specification/IPTC-PhotoMetadata. """ + __metadata_type__ = "image" + __metadata_type_version__ = "0.1.0" + repositoryid: Optional[str] = Field( "central", description="Abbreviation for the collection that owns the document", diff --git a/pydantic_schemas/indicator_schema.py b/pydantic_schemas/indicator_schema.py index 8a0b79f..7529e62 100644 --- a/pydantic_schemas/indicator_schema.py +++ b/pydantic_schemas/indicator_schema.py @@ -6,7 +6,7 @@ from enum import Enum from typing import Any, Dict, List, Optional, Union -from pydantic import Extra, Field +from pydantic import ConfigDict, Field from .utils.schema_base_model import SchemaBaseModel @@ -38,9 +38,9 @@ class MetadataInformation(SchemaBaseModel): Information on the production of the metadata """ - class Config: - extra = Extra.forbid - + model_config = ConfigDict( + extra="forbid", + ) title: Optional[str] = Field(None, description="Document title", title="Document title") idno: Optional[str] = Field(None, title="Unique ID number for the document") producers: Optional[List[Producer]] = Field(None, description="List of producers", title="Producers") @@ -718,6 +718,9 @@ class TimeseriesSchema(SchemaBaseModel): Schema for timeseries data type """ + __metadata_type__ = "indicator" + __metadata_type_version__ = "0.1.0" + idno: Optional[str] = Field(None, description="Project unique identifier", title="Project unique identifier") metadata_information: Optional[MetadataInformation] = Field( None, description="Information on the production of the metadata", title="Metadata creation" diff --git a/pydantic_schemas/indicators_db_schema.py b/pydantic_schemas/indicators_db_schema.py index c796872..83afcd6 100644 --- a/pydantic_schemas/indicators_db_schema.py +++ b/pydantic_schemas/indicators_db_schema.py @@ -6,7 +6,7 @@ from enum import Enum from typing import Any, Dict, List, Optional -from pydantic import Extra, Field +from pydantic import ConfigDict, Field from .utils.schema_base_model import SchemaBaseModel @@ -32,9 +32,9 @@ class MetadataInformation(SchemaBaseModel): Document description """ - class Config: - extra = Extra.forbid - + model_config = ConfigDict( + extra="forbid", + ) title: Optional[str] = Field(None, description="Document title", title="Document title") idno: Optional[str] = Field(None, title="Unique ID number for the document") producers: Optional[List[Producer]] = Field(None, description="List of producers", title="Producers") @@ -247,9 +247,9 @@ class DatabaseDescription(SchemaBaseModel): Database Description """ - class Config: - extra = Extra.forbid - + model_config = ConfigDict( + extra="forbid", + ) title_statement: TitleStatement = Field(..., description="Study title") authoring_entity: Optional[List[AuthoringEntityItem]] = Field( None, @@ -388,6 +388,9 @@ class TimeseriesDatabaseSchema(SchemaBaseModel): Schema for timeseries database """ + __metadata_type__ = "indicators_db" + __metadata_type_version__ = "0.1.0" + published: Optional[int] = Field(0, description="0=draft, 1=published", title="Status") overwrite: Optional[Overwrite] = Field("no", description="Overwrite database if already exists?") metadata_information: Optional[MetadataInformation] = Field( diff --git a/pydantic_schemas/metadata_manager.py b/pydantic_schemas/metadata_manager.py index e96afa2..591508d 100644 --- a/pydantic_schemas/metadata_manager.py +++ b/pydantic_schemas/metadata_manager.py @@ -1,4 +1,5 @@ import importlib.metadata +import warnings from copy import copy from typing import Dict, List, Optional, Type, Union @@ -18,8 +19,9 @@ video_schema, ) from .utils.excel_to_pydantic import excel_doc_to_pydantic, excel_single_sheet_to_pydantic -from .utils.pydantic_to_excel import write_across_many_sheets, write_to_single_sheet +from .utils.pydantic_to_excel import parse_version, write_across_many_sheets, write_to_single_sheet from .utils.quick_start import make_skeleton +from .utils.schema_base_model import SchemaBaseModel from .utils.utils import merge_dicts, standardize_keys_in_dict __version__ = importlib.metadata.version("metadataschemas") @@ -158,9 +160,9 @@ def create_metadata_outline( skeleton_object = make_skeleton(schema, debug=debug) return skeleton_object - def _get_name_version_schema_writer(self, metadata_name_or_class): + def _get_name_schema_writer(self, metadata_name_or_class): """ - Determines the metadata name, version, schema, and writer based on the provided metadata name or class. + Determines the metadata name, schema, and writer based on the provided metadata name or class. Args: metadata_name_or_class (str or class): The metadata name as a string or the metadata class. @@ -168,13 +170,12 @@ def _get_name_version_schema_writer(self, metadata_name_or_class): Returns: tuple: A tuple containing: - metadata_name (str): The standardized metadata name. - - version (str): The version information of the metadata. - schema (type(BaseModel)): The schema associated with the metadata. - writer (function): The writer function for the metadata. If `metadata_name_or_class` is a string or is one of the standard metadata types (document, geospatial, image, indicator, indicators_db, microdata, resource, script, table, video), - it retrieves the corresponding metadata name, schema, version, and writer from the internal + it retrieves the corresponding metadata name, schema, and writer from the internal mappings. Otherwise, it assumes this is a template and retrieves the title from the class, and uses a default single page writer function. """ @@ -190,14 +191,12 @@ def _get_name_version_schema_writer(self, metadata_name_or_class): for metadata_name, schema in self._TYPE_TO_SCHEMA.items(): if schema is metadata_name_or_class or schema is type(metadata_name_or_class): break - version = f"{metadata_name} type metadata version {__version__}" writer = self._TYPE_TO_WRITER[metadata_name] else: writer = write_to_single_sheet metadata_name = metadata_name_or_class.model_json_schema()["title"] - version = f"Template: {metadata_name}" schema = metadata_name_or_class - return metadata_name, version, schema, writer + return metadata_name, schema, writer def write_metadata_outline_to_excel( self, @@ -235,10 +234,10 @@ def write_metadata_outline_to_excel( and type(metadata_name_or_class) not in self._TYPE_TO_SCHEMA.values() ): metadata_type = self.standardize_metadata_name(metadata_type) - _, _, _, writer = self._get_name_version_schema_writer(metadata_type) - metadata_name, version, schema, _ = self._get_name_version_schema_writer(metadata_name_or_class) + _, _, writer = self._get_name_schema_writer(metadata_type) + metadata_name, schema, _ = self._get_name_schema_writer(metadata_name_or_class) else: - metadata_name, version, schema, writer = self._get_name_version_schema_writer(metadata_name_or_class) + metadata_name, schema, writer = self._get_name_schema_writer(metadata_name_or_class) skeleton_object = self.create_metadata_outline(schema, debug=False) if filename is None: @@ -248,7 +247,7 @@ def write_metadata_outline_to_excel( if not str(filename).endswith(".xlsx"): filename += ".xlsx" - writer(filename, skeleton_object, version, title) + writer(filename, skeleton_object, title) return filename def save_metadata_to_excel( @@ -283,13 +282,10 @@ def save_metadata_to_excel( and type(object) not in self._TYPE_TO_SCHEMA.values() ): metadata_type = self.standardize_metadata_name(metadata_type) - _, _, _, writer = self._get_name_version_schema_writer(metadata_type) - metadata_name, version, schema, _ = self._get_name_version_schema_writer(type(object)) + _, _, writer = self._get_name_schema_writer(metadata_type) + metadata_name, schema, _ = self._get_name_schema_writer(type(object)) else: - metadata_name, version, schema, writer = self._get_name_version_schema_writer(type(object)) - # metadata_name, version, schema, writer = self._get_name_version_schema_writer( - # type(object) - # ) # metadata_name_or_class) + metadata_name, schema, writer = self._get_name_schema_writer(type(object)) skeleton_object = self.create_metadata_outline(metadata_name_or_class=schema, debug=False) if filename is None: @@ -306,11 +302,11 @@ def save_metadata_to_excel( ) combined_dict = standardize_keys_in_dict(combined_dict) new_ob = schema.model_validate(combined_dict) - writer(filename, new_ob, version, title, verbose=verbose) + writer(filename, new_ob, title, verbose=verbose) return filename @staticmethod - def _get_metadata_name_from_excel_file(filename: str) -> str: + def get_metadata_type_info_from_excel_file(filename: str) -> str: error_message = "Improperly formatted Excel file for metadata" workbook = load_workbook(filename) # Select the 'metadata' sheet @@ -329,33 +325,21 @@ def _get_metadata_name_from_excel_file(filename: str) -> str: if not type_info or not isinstance(type_info, str): raise ValueError(f"Cell C1 is empty or not a string. {error_message}") - cell_values = type_info.split(" ") - - if cell_values[0] == "Template:": - return " ".join(cell_values[1:]) - - if len(cell_values) < 3 or cell_values[1] != "type" or cell_values[2] != "metadata": - raise ValueError(f"Cell C1 is improperly formatted. {error_message}") - - return cell_values[0] + return parse_version(type_info) def read_metadata_from_excel( self, filename: str, - metadata_class: Optional[Type[BaseModel]] = None, - metadata_type: Optional[str] = None, + metadata_class: Optional[Type[SchemaBaseModel]] = None, verbose: bool = False, ) -> BaseModel: """ Read in metadata from an appropriately formatted Excel file as a pydantic object. - If using standard metadata types (document, geospatial, image, indicator, indicators_db, microdata, resource, script, table, video) then there is no need to pass in the metadata_class. But if using a template, then the class must be provided. + If using standard metadata types (document, geospatial, image, indicator, indicators_db, microdata, resource, script, table, video) then there is no need to pass in the metadata_class. But if using a template, then the class should be provided to avoid compatability issues. Args: filename (str): The path to the Excel file. metadata_class (Optional type of BaseModel): A pydantic class type correspondong to the type used to write the Excel file - metadata_type (Optional[str]): The name of the metadata type, such as 'geospatial', 'document', etc. Used if - the metadata_name_or_class is an instance of a template. The name is used to determine the number of - sheets in the Excel file. verbose (bool): If True, print debug information on the file reading. @@ -370,27 +354,59 @@ def read_metadata_from_excel( >>> manager = MetadataManager() >>> document_metadata = manager.read_metadata_from_excel("document_metadata.xlsx") """ - metadata_name = self._get_metadata_name_from_excel_file(filename) - try: - metadata_name = self.standardize_metadata_name(metadata_name) - schema = self._TYPE_TO_SCHEMA[metadata_name] - reader = self._TYPE_TO_READER[metadata_name] - except ValueError as e: - if metadata_class is None: + metadata_type_info = self.get_metadata_type_info_from_excel_file(filename) + metadata_name = metadata_type_info["metadata_type"] + metadata_version = metadata_type_info["metadata_type_version"] + template_uid = metadata_type_info.get("template_uid", None) + + if metadata_class is not None: + if metadata_class.__metadata_type__ != metadata_name: + warnings.warn( + f"metadata_class metadata type {metadata_class.__metadata_type__} does not match the Excel file metadata type {metadata_name}" + "this may cause compatability issues" + ) + elif metadata_class.__metadata_type_version__ != metadata_version: + warnings.warn( + f"metadata_class metadata version {metadata_class.__metadata_type_version__} does not match the Excel file metadata version {metadata_version}" + "this may cause issues" + ) + elif metadata_class.__template_uid__ is not None and template_uid is None: + warnings.warn( + f"metadata_class template_uid {metadata_class.__template_uid__} does not match the Excel file which is not from a template" + "this may cause compatability issues" + ) + elif metadata_class.__template_uid__ is not None and metadata_class.__template_uid__ != template_uid: + warnings.warn( + f"metadata_class template_uid {metadata_class.__template_uid__} does not match the Excel file template_uid {metadata_type_info.get('template_uid', None)}" + "this may cause compatability issues" + ) + elif metadata_class.__template_uid__ is None and template_uid is not None: + warnings.warn( + f"metadata_class is not a template type but the Excel file is from a template" + "this may cause compatability issues" + ) + metadata_name = metadata_class.__metadata_type__ + else: + if metadata_type_info.get("template_uid", None) is not None: raise ValueError( - f"'{metadata_name}' not supported. Must be: {list(self._TYPE_TO_SCHEMA.keys())} or try passing in the metadata_class" - ) from e - schema = metadata_class - if metadata_type is not None: - metadata_type = self.standardize_metadata_name(metadata_type) - reader = self._TYPE_TO_READER[metadata_type] + "metadata_class must be provided when reading in a template Excel file, but none was provided" + ) else: - reader = excel_single_sheet_to_pydantic - if verbose: - print("reader is falling back to excel_single_sheet_to_pydantic") - read_object = reader(filename, schema, verbose=verbose) + metadata_class = self.metadata_class_from_name(metadata_name) + + try: + metadata_name = self.standardize_metadata_name(metadata_class.__metadata_type__) + reader = self._TYPE_TO_READER[metadata_name] + except ValueError: + reader = excel_single_sheet_to_pydantic + warnings.warn( + f"metadata_class metadata type {metadata_class.__metadata_type__} is not a standard type" + "falling back to excel_single_sheet_to_pydantic" + ) + + read_object = reader(filename, metadata_class, verbose=verbose) - skeleton_object = self.create_metadata_outline(metadata_name_or_class=schema, debug=verbose) + skeleton_object = self.create_metadata_outline(metadata_name_or_class=metadata_class, debug=verbose) read_object_dict = read_object.model_dump( mode="json", exclude_none=False, exclude_unset=True, exclude_defaults=True @@ -404,7 +420,7 @@ def read_metadata_from_excel( skeleton_mode=True, ) combined_dict = standardize_keys_in_dict(combined_dict) - new_ob = schema.model_validate(combined_dict) + new_ob = metadata_class.model_validate(combined_dict) return new_ob def _raise_if_unsupported_metadata_name(self, metadata_name: str): diff --git a/pydantic_schemas/microdata_schema.py b/pydantic_schemas/microdata_schema.py index fdf0c0a..257ef92 100644 --- a/pydantic_schemas/microdata_schema.py +++ b/pydantic_schemas/microdata_schema.py @@ -6,7 +6,7 @@ from enum import Enum from typing import Any, Dict, List, Optional, Union -from pydantic import Extra, Field, constr +from pydantic import ConfigDict, Field, constr from .utils.schema_base_model import SchemaBaseModel @@ -279,9 +279,9 @@ class DocDesc(SchemaBaseModel): Document Description """ - class Config: - extra = Extra.forbid - + model_config = ConfigDict( + extra="forbid", + ) title: Optional[str] = Field(None, description="Document title", title="Document title") idno: Optional[str] = Field(None, title="Unique ID number for the document") producers: Optional[List[Producer]] = Field(None, description="List of producers", title="Producers") @@ -1263,9 +1263,9 @@ class StudyDesc(SchemaBaseModel): Study Description """ - class Config: - extra = Extra.forbid - + model_config = ConfigDict( + extra="forbid", + ) title_statement: TitleStatement = Field(..., description="Study title") authoring_entity: Optional[List[AuthoringEntityItem]] = Field( None, @@ -1376,6 +1376,9 @@ class DdiSchema(SchemaBaseModel): Schema for Microdata data type based on DDI 2.5 """ + __metadata_type__ = "microdata" + __metadata_type_version__ = "0.1.0" + doc_desc: Optional[DocDesc] = None study_desc: Optional[StudyDesc] = None data_files: Optional[List[DatafileSchema]] = Field(None, description="Data files") diff --git a/pydantic_schemas/resource_schema.py b/pydantic_schemas/resource_schema.py index beaf096..8ade9d5 100644 --- a/pydantic_schemas/resource_schema.py +++ b/pydantic_schemas/resource_schema.py @@ -15,6 +15,9 @@ class Model(SchemaBaseModel): External resource schema """ + __metadata_type__ = "resource" + __metadata_type_version__ = "0.1.0" + dctype: Optional[str] = Field( "doc/oth", description=( diff --git a/pydantic_schemas/script_schema.py b/pydantic_schemas/script_schema.py index 74b592a..763b87d 100644 --- a/pydantic_schemas/script_schema.py +++ b/pydantic_schemas/script_schema.py @@ -6,7 +6,7 @@ from enum import Enum from typing import Any, Dict, List, Optional -from pydantic import Extra, Field +from pydantic import ConfigDict, Field from .utils.schema_base_model import SchemaBaseModel @@ -32,9 +32,9 @@ class DocDesc(SchemaBaseModel): Document description; the Document is the file containing the structured metadata """ - class Config: - extra = Extra.forbid - + model_config = ConfigDict( + extra="forbid", + ) title: Optional[str] = Field(None, description="Document title", title="Document title") idno: Optional[str] = Field(None, title="Unique ID number for the document") producers: Optional[List[Producer]] = Field( @@ -364,9 +364,9 @@ class Method(SchemaBaseModel): class SoftwareItem(SchemaBaseModel): - class Config: - extra = Extra.forbid - + model_config = ConfigDict( + extra="forbid", + ) name: Optional[str] = Field(None, title="Name") version: Optional[str] = Field(None, title="Version") library: Optional[List[str]] = Field( @@ -626,6 +626,9 @@ class ResearchProjectSchemaDraft(SchemaBaseModel): Schema for documenting research projects and data analysis scripts """ + __metadata_type__ = "script" + __metadata_type_version__ = "0.1.0" + repositoryid: Optional[str] = Field( None, description="Abbreviation for the collection that owns the research project", diff --git a/pydantic_schemas/table_schema.py b/pydantic_schemas/table_schema.py index 172b401..96359a1 100644 --- a/pydantic_schemas/table_schema.py +++ b/pydantic_schemas/table_schema.py @@ -6,7 +6,7 @@ from enum import Enum from typing import Any, Dict, List, Optional -from pydantic import Extra, Field +from pydantic import ConfigDict, Field, RootModel from .utils.schema_base_model import SchemaBaseModel @@ -32,9 +32,9 @@ class MetadataInformation(SchemaBaseModel): Document description """ - class Config: - extra = Extra.forbid - + model_config = ConfigDict( + extra="forbid", + ) idno: Optional[str] = Field(None, title="Unique ID number for the document") title: Optional[str] = Field(None, title="Document title") producers: Optional[List[Producer]] = Field(None, description="List of producers", title="Producers") @@ -362,9 +362,9 @@ class TableDescription(SchemaBaseModel): Table Description """ - class Config: - extra = Extra.forbid - + model_config = ConfigDict( + extra="forbid", + ) title_statement: Optional[TitleStatement] = Field(None, description="Title statement") identifiers: Optional[List[Identifier]] = Field(None, description="Other identifiers", title="Other identifiers") authoring_entity: Optional[List[AuthoringEntityItem]] = Field( @@ -454,6 +454,9 @@ class Model(SchemaBaseModel): Draft Schema for Table data type """ + __metadata_type__ = "table" + __metadata_type_version__ = "0.1.0" + repositoryid: Optional[str] = Field( None, description="Abbreviation for the collection that owns the document", diff --git a/pydantic_schemas/tests/test_generators.py b/pydantic_schemas/tests/test_generators.py new file mode 100644 index 0000000..cfae612 --- /dev/null +++ b/pydantic_schemas/tests/test_generators.py @@ -0,0 +1,71 @@ +import importlib.metadata +import importlib.util +import os + +import yaml + +from pydantic_schemas.metadata_manager import MetadataManager + + +def test_yaml_file(): + # Load the YAML file + with open("json_to_python_config.yaml", "r") as file: + data = yaml.safe_load(file) + + # Get the version from importlib.metadata + __version__ = importlib.metadata.version("metadataschemas") + + for section, details in data.items(): + # Check that each value is non-null + assert details["version"] is not None, f"Version is null in section {section}" + assert details["json_file"] is not None, f"JSON file is null in section {section}" + assert details["python_file"] is not None, f"Python file is null in section {section}" + assert details["model_name"] is not None, f"Model name is null in section {section}" + + # Check that the JSON and Python files exist + json_file = os.path.join("schemas", details["json_file"]) + assert os.path.exists(json_file), f"JSON file {json_file} does not exist in section {section}" + python_file = os.path.join("pydantic_schemas", details["python_file"]) + assert os.path.exists(python_file), f"Python file {python_file} does not exist in section {section}" + + # Check that the version is equal to or less than the version from importlib.metadata + assert ( + details["version"] <= __version__ + ), f"Version {details['version']} in section {section} is greater than {__version__}" + + # Check the version is a string fomatted as digits.digits.digits + assert isinstance(details["version"], str), f"Version {details['version']} in section {section} is not a string" + assert ( + details["version"].count(".") == 2 + ), f"Version {details['version']} in section {section} is not formatted as digits.digits.digits" + assert all( + [x.isdigit() for x in details["version"].split(".")] + ), f"Version {details['version']} in section {section} is not formatted as digits.digits.digits" + + +def test_every_schema_has_version(): + mm = MetadataManager() + for v in mm.metadata_type_names: + m = mm.create_metadata_outline(mm.metadata_class_from_name(v)) + assert m.__metadata_type__ is not None, f"__metadata_type__ is None for {v}" + assert m.__metadata_type_version__ is not None, f"__metadata_type_version__ is None for {v}" + assert hasattr(m, "__template_name__"), f"__template_name__ not in {v}" + assert hasattr(m, "__template_uid__"), f"__template_uid__ not in {v}" + assert m.__template_name__ is None, f"__template_name__ is not None for {v} = {m.__template_name__}" + assert m.__template_uid__ is None, f"__template_uid__ is not None for {v} = {m.__template_uid__}" + + m = mm.create_metadata_outline(v) + assert m.__metadata_type__ is not None, f"__metadata_type__ is None for {v}" + assert m.__metadata_type_version__ is not None, f"__metadata_type_version__ is None for {v}" + assert hasattr(m, "__template_name__"), f"__template_name__ not in {v}" + assert hasattr(m, "__template_uid__"), f"__template_uid__ not in {v}" + assert m.__template_name__ is None, f"__template_name__ is not None for {v} = {m.__template_name__}" + assert m.__template_uid__ is None, f"__template_uid__ is not None for {v} = {m.__template_uid__}" + + m = mm._TYPE_TO_SCHEMA[v] + assert m.__metadata_type__ is not None, f"__metadata_type__ is None for {v}" + assert m.__metadata_type_version__ is not None, f"__metadata_type_version__ is None for {v}" + assert hasattr(m, "__template_name__"), f"__template_name__ not in {v}" + assert hasattr(m, "__template_uid__"), f"__template_uid__ not in {v}" + assert m.__template_name__ is None, f"__template_name__ is not None for {v} = {m.__template_name__}" + assert m.__template_uid__ is None, f"__template_uid__ is not None for {v} = {m.__template_uid__}" diff --git a/pydantic_schemas/tests/test_metadata_manager.py b/pydantic_schemas/tests/test_metadata_manager.py index 5b8a735..13cf2d2 100644 --- a/pydantic_schemas/tests/test_metadata_manager.py +++ b/pydantic_schemas/tests/test_metadata_manager.py @@ -3,6 +3,7 @@ import pytest from pydantic import BaseModel, ValidationError +from utils.schema_base_model import SchemaBaseModel from utils.test_utils import assert_pydantic_models_equal, fill_in_pydantic_outline from pydantic_schemas.metadata_manager import MetadataManager @@ -122,16 +123,21 @@ class Midlevel(BaseModel): c: Optional[str] = None d: Optional[List[Simple]] - class TopLevel(BaseModel): + class TopLevel(SchemaBaseModel): e: Optional[Midlevel] f: Optional[int] + __metadata_type__ = "TopLevel" + __metadata_type_version__ = "1.0.0" mm = MetadataManager() filename1 = tmpdir.join(f"test_templates_1.xlsx") mm.write_metadata_outline_to_excel(TopLevel, filename=filename1, title="Outline Test") - assert mm._get_metadata_name_from_excel_file(filename1) == "TopLevel" + assert mm.get_metadata_type_info_from_excel_file(filename1) == { + "metadata_type": "TopLevel", + "metadata_type_version": "1.0.0", + } example = TopLevel( e=Midlevel( @@ -142,12 +148,17 @@ class TopLevel(BaseModel): ], ), f=99, + __metadata_type__="TopLevel", + __metadata_type_version__="1.0.0", ) filename2 = tmpdir.join(f"test_templates_2.xlsx") mm.save_metadata_to_excel(example, filename2) - assert mm._get_metadata_name_from_excel_file(filename2) == "TopLevel" + assert mm.get_metadata_type_info_from_excel_file(filename2) == { + "metadata_type": "TopLevel", + "metadata_type_version": "1.0.0", + } actual = mm.read_metadata_from_excel(filename2, TopLevel) assert actual == example diff --git a/pydantic_schemas/tests/test_pydantic_to_excel.py b/pydantic_schemas/tests/test_pydantic_to_excel.py index 4a24e11..0e39f52 100644 --- a/pydantic_schemas/tests/test_pydantic_to_excel.py +++ b/pydantic_schemas/tests/test_pydantic_to_excel.py @@ -5,6 +5,7 @@ import pandas as pd import pytest from pydantic import BaseModel, Field +from utils.schema_base_model import SchemaBaseModel from pydantic_schemas.document_schema import ScriptSchemaDraft from pydantic_schemas.geospatial_schema import GeospatialSchema @@ -22,8 +23,10 @@ from pydantic_schemas.utils.pydantic_to_excel import ( correct_column_widths, create_sheet, + create_version, open_or_create_workbook, - shade_30_rows_and_protect_sheet, + parse_version, + shade_80_rows_and_protect_sheet, shade_locked_cells, write_across_many_sheets, write_pydantic_to_sheet, @@ -35,12 +38,14 @@ def test_simple_schema(tmpdir, index_above=False): - class Simple(BaseModel): + class Simple(SchemaBaseModel): idno: str title: str author: str - simple_original = Simple(idno="AVal", title="BVal", author="CVal") + simple_original = Simple( + idno="AVal", title="BVal", author="CVal", __metadata_type__="simple", __metadata_type_version__="1.0" + ) filename = tmpdir.join(f"integration_test_simple_schema_.xlsx") write_to_single_sheet(filename, simple_original, "simple_original", "Simple Metadata") @@ -59,13 +64,15 @@ class Country(BaseModel): name: str initials: str - class ProductionAndCountries(BaseModel): + class ProductionAndCountries(SchemaBaseModel): production: Production countries: Country inp = ProductionAndCountries( production=Production(idno="AVal", title="BVal", author="CVal"), countries=Country(name="MyCountry", initials="MC"), + __metadata_type__="production_and_countries", + __metadata_type_version__="1.0", ) filename = tmpdir.join(f"integration_test_two_layer_simple_schema.xlsx") @@ -97,7 +104,11 @@ class SeriesDescription(BaseModel): language: Language topic: Topic - class ProductionAndCountries(BaseModel): + series_description = SeriesDescription( + language=Language(name="English", code="EN"), topic=Topic(id="topic1", name="topic1") + ) + + class ProductionAndCountries(SchemaBaseModel): production: Production countries: Country series_description: SeriesDescription @@ -105,16 +116,14 @@ class ProductionAndCountries(BaseModel): title: Optional[str] = None subtitle: Optional[str] = None - series_description = SeriesDescription( - language=Language(name="English", code="EN"), topic=Topic(id="topic1", name="topic1") - ) - inp = ProductionAndCountries( production=Production(idno="AVal", title="BVal", author="CVal"), countries=Country(name="MyCountry", initials="MC"), series_description=series_description, idno="example_idno", title="example_title", + __metadata_type__="production_and_countries", + __metadata_type_version__="1.0", ) filename = tmpdir.join(f"integration_test_multilayer_simple_schema_.xlsx") @@ -124,24 +133,28 @@ class ProductionAndCountries(BaseModel): def test_optional_missing_deprecated_new_simple(tmpdir): - class Production(BaseModel): + class Production(SchemaBaseModel): idno: Optional[str] = None title: Optional[str] = None subtitle: Optional[str] = None author: str deprecatedFeature: str + __metadata_type__ = "production" + __metadata_type_version__ = "1.0" original_production = Production(idno="", subtitle=None, author="author", deprecatedFeature="toberemoved") filename = tmpdir.join(f"integration_test_optional_missing_deprecated_new_simple_.xlsx") write_to_single_sheet(filename, original_production, "Production", "Production") - class Production(BaseModel): + class Production(SchemaBaseModel): idno: Optional[str] = None title: Optional[str] = None author: str newFeature: Optional[str] = None requiredNewFeature: str + __metadata_type__ = "production" + __metadata_type_version__ = "1.0" new_production = excel_sheet_to_pydantic(filename=filename, sheetname="metadata", model_type=Production) assert new_production.idno is None @@ -152,24 +165,29 @@ class Production(BaseModel): def test_optional_missing_deprecated_new_two_level(tmpdir): - class Production(BaseModel): + class Production(SchemaBaseModel): idno: Optional[str] = None title: Optional[str] = None subtitle: Optional[str] = None author: str deprecatedFeature: str - class Country(BaseModel): + class Country(SchemaBaseModel): name: str initials: str - class ProductionAndCountries(BaseModel): + class ProductionAndCountries(SchemaBaseModel): production: Production countries: Country example_production = Production(idno="", subtitle=None, author="author", deprecatedFeature="toberemoved") example_country = Country(name="MadeupCountry", initials="MC") - example_production_and_country = ProductionAndCountries(production=example_production, countries=example_country) + example_production_and_country = ProductionAndCountries( + production=example_production, + countries=example_country, + __metadata_type__="production_and_countries", + __metadata_type_version__="1.0", + ) filename = tmpdir.join(f"integration_test_optional_missing_deprecated_new_two_level_.xlsx") @@ -226,7 +244,7 @@ class Country(BaseModel): name: str initials: str - class ProductionAndCountries(BaseModel): + class ProductionAndCountries(SchemaBaseModel): production: Production countries: List[Country] dates: List[str] @@ -254,6 +272,8 @@ class ProductionAndCountries(BaseModel): dates=["April", "May", "June"], other=[], otherOptional=None, + __metadata_type__="production_and_countries", + __metadata_type_version__="1.0", ) filename = tmpdir.join(f"integration_test_lists_.xlsx") @@ -294,7 +314,7 @@ class Country(BaseModel): name: str initials: str - class ProductionAndCountries(BaseModel): + class ProductionAndCountries(SchemaBaseModel): production: Production countries: List[Country] dates: List[str] @@ -317,6 +337,8 @@ class ProductionAndCountries(BaseModel): other=["12"], otherOptional=None, single_val="single", + __metadata_type__="production_and_countries", + __metadata_type_version__="1.0", ) filename = tmpdir.join(f"integration_test_optional_missing_deprecated_new_two_level_.xlsx") @@ -367,14 +389,18 @@ class StudyDesc(BaseModel): None, description="Methodology and processing", title="Methodology and Processing" ) - class MicrodataSchema(BaseModel): + class MicrodataSchema(SchemaBaseModel): """ Schema for Microdata data type based on DDI 2.5 """ study_desc: Optional[StudyDesc] = None - ms = MicrodataSchema(study_desc=StudyDesc(method=Method(study_class=["a1", "b2"]))) + ms = MicrodataSchema( + study_desc=StudyDesc(method=Method(study_class=["a1", "b2"])), + __metadata_type__="microdata", + __metadata_type_version__="1.0", + ) filename = tmpdir.join(f"integration_test_union_list_.xlsx") write_across_many_sheets(filename, ms, "UnionList", "Looking at a union with a list") @@ -386,12 +412,17 @@ def test_dictionaries(tmpdir): class SubDict(BaseModel): sub_additional: Optional[Dict[str, Any]] = Field(None, description="Additional metadata at a lower level") - class WithDict(BaseModel): + class WithDict(SchemaBaseModel): additional: Optional[Dict[str, Any]] = Field(None, description="Additional metadata") optional_dict: Optional[Dict[str, Any]] = None sub: SubDict - wd = WithDict(additional={"s": "sa", "a": "va"}, sub=SubDict(sub_additional={"sub": "subval", "sub2": "subval2"})) + wd = WithDict( + additional={"s": "sa", "a": "va"}, + sub=SubDict(sub_additional={"sub": "subval", "sub2": "subval2"}), + __metadata_type__="with_dict", + __metadata_type_version__="1.0", + ) filename = tmpdir.join(f"integration_test_dictionaries_.xlsx") write_across_many_sheets(filename, wd, "WithDict", "Looking at dictionaries") @@ -453,7 +484,7 @@ class ServiceIdentification(BaseModel): None, description="Constraints associated to the service", title="Service constraints" ) - class MetaDataOfVariousHierarchies(BaseModel): + class MetaDataOfVariousHierarchies(SchemaBaseModel): citation: Optional[Citation] = None identification_info: Optional[IdentificationInfo] = None lst: Optional[List[str]] = (None,) @@ -470,6 +501,8 @@ class MetaDataOfVariousHierarchies(BaseModel): Constraints(legalConstraints=LegalConstraints(useLimitation=["s1", "s2"], accessConstraints=["s3"])) ] ), + __metadata_type__="metadata_of_various_hierarchies", + __metadata_type_version__="1.0", ) # index = pd.MultiIndex.from_tuples([("identification_info", "citation", "title"), ("identification_info", "citation", "alternateTitle"), ("service_identification", "restrictions", "legalConstraints", "useLimitation"), ("service_identification", "restrictions", "legalConstraints", "accessConstraints")]) @@ -611,7 +644,7 @@ class ServiceIdentification(BaseModel): None, description="Constraints associated to the service", title="Service constraints" ) - class MetaDataOfVariousHierarchies(BaseModel): + class MetaDataOfVariousHierarchies(SchemaBaseModel): idno: Optional[str] = None database_name: Optional[str] = None single_level_data: SingleLevelData @@ -642,9 +675,60 @@ class MetaDataOfVariousHierarchies(BaseModel): service_identification=ServiceIdentification( restrictions=[Constraints(legalConstraints=LegalConstraints(useLimitation=[], accessConstraints=[]))] ), + __metadata_type__="metadata_of_various_hierarchies", + __metadata_type_version__="1.0", ) if os.path.exists(filename): os.remove(filename) - write_to_single_sheet(filename, example, "MetaDataOfVariousHierarchies", sheet_title, verbose=True) + write_to_single_sheet(filename, example, sheet_title, verbose=True) + + +def test_create_version(): + class Sub2(SchemaBaseModel): + a: str + b: str + __metadata_type__ = "dataset" + __metadata_type_version__ = "1.0" + + # test with no template name or uid + ob_with_sub2 = Sub2(a="a", b="b") + assert ob_with_sub2.__template_name__ is None + assert ob_with_sub2.__template_uid__ is None + version_with_sub2 = create_version(ob_with_sub2) + assert version_with_sub2 == "metadata_type: dataset, metadata_type_version: 1.0" + expected_output_with_sub2 = {"metadata_type": "dataset", "metadata_type_version": "1.0"} + assert parse_version(version_with_sub2) == expected_output_with_sub2 + + # test with template name and uid + ob_with_sub2.__template_name__ = "My Template" + ob_with_sub2.__template_uid__ = "1234" + version_with_sub2 = create_version(ob_with_sub2) + assert ( + version_with_sub2 + == "metadata_type: dataset, metadata_type_version: 1.0, template_uid: 1234, template_name: My Template" + ) + expected_output_with_sub2 = { + "metadata_type": "dataset", + "metadata_type_version": "1.0", + "template_uid": "1234", + "template_name": "My Template", + } + + assert parse_version(version_with_sub2) == expected_output_with_sub2 + + # test with commas and colons in template name + ob_with_sub2.__template_name__ = "My: Template, with, commas: and colons" + version_with_sub2 = create_version(ob_with_sub2) + assert ( + version_with_sub2 + == "metadata_type: dataset, metadata_type_version: 1.0, template_uid: 1234, template_name: My: Template, with, commas: and colons" + ) + expected_output_with_sub2 = { + "metadata_type": "dataset", + "metadata_type_version": "1.0", + "template_uid": "1234", + "template_name": "My: Template, with, commas: and colons", + } + assert parse_version(version_with_sub2) == expected_output_with_sub2 diff --git a/pydantic_schemas/utils/pydantic_to_excel.py b/pydantic_schemas/utils/pydantic_to_excel.py index d8b0c30..0b2d2ce 100644 --- a/pydantic_schemas/utils/pydantic_to_excel.py +++ b/pydantic_schemas/utils/pydantic_to_excel.py @@ -13,6 +13,7 @@ from openpyxl.worksheet.worksheet import Worksheet from pydantic import AnyUrl, BaseModel +from .schema_base_model import SchemaBaseModel from .utils import ( annotation_contains_dict, annotation_contains_list, @@ -109,9 +110,9 @@ def correct_column_widths(worksheet: Worksheet): worksheet.column_dimensions[column].width = adjusted_width -def shade_30_rows_and_protect_sheet(worksheet: Worksheet, startrow: int): +def shade_80_rows_and_protect_sheet(worksheet: Worksheet, startrow: int): """For use after all data is written so there is a clear border around the data""" - for r in range(startrow, startrow + 30): + for r in range(startrow, startrow + 80): protect_and_shade_row(worksheet, r) worksheet.protection = SheetProtection( sheet=True, @@ -504,25 +505,81 @@ def create_sheet(workbook, sheetname, sheet_number): return new_sheet -def write_to_single_sheet(doc_filepath: str, ob: BaseModel, version: str, title: Optional[str] = None, verbose=False): +def write_to_single_sheet(doc_filepath: str, ob: BaseModel, title: Optional[str] = None, verbose=False): model_default_name = ob.model_json_schema()["title"] if title is None: title = model_default_name wb = open_or_create_workbook(doc_filepath) ws = create_sheet(wb, "metadata", sheet_number=0) + version = create_version(ob) current_row = write_title_and_version_info(ws, title, version, protect_title=False) current_row = write_pydantic_to_sheet(ws, ob, current_row, debug=verbose) correct_column_widths(worksheet=ws) - shade_30_rows_and_protect_sheet(worksheet=ws, startrow=current_row) + shade_80_rows_and_protect_sheet(worksheet=ws, startrow=current_row) shade_locked_cells(worksheet=ws) wb.save(doc_filepath) -def write_across_many_sheets( - doc_filepath: str, ob: BaseModel, version: str, title: Optional[str] = None, verbose=False -): +def create_version(ob: SchemaBaseModel): + """ + Create a version string from the metadata_type and metadata_type_version attributes of a SchemaBaseModel. + Optionally include the template_uid and template_name attributes if they are not None. + + Args: + ob (SchemaBaseModel): The SchemaBaseModel object to generate the version string for. + + Returns: + str: The version string. + + Example output: + 'metadata_type: dataset, metadata_type_version: 1.0' + 'metadata_type: dataset, metadata_type_version: 1.0, template_uid: 1234, template_name: My Template' + """ + output = f"metadata_type: {ob.__metadata_type__}, metadata_type_version: {ob.__metadata_type_version__}" + if ob.__template_name__ is not None and ob.__template_uid__ is not None: + output += f", template_uid: {ob.__template_uid__}, template_name: {ob.__template_name__}" + return output + + +def parse_version(version: str): + """ + Parse a version string into a dictionary of key-value pairs. + + Args: + version (str): The version string to parse. + + Returns: + dict: A dictionary of key-value pairs extracted from the version string. + + Example input: + 'metadata_type: dataset, metadata_type_version: 1.0, template_uid: 1234, template_name: My Template' + + Example output: + { + 'metadata_type': 'dataset', + 'metadata_type_version': '1.0', + 'template_uid': '1234', + 'template_name': 'My Template' + } + """ + version_dict = {} + version_info = version.split(",") + if len(version_info) > 4: + template_name_info = ",".join(version_info[3:]) + version_info = version_info[:3] + version_info.append(template_name_info) + for item in version_info: + key_values = item.strip().split(":") + key = key_values[0] + value = ":".join(key_values[1:]) + version_dict[key.strip()] = value.strip() + return version_dict + + +def write_across_many_sheets(doc_filepath: str, ob: SchemaBaseModel, title: Optional[str] = None, verbose=False): wb = open_or_create_workbook(doc_filepath) ws = create_sheet(wb, "metadata", sheet_number=0) + version = create_version(ob) current_row = write_title_and_version_info(ws, title, version, protect_title=False) children = seperate_simple_from_pydantic(ob) @@ -534,9 +591,9 @@ def write_across_many_sheets( child_object = subset_pydantic_model(ob, children["simple"]) current_row = write_pydantic_to_sheet(ws, child_object, current_row, debug=verbose) - correct_column_widths(worksheet=ws) - shade_30_rows_and_protect_sheet(worksheet=ws, startrow=current_row) - shade_locked_cells(worksheet=ws) + correct_column_widths(worksheet=ws) + shade_80_rows_and_protect_sheet(worksheet=ws, startrow=current_row) + shade_locked_cells(worksheet=ws) sheet_number += 1 for fieldname in children["pydantic"]: @@ -554,7 +611,7 @@ def write_across_many_sheets( current_row = write_title_and_version_info(ws, sheet_title, None, protect_title=True) current_row = write_pydantic_to_sheet(ws, child_object, current_row, debug=verbose) correct_column_widths(worksheet=ws) - shade_30_rows_and_protect_sheet(worksheet=ws, startrow=current_row) + shade_80_rows_and_protect_sheet(worksheet=ws, startrow=current_row) shade_locked_cells(worksheet=ws) sheet_number += 1 wb.save(doc_filepath) diff --git a/pydantic_schemas/utils/schema_base_model.py b/pydantic_schemas/utils/schema_base_model.py index c52732b..77a798c 100644 --- a/pydantic_schemas/utils/schema_base_model.py +++ b/pydantic_schemas/utils/schema_base_model.py @@ -1,4 +1,6 @@ -from pydantic import BaseModel, ConfigDict +from typing import Optional + +from pydantic import BaseModel, ConfigDict, Field from rich import print as print_rich # from rich.pretty import pretty_repr @@ -12,6 +14,31 @@ class SchemaBaseModel(BaseModel): def pretty_print(self): print_rich(self) + __metadata_type__: Optional[str] = None + __metadata_type_version__: Optional[str] = None + __template_name__: Optional[str] = None + __template_uid__: Optional[str] = None + + # metadata_type_: Optional[str] = Field(default=None, alias="__metadata_type__") + # metadata_type_version_: Optional[str] = Field(default=None, alias="__metadata_type_version__") + # template_name_: Optional[str] = Field(default=None, alias="__template_name__") + # template_uid_: Optional[str] = Field(default=None, alias="__template_uid__") + + # @property + # def __metadata_type__(self): + # return self.metadata_type_ + + # @property + # def __metadata_type_version__(self): + # return self.metadata_type_version_ + + # @property + # def __template_name__(self): + # return self.template_name_ + # @property + # def __template_uid__(self): + # return self.template_uid_ + # def __repr__(self): # return pretty_repr(self) diff --git a/pydantic_schemas/video_schema.py b/pydantic_schemas/video_schema.py index 42c0932..e9d1149 100644 --- a/pydantic_schemas/video_schema.py +++ b/pydantic_schemas/video_schema.py @@ -6,7 +6,7 @@ from enum import Enum from typing import Any, Dict, List, Optional -from pydantic import Extra, Field +from pydantic import ConfigDict, Field from .utils.schema_base_model import SchemaBaseModel @@ -32,9 +32,9 @@ class MetadataInformation(SchemaBaseModel): Document description """ - class Config: - extra = Extra.forbid - + model_config = ConfigDict( + extra="forbid", + ) title: Optional[str] = Field(None, description="Document title", title="Document title") idno: Optional[str] = Field(None, title="Unique ID number for the document") producers: Optional[List[Producer]] = Field(None, description="List of producers", title="Producers") @@ -278,6 +278,9 @@ class Model(SchemaBaseModel): Video schema based on the elements from Dublin Core and Schema.org's VideoObject """ + __metadata_type__ = "video" + __metadata_type_version__ = "0.1.0" + repositoryid: Optional[str] = Field( None, description="Abbreviation for the collection that owns the document",