From 12cdb857480eddc1510afc329251494104f0c965 Mon Sep 17 00:00:00 2001 From: gordonblackadder <171737385+gblackadder@users.noreply.github.com> Date: Fri, 25 Oct 2024 14:45:56 -0400 Subject: [PATCH] geospatial and image (#5) * include resource * make geospatial work * test excel with non null values, fix lists and lists of lists handling, improve dictionary handling, make all embedding vectors lists not dicts * fix image schema json; resolve issue of optional default values * include excel files for geospatial and image * remove LDA and Embeddings from schemas --- README.md | 9 +- excel_sheets/Document_metadata.xlsx | Bin 38759 -> 32547 bytes excel_sheets/Geospatial_metadata.xlsx | Bin 0 -> 52553 bytes excel_sheets/Image_metadata.xlsx | Bin 0 -> 33154 bytes excel_sheets/Indicators_db_metadata.xlsx | Bin 15567 -> 15735 bytes excel_sheets/Microdata_metadata.xlsx | Bin 53801 -> 49097 bytes excel_sheets/Script_metadata.xlsx | Bin 40325 -> 34124 bytes excel_sheets/Table_metadata.xlsx | Bin 38929 -> 32726 bytes excel_sheets/Video_metadata.xlsx | Bin 16584 -> 15705 bytes pydantic_schemas/document_schema.py | 42 +-- .../generators/generate_excel_files.py | 14 +- pydantic_schemas/geospatial_schema.py | 42 +-- pydantic_schemas/image_schema.py | 56 +-- pydantic_schemas/metadata_manager.py | 44 +-- pydantic_schemas/microdata_schema.py | 40 --- pydantic_schemas/script_schema.py | 42 +-- pydantic_schemas/table_schema.py | 42 +-- .../tests/test_metadata_manager.py | 172 +++++++++- .../tests/test_pydantic_to_excel.py | 241 +++++++++++-- .../tests/test_pydantic_to_pandas.py | 318 ++++++++++++++++++ pydantic_schemas/tests/test_quick_start.py | 22 ++ pydantic_schemas/utils/excel_to_pydantic.py | 131 ++++++-- pydantic_schemas/utils/pydantic_to_excel.py | 165 ++++++--- pydantic_schemas/utils/quick_start.py | 101 +++--- pydantic_schemas/utils/utils.py | 11 +- pydantic_schemas/video_schema.py | 42 +-- schemas/document-schema.json | 139 -------- schemas/geospatial-schema.json | 130 ------- schemas/image-schema.json | 130 ------- schemas/iptc-phovidmdshared-schema.json | 12 - schemas/microdata-schema.json | 136 -------- schemas/script-schema.json | 130 ------- schemas/table-schema.json | 136 -------- schemas/video-schema.json | 131 -------- 34 files changed, 1057 insertions(+), 1421 deletions(-) create mode 100644 excel_sheets/Geospatial_metadata.xlsx create mode 100644 excel_sheets/Image_metadata.xlsx create mode 100644 pydantic_schemas/tests/test_pydantic_to_pandas.py diff --git a/README.md b/README.md index ae048b6..e30b71a 100644 --- a/README.md +++ b/README.md @@ -33,9 +33,11 @@ There are metadata objects for each of the following metadata types: |------------------|-------------------------------------------------| | document | `document_schema.ScriptSchemaDraft` | | geospatial | `geospatial_schema.GeospatialSchema` | +| image | `image_schema.ImageDataTypeSchema` | | indicator | `indicator_schema.TimeseriesSchema` | | indicators_db | `indicators_db_schema.TimeseriesDatabaseSchema` | -| microdata | `microdata_schema.MicrodataSchema` | +| microdata | `microdata_schema.MicrodataSchema` | +| resource |`resource_schema.Model` | | script | `script_schema.ResearchProjectSchemaDraft` | | table | `table_schema.Model` | | video | `video_schema.Model` | @@ -65,9 +67,6 @@ filename = mm.save_metadata_to_excel('indicator', object=indicator_metadata) updated_indicator_metadata = mm.read_metadata_from_excel(filename) ``` - -Note that the Excel write and save functions do not currently support Geospatial metadata. - The manager also offers a convenient way to get started creating metadata in pydantic by creating an empty pydantic object for a given metadata type which can then be updated as needed. ```python @@ -98,7 +97,7 @@ Next update the pydantic schemas so that they match the latest json schemas by r Finally update the Excel sheets by running - `python pydantic_schemas/generators/generate_excel_files.py` + `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 f9ec93d30771db6adb52bad1627a8aac737d9497..2020cc3de0f06e5452fec42491ab209910ca5bb9 100644 GIT binary patch delta 16677 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 B9pL}~ delta 19273 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 literal 0 HcmV?d00001 diff --git a/excel_sheets/Indicators_db_metadata.xlsx b/excel_sheets/Indicators_db_metadata.xlsx index 83e63310c027e3a0a5d0052800f3d4aae92441d4..a563383efe94d6637fda9c6a6cae26475ef569fb 100644 GIT binary patch literal 15735 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%0AjGpWY5qOoMSRYL&g~$AcWGh6L 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 Si-kpmekq|z{JQ+lZ~qHxXH=m8 literal 15567 zcmeHucU05cmoL5dB7#6{H0ed9BPi0Fpr8Ul1u4>dO9F^g6_KbQy^9D@=`8_4P+CNq zh@nU)LVyqggd{KCJ2UrpeKT*&eSgh*57sAHS&O}M_UG(&4&QBUM$aHXLqo$t_`+yoBq5PD)-Y zQX%ACy^K%%K4y(U&o?6C!;vC|+jA|-TF z`OP$*$ug2tZTkdm^A>yk4qO=#%*{g9e>34@6_fHhW$AsiG&H>bYQinQK)0VZJW02> z)2YOfKsNoTlb!)S_W<43Z7I)j<;$0WFJ4Cu^>mLJ?_-u+eZIbN3ayNj1+wA-c9-vR21^Z zeNK*g^_M6;N5`Z%KI$|ieLG9Hp7LFV&=Pa|siePV)Hm1h`4G}ZBc#{${Y%5av52}a z_V*RXTOHxt)d1qz!jLlE5wcqK`hELYferNZ{4eBWIp&<;VWOeQk)omDr;rowtr+a# z=HvF~tMX5J)~;O-`l>Df#=t&>yYKk0cb|!^ujLQ&44tgaieIw?+~ak4;2q(b9uGkn zo#E+w%DMK~qB&ceL2iXGXlS&wtR8_~SGc*HkObWdcbQO}9BXM%oSLy$ImxT9BwP;L zXlE-mcdonUNz};<20gu)$091Dc`|KXL71Q$D>q|Bz)o$&1zd!gorR#=V(G*jO^;J6Ja1=7=S3Ylf zEos*7U01u8yWW*MJwu<3W8yx7pp4BXJH^9-EGnE@nz;^xq0u{CYx(zkzXq%Mh|J7w zhO5{2eNOckWtg#P$$OW(?TskWXRR&v{N(y&=WBI(*~OGEG&aqtfv>;Fvu z)LCXf1S&Ei)`HS^+;CXeNaiJVzh@Zp{4D%b?P6f=BStQovt2{x*3mHi`jnY>k3Ty; z8biv|S&Ohd{}78aiaYlH8;pp1*7C7LH8K~ZId}&O@vNjRUu(%mW*h`9&*ztCj9VBC z`nSrlZW2yhzmX{AtHmFvDe7(zjojHuFO^V1V)oe3mfG%sD^FiLA(8WW_XAI~pmrQU zJ61-%C&nw18cSSHi0Nv@l_}w)++D3_qv= z&hlaUK@z^w@41eBp@YfS_+6fhBHwd@7K4Vo3CyJzhjuQvek*?FAV~Y{@U+^?({k^} z!k@HnR^@1m@$(Hp*_reco}HBBtIz;j81s{4OTUtGhR9`0x5|ZfgSv!)%xR-O9O*H+87@hl(nv2KcJzF-!VJ9CxuX@KTYoL2m_>4$k-J#g} zfQopRYkb{9;g+9kc@M6>I-Q~G^5MroddT2dIv4L_=d+Z*9ZY@1ix5vxH+{ZV6yU%> z%yaHE=~#~IyG?8aCyc6k%RR&!jR={}b4!-%t*EFm7eBws+7^GsEB|Gpn(hm66*k7r zCUM<`$Z)o^PrF;KtK*_*$wObyzN_4gYuA7%fRLq_rfr?V&kbi!fZRKVQ?E~5^u69i zs8;?uuBIS;%RX@6LysLSL*|P{e)|uihi=~H+Wy4Jc-Zr+@+S^GGK;OzKiPPY+PiV8 zi*?!cx&&*V-LTu)6uvxzBfcxPIMXNN3$P~XM(ykKE3m>~T{+~QHgKtSfL--`L!jAe zJrrIiscyRcrr+OEypKc6UHh?EeOQ6mX~K?E==_ONCEI7`8$}G@^ghkG-!#?pGSi#b zhA%zx>)cy;A1?A;V}hXxlxgp?^kry>u+(Amx)`JVRIDdytLWHzVL~Rd^L%1N6fo2M zNw>9{fRFd9lX4duTpGtWn>#Oqj6G@k<)-JTv*-D|@Hh-gI0RLtehV^&0M_Q!8kRBwelgkk7$ud5E3Q z_!d;C%(MtAYztqKHR-ynjxbcM=X-Xzb#AdIz|yvnFSMSUPNk_uqaxBx*}ZIQTAqVd z?t0QyzV8-JYoklqIn|NJL-(JBb+~L1{XZIUFK8_Rx1G*cgLhDK`@R`k#^yN@?|Es1 z4e3ri5oJ0gq%v>4{`e6SKkWeY@}B&S5YaUSapg_vK=J;_XyiM}>ONCwJ(m2A=<7CxNa0D#?Ct^1IzYiwIoO z#jG>ZH&4S39S>;Cm^HDH>+ z&8o6DRo!2eIU-^lupEWA)u$5;pNpuDroPg6z~-kvdTv2)a)i@khf_^7ozYo2tNu*! zyd|6VHxh>P^}DWzvumjc+P_!VG}{NGwx?)lPPfz29R9O_g!u*D3G#4r3l38J_Qr=-jX(20~n!og?nA5|>hSza0ecVv*%~pvGo6aWfx6*6zcQ_wX%NX=le9 z&h1L%n#?D|+cllLd*mO8A+2ZWwV;P~kNo4wuOj$uW}J{CL$ zgg8v=I!>cV{s__nKri7gnXU6~aVk;>1h$_YJgQA`GU!FKjOgYWu#d2mii{7VH6%BH#xhy5w6NFmV`Z z4^L>(12;n8d&4Ll$mU-DHg>^^%rxGBl(p8G(p$j|hoq6seW!tlmdMr?$nGq067Uu` zH%cgP*&|q`S>cJ%srgf&w)~D~1dW4ey#^qu5s6*yJ7B>>h)YC7L^HVu0&yg7%%c6a zIx%PhGHM$;%|+S?Li%ZR`9&n}v0#-Kl^0nT`TNT=%QM|GkxKcu@@_f1INx->;e2b# zW$NbC4S0KW@*W$OhvC{+9@fav_40CKVpe6RUQo#HUP2EHbHhI=$3?g6fDg;Nc&qe=I~@Ty1b_huR{(MmD3g+PG87sEq_pQWP4Nq2}CxD=0+;^)s;=4R~`t6{3h{pOT+4MhWNw?~1+`!4emHl#c_ESY@6=%u22i$_fwQzN?7EV&2B=ZM-5YuTi;y z6b3%k1YI`+la?OmL*TQFBzn@C+hBZB57w;TC^JntP1#B* z-$nPm_IvHl=!88{tSFWa%ZFvdieQljEk$HB&Z9Qiv=c9bos6EVbykjHkOG;MCC}&txf!vwOsPJFe-dkU-oeYgRA)6CRAYF2@|$ zuEg<3)0_`%-aG1wm6iijewp!P|HXHU1l+S>L%5nd6{ZaO#U?laud zmosMI@34EsrqW@qBs3pqV9j<)U*3bEs(@dP!K}lShtBO>vNUM2QLJo089)3i>fQDN z(=*J==XMCmHg06sHV_qMiIU^sxB^)NMJ_Apms21 zoS;HcQ@%u%nvAy^4iq(S!LJG72YbQ;*?4>6gN&*>1ZU=U-lYZm@gWY+=g& z!smZ(+U+23$ZO0uk_*0rI9lE_y}l0fgMez_pjPk?^w!0w^*CllMQ%J{{~@zF5%4>@ zdt>-)5_H}YZzW{Ukvl;>WT4*$vC=iMC5kr6`HJqyCW0!1+r1x>x$$CTNSnTuOKAR@ zUcN&$WIFwD?k4-ixHm@+os3I%Ic3b4TlCTjD!L7f+l*UwhxGaj69xh-(C6*TKYXNSj|=Lr4VBe^5~LG#wS%xCYC3{TpEv5Y)**{-JQl z{FvCbp^@Gwg>5tyY!l?|&oYyY2wVqgq4{Xc3wiQ_Y@(4O5=ih;GxB0&`HlGdw;0KN zX(2n~0hFCFu8Ju}RnZ(c3~VgB0B?@4@ldh(%Gp=iDG8h}=tY<4x_Liwe|8SF)`^=$ zXOpUzHqc_x*Eb+ej7B!@2GQzMsGIe9s&iYL`5+Qq6c97 z`s!MFb|qmdZjiD(s(KIXTTd}j%pEaNv?}vM{0Q&@eqHS<6#({ zUL2$Aj6g#eUGVpInbsDReKA;(C+t+j&ERmE=Grr@0zx+z`JrzDN~Wx zrEc4bt54aBt6zzj2@i{K!OPOVD&e^}- zM{>~HhFgs!pJJlQxdVfnD-P$ZS00XT>%dRf=dLU8yn^j#1im~gKvnUfa1<~T3=EjD zdmlTDdLNrexlu#lE*j=iN0ZG&Fq0)dgvoMLSRI8DZft8YvKtYhY9*95;tpjc6yTf4 zsY3L5B=SxZB%$HbWOH%eGdjyrfnBy~ga9?wF#e21>@bJ|)V&xX-sWZ?5F|6ZE$M-D z7s+|4@Bb31MoswZXESxIz$sXtBO?K)Lc*peMZW}9Jf|;LZ)+2vrsC(8THKmh>C?HY zIU1BdoNLk)N!S4s3AoV{x>DQd1)XGCs~V6&<%z@qpQy!YmEqRjdaw5j1mrRTPe<%x5cR9s5&qWl?nOR!zdv5 zlToO(xlyfF>5cJ7TK82(>T28PGNGTXhg|NeR>Lr2M|8=~ggQrtS^y|J5126r#nhlc zF(AeKibK>nu~PQ6_6hj4_AD=2_m(@fXm0!Qvu!pu^3-j!-I=L`WeaXRtw zv*p|q~ z%1NM}6R~+6vaxq3U&FupGgSnwDfyVvFA*r%?~lbIcCzS~yqKu+I?LWNVShM`0zx=> z71e_4?96UKPCvrw?fih#x5;kr_3=-@twhy(Zmj?A1<=oHKv$ngI?9pJWUNB`u4S0y zMMc4!)nhDFc50_%v7pnjL8AC%ZoX&D&$QFDE7oC$LttPKVilgqP+z@f_TU=5P^2F1Z{D^4?0{`+JUJ_ z0AXOU96x(9l1!KjvR&#S@6YZG4-AlZXL|-vIs@Q?O)?Rdr`2{4S*~+~{x?r7KX1al zvsXLb!&f`*y<`?@sbtOv%tYP_9XnDZlxF3g!a>FI(IJ{)KrR@7MD>MhE8hNjod7BM(2l2-*;gG@$?eE0-%C zT;M4vR~}+YEZeVPSNH66{Pc4a5T*M~n{lZv1G}aRPa8bDr>g~3mMA+3Wpy((kr8&g zm}MJisxKoWSX!nAefrtv5LCj=khMci$*}AOpp9*`Bh|W1PWqegy_M7)d_1elf#fu? zLP6$4Zzi8DUvIohX$kbcN^!r8Mri>QnsUij#MEp>n!We&+oE_Ek<8<^m^^sOhuPF5 zQ80XyQCv%RYMKa#w>5*t@h_BjLvdk{lq8iB$4eJ~XHoO=oX*3aTFPNJgSU@gB=oXO z`xg3rru+v1(gp|B5C(LMK=`f6Q6dJN*mD*G+waqzuEr3~SYiCn3ESQ@HOR6qgF+wM z+=gmMlnX!~U$XHsJ?|+a=B9u3(iJF=IlZmxDr(q%RtwAv2R%vfYgmq2uU+aC$O8sD zATY?sy&OM`_jGoX56o zrY05Ar%MZKp%32{$jQi>vXsTjsNmLDDwT5KEW>cG&W7vT_g3`e&u;guKm+3LOW!gz zH<9M=rC6iVdtaq|9vXM)(kW95PigtmVmWEE-W0w#PnQ()wy6D6mhOt*YhkS$YllDq zU!8#A;CI`ONW#V&Jp7Xm3pZ zEG@BvKL2ce&D7|s^;4*z1jBHdWh4eg&yA-{ zFwbR5l=DUeG=tlRLrngG@LG~6T-`};ex>=JQuaSp7_xtjH}(Sb*urC#qW?a5Z9Hc3 z^W1oz@H4j4h04*1+D^~@zwrMd+y6xv>fwi>)M4C-ylS(5m=ONQ3YWtK|1Uzi>z{@6 zpZVbbEn!98y_MMp+oa={ zTiDvBwg?>vd|$>}kK~Wvf#{*W_igX*+yIdsrihyxS?~iD621pV9Kfw1;NKQkhv!hs z`+Mib^`;kRQTscqr2TbvQeYT)c2&8#Wp8_Sr*ED(444TZ25v1l11)u($zZ$#u&wPX zWDkUJ#4Z8m(7B{Nck&im@!-I|sfE0CHe%m>KOBeFY-uLrR`9#Cw~2kj2TO=t9i5sM z1PRxm2iXkAZ(`S`r*w3uK)VMEe&|X(5>l8?4&90-howPC-}k0TKc4{wk(dcfWahs= zh#K8lJ}5^|L-T+#7-o+ z=u0K5%og+*~R{U7D4jbW@>z!KvbopG1Q}IjIXD z_rCQ}(UE2xDkjU!bXA8xZ{O_Q)w0ZNbJK3TZ?u=umB(fap2Z1eV2{LAJzs+_!TMgE zmsO5E5#!7`g3mo2qkFIz{XPAZjMqmK)ZLSc*1(zINbMog2vfVdUy#6_uZuD)Y0NHJ zYdGMjR;2HpuU3TKd5@|Ox;az(&rX=<$T63rOD*cJx!>ND1pc@cp)$fJ`lg=rBaKxy z<5c-e&L7=+9P*1fheS3~s?OFIy1D6?RJpNM<)qgMbQywUa5MGcwm?7g%@D`(V)rgT z2`#ax$k>@$6hn+i$ygdPnvp{hI9bzcQr>`Uz zM-zLxrg_I+U9_{YWlh-Kd_LF+>o$BE|8R=2OJ_YtR~a?ItXwGVe!<-30;o{M8roS1 zC-pSm4tE?(I+eqjeOuitp;*-SQ*A9^L@3U*|89KFNgIJ4kH}|zH-yd95(79OEXQ8* zD7{eJ*!kL@CeUs?(Q18U`Bd6dPLVC+b40Q7jQdIc)n)}$ymrj}E9E{Hm zXkO7pr~K;6>+?zMjmb;JSzYP3y0&*7d}%-G!D~j&%aez3s}+Qb22GZFcPn){PFy)% zMf3KKj7wfXk;AFeX6NJ!)gFDqec_Q?#7caS@AMsOFb_H~*=9>eHwV?ag|P|XXm_hS zpPw*2e{CWlM%9cw@D0f}@;n#z>c>I(>5Z~}Ebi!|A|pe8x$X24^VCo%Og9e&FTs}H zx>*}Oy-db_dLf{*hsaebP5wM?$SUdgdXMq%h8AZ`d{?E6h{;k6<^D5&{Ctr8rk~%P zpBd!AqajPmkeDH)w3qplZ=8ua+Bq}EgoWpP6ka5(?rGQM@4l~LQl2vA-|tEFjE{Y6 zbI$n!2<=b&(J3Od2)BD+tcAo{pY}NB7?7$k=(Tg`WIZ@P$jSDeY_(>p<(8*Se_$F@ z{Fi$PUkjMNLL3TlEShRbHwR+gp0kt}Dz355Uw9_Qer2V+MAFSHoDIQVG4N2p!zf|C z#*R2y&?~3$Hu$2`O9QvU+2IpWnl=1N*}XN&rO!F;%10KirnQ=!nZ6+uY3PD~s`AY1 z$^{n-U1P)$_Skdnn~vhmmb-cP3xKgRcbBRq8ZgSr7s2VpdhZ=nnGcz{*_^w3E|7P$ zPF&RFg3~-UD1{y|HvK8R0o{=EcrUxXgTkAzT-}4S{r`2s% z_bL9JUsd#6a}|YG0)=CdKl%OBm;ROIKm4jLU5D$g630Cl-vZ6y7bAsqWeTqZg_7d- zC=KkJ%6Dr=I%tf=nw731pCa(C?uu$DQCc?RT6|a!$zHdfAL2A6TB3r6W3vL14{*ib zkAm1OSM=*xJVmEF0K#uSJT$kF=OQ2bTDEdpw+fu5zR9f={R;5nysONqh3oT<@4R=; zbXjF zpTTjPZW>BA1%Bx0zToI@ke6JObZlF8)^0a426{iiTb94gxW}`ohHk}!wy#gby_qA# zM_{>N=b@SDeb$n0rkEUgqoMbLYMYO4983-Px;$z!o*(gHuUqydS-G7PFp7~oM|5fnU zRQQ+RDn*cD`oD(kufo5^`Cr1O>VF9T7g7JKo?k=hFFlGH|7l=Zn=w)bS!rlkDZjdu LBp#*t^V9zVT%D4l diff --git a/excel_sheets/Microdata_metadata.xlsx b/excel_sheets/Microdata_metadata.xlsx index 6f1691ef7d5e8a1a6fce50c954a1787f4258605c..b6d47d551eb21f8ff68810ceaff971c9f8a0e51e 100644 GIT binary patch delta 20197 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 Gum1xiC&&Q+ diff --git a/excel_sheets/Script_metadata.xlsx b/excel_sheets/Script_metadata.xlsx index 9038bdc3f4e73ab6df07420dc4518ada656c3775..2b725d51a9117ad328e2c5907367c2b7aeae13c4 100644 GIT binary patch delta 2501 zcmZuz2UJtr61_=4YC;PtAVH8CNGh@nXfpoA)@5cnZ)`TxiD?^<`wxo2k1o-=FD+=;G-zKVy!k6XYvkN^O1 z1AIZwsqkhpOE^nSn^e!SHmMK*5Ms%5{vO9k{sEUT9{!~BY9SZ>(o>%wYr*ozxSa25 z3Rz@#g@g~bC1Yjo-wNL5RsLHJs8jnJvB`5LuCu6pAc<8x4w=U>2#3e+}eAF zApH|_aKyPLXiFbNst_MMPCa7BtTiI2Eu7{B6XbQsGwqY}%vyuq6zX+mNbuU?+cPsIOQrmj33-$GKT2>g26iT^97r@zIYw4Hi>E|oi7he zme)diw{;e}lm(FSy$Mx?&F+H{k+Un4B&^TLVM07(!{B*Sh8WH*tGSB$-_aP_veCs$&d7?Q`p>he#sO)&|Fa(mr`Rm zvKmDbVH`NX05iqb=mIJ4Cs$n06c9%?!?zYTKbMo40byHj>)T_)BJb%r1Rv{@YzEZ` zvNGpQpB7t|2G;0fN|r$n$}-r}vRujVGqhUQbw2o@sHs6m9LW!BnU$};!aJ-F?^1Wp%=ABXUtB*(J}H5Y^C&FQs(lAx(%vXBk{qW z5qZ((IGMToDka2jV}sG%5nX-z_8sMIc{EWcpG3d!glJ3~@0qUU_cD%)@VlGzS2;6Q zlosXceqHg+sv_4!-^S%HU-4w*B-Y(He=Du;zJUmF%-R@{fHOIHOYX;A`?ZTUqR9q& zKM&hgq@vwha+MJ)ooAczv&|nIXaBY~@DS|PU-EeJgCpUi5;P%p@KR~=5uOuvalHD9q$~EL1C>%>K350kQnmI~2{{)h|7^-f1S*cfbQo8 z9}-1DYi>?~-;XF_sh=XZ)#@7Clf-ZO|NVmkds(B$uP5s2H5vAydvk@@I4ZdkP+@oW z*4bR4{X6>)@S874WEqNs6}kLuLoTzeXbcPhzOsfJ_%8&OjDl2zf^K;dYyfhe1QZql z=H-c<+0nCDegZpuzD58*h#w}{pXBTA@9+EnZuL*4Z5J8I}M}2llhVeI4fXBRtTe4x3&KUfXGB$ delta 5523 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 diff --git a/excel_sheets/Table_metadata.xlsx b/excel_sheets/Table_metadata.xlsx index 422a2cc8b69f2563ef8d9f47995d7651a8d6994d..16cc3fc6a170b4fe420dce5360ecd7d11db0d3ff 100644 GIT binary patch delta 1761 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 Z+z|f~4zP=IHCq-1prZV&GNX;*{u@=b6%+sf diff --git a/pydantic_schemas/document_schema.py b/pydantic_schemas/document_schema.py index e15b6ea..e21163f 100644 --- a/pydantic_schemas/document_schema.py +++ b/pydantic_schemas/document_schema.py @@ -4,7 +4,7 @@ from __future__ import annotations from enum import Enum -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Optional from pydantic import Extra, Field @@ -261,44 +261,6 @@ class Tag(SchemaBaseModel): tag_group: Optional[str] = Field(None, title="Tag group") -class ModelInfoItem(SchemaBaseModel): - source: Optional[str] = Field(None, title="Source") - author: Optional[str] = Field(None, title="Author") - version: Optional[str] = Field(None, title="Version") - model_id: Optional[str] = Field(None, title="Model Identifier") - nb_topics: Optional[float] = Field(None, title="Number of topics") - description: Optional[str] = Field(None, title="Description") - corpus: Optional[str] = Field(None, title="Corpus name") - uri: Optional[str] = Field(None, title="URI") - - -class TopicWord(SchemaBaseModel): - word: Optional[str] = Field(None, title="Word") - word_weight: Optional[float] = Field(None, title="Word weight") - - -class TopicDescriptionItem(SchemaBaseModel): - topic_id: Optional[Union[int, str]] = Field(None, title="Topic identifier") - topic_score: Optional[Union[float, str]] = Field(None, title="Topic score") - topic_label: Optional[str] = Field(None, title="Topic label") - topic_words: Optional[List[TopicWord]] = Field(None, description="Words", title="Topic words") - - -class LdaTopic(SchemaBaseModel): - class Config: - extra = Extra.forbid - - model_info: Optional[List[ModelInfoItem]] = Field(None, title="Model information") - topic_description: Optional[List[TopicDescriptionItem]] = Field(None, title="Topic information") - - -class Embedding(SchemaBaseModel): - id: str = Field(..., title="Vector Model ID") - description: Optional[str] = Field(None, title="Vector Model Description") - date: Optional[str] = Field(None, title="Date (YYYY-MM-DD)") - vector: Union[Dict[str, Any], List[Any]] = Field(..., title="Vector") - - class OriginDescription(SchemaBaseModel): harvest_date: Optional[str] = Field(None, description="Harvest date using UTC date format") altered: Optional[bool] = Field( @@ -587,6 +549,4 @@ class ScriptSchemaDraft(SchemaBaseModel): ) provenance: Optional[List[ProvenanceSchema]] = Field(None, description="Provenance") tags: Optional[List[Tag]] = Field(None, description="Tags", title="Tags") - lda_topics: Optional[List[LdaTopic]] = Field(None, description="LDA topics", title="LDA topics") - embeddings: Optional[List[Embedding]] = Field(None, description="Word embeddings", title="Word embeddings") additional: Optional[Dict[str, Any]] = Field(None, description="Additional metadata") diff --git a/pydantic_schemas/generators/generate_excel_files.py b/pydantic_schemas/generators/generate_excel_files.py index d1acbfb..a43725a 100644 --- a/pydantic_schemas/generators/generate_excel_files.py +++ b/pydantic_schemas/generators/generate_excel_files.py @@ -16,9 +16,9 @@ def compare_excel_files(file1, file2): # Check if both workbooks have the same sheets if sheets1 != sheets2: - print("Sheet names do not match") - print(f"File1 sheets: {sheets1}") - print(f"File2 sheets: {sheets2}") + # print("Sheet names do not match") + # print(f"File1 sheets: {sheets1}") + # print(f"File2 sheets: {sheets2}") return False # Iterate through each sheet @@ -62,9 +62,9 @@ def compare_excel_files(file1, file2): differences.append(f"Alignment: {ws1[cell_address].alignment} != {ws2[cell_address].alignment}") if differences: - print(f"Differences found at {sheet_name} {cell_address}:") - for difference in differences: - print(f" - {difference}") + # print(f"Differences found at {sheet_name} {cell_address}:") + # for difference in differences: + # print(f" - {difference}") return False return True @@ -73,8 +73,6 @@ def compare_excel_files(file1, file2): metadata_manager = MetadataManager() for metadata_name in metadata_manager.metadata_type_names: - if metadata_name in ["image", "geospatial"]: - continue filename = f"excel_sheets/{metadata_name.capitalize()}_metadata.xlsx" print(f"Writing {metadata_name} outline to {filename}") if os.path.exists(filename): diff --git a/pydantic_schemas/geospatial_schema.py b/pydantic_schemas/geospatial_schema.py index 3678503..d04b1c7 100644 --- a/pydantic_schemas/geospatial_schema.py +++ b/pydantic_schemas/geospatial_schema.py @@ -4,7 +4,7 @@ from __future__ import annotations from enum import Enum -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Optional from pydantic import Extra, Field, confloat @@ -493,44 +493,6 @@ class Tag(SchemaBaseModel): tag_group: Optional[str] = Field(None, title="Tag group") -class ModelInfoItem(SchemaBaseModel): - source: Optional[str] = Field(None, title="Source") - author: Optional[str] = Field(None, title="Author") - version: Optional[str] = Field(None, title="Version") - model_id: Optional[str] = Field(None, title="Model Identifier") - nb_topics: Optional[float] = Field(None, title="Number of topics") - description: Optional[str] = Field(None, title="Description") - corpus: Optional[str] = Field(None, title="Corpus name") - uri: Optional[str] = Field(None, title="URI") - - -class TopicWord(SchemaBaseModel): - word: Optional[str] = Field(None, title="Word") - word_weight: Optional[float] = Field(None, title="Word weight") - - -class TopicDescriptionItem(SchemaBaseModel): - topic_id: Optional[Union[int, str]] = Field(None, title="Topic identifier") - topic_score: Optional[Union[float, str]] = Field(None, title="Topic score") - topic_label: Optional[str] = Field(None, title="Topic label") - topic_words: Optional[List[TopicWord]] = Field(None, description="Words", title="Topic words") - - -class LdaTopic(SchemaBaseModel): - class Config: - extra = Extra.forbid - - model_info: Optional[List[ModelInfoItem]] = Field(None, title="Model information") - topic_description: Optional[List[TopicDescriptionItem]] = Field(None, title="Topic information") - - -class Embedding(SchemaBaseModel): - id: str = Field(..., title="Vector Model ID") - description: Optional[str] = Field(None, title="Vector Model Description") - date: Optional[str] = Field(None, title="Date (YYYY-MM-DD)") - vector: Dict[str, Any] = Field(..., title="Vector") - - class ResourceSchema(SchemaBaseModel): """ External resource schema @@ -1523,8 +1485,6 @@ class GeospatialSchema(SchemaBaseModel): description: Description = Field(..., title="Geospatial schema") provenance: Optional[List[ProvenanceSchema]] = Field(None, description="Provenance") tags: Optional[List[Tag]] = Field(None, description="Tags", title="Tags") - lda_topics: Optional[List[LdaTopic]] = Field(None, description="LDA topics", title="LDA topics") - embeddings: Optional[List[Embedding]] = Field(None, description="Word embeddings", title="Word embeddings") additional: Optional[Dict[str, Any]] = Field( None, description="Any additional metadata", title="Additional metadata" ) diff --git a/pydantic_schemas/image_schema.py b/pydantic_schemas/image_schema.py index a586e22..a71d648 100644 --- a/pydantic_schemas/image_schema.py +++ b/pydantic_schemas/image_schema.py @@ -5,9 +5,9 @@ from datetime import datetime from enum import Enum -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Optional -from pydantic import AnyUrl, Extra, Field, confloat, constr +from pydantic import AnyUrl, Extra, Field, confloat from .utils.schema_base_model import SchemaBaseModel @@ -71,44 +71,6 @@ class Tag(SchemaBaseModel): tag_group: Optional[str] = Field(None, title="Tag group") -class ModelInfoItem(SchemaBaseModel): - source: Optional[str] = Field(None, title="Source") - author: Optional[str] = Field(None, title="Author") - version: Optional[str] = Field(None, title="Version") - model_id: Optional[str] = Field(None, title="Model Identifier") - nb_topics: Optional[float] = Field(None, title="Number of topics") - description: Optional[str] = Field(None, title="Description") - corpus: Optional[str] = Field(None, title="Corpus name") - uri: Optional[str] = Field(None, title="URI") - - -class TopicWord(SchemaBaseModel): - word: Optional[str] = Field(None, title="Word") - word_weight: Optional[float] = Field(None, title="Word weight") - - -class TopicDescriptionItem(SchemaBaseModel): - topic_id: Optional[Union[int, str]] = Field(None, title="Topic identifier") - topic_score: Optional[Union[float, str]] = Field(None, title="Topic score") - topic_label: Optional[str] = Field(None, title="Topic label") - topic_words: Optional[List[TopicWord]] = Field(None, description="Words", title="Topic words") - - -class LdaTopic(SchemaBaseModel): - class Config: - extra = Extra.forbid - - model_info: Optional[List[ModelInfoItem]] = Field(None, title="Model information") - topic_description: Optional[List[TopicDescriptionItem]] = Field(None, title="Topic information") - - -class Embedding(SchemaBaseModel): - id: str = Field(..., title="Vector Model ID") - description: Optional[str] = Field(None, title="Vector Model Description") - date: Optional[str] = Field(None, title="Date (YYYY-MM-DD)") - vector: Dict[str, Any] = Field(..., title="Vector") - - class SceneCodesLabelledItem(SchemaBaseModel): code: Optional[str] = Field(None, description="Scene code as a string of 6 digits", title="Scene Code") label: Optional[str] = Field(None, description="Label", title="Scene Label") @@ -139,18 +101,6 @@ class Config: description: Optional[str] = None -class AltLangObject(SchemaBaseModel): - class Config: - extra = Extra.forbid - - __root__: Dict[ - constr( - regex=r"^(((([A-Za-z]{2,3}(-([A-Za-z]{3}(-[A-Za-z]{3}){0,2}))?)|[A-Za-z]{4}|[A-Za-z]{5,8})(-([A-Za-z]{4}))?(-([A-Za-z]{2}|[0-9]{3}))?(-([A-Za-z0-9]{5,8}|[0-9][A-Za-z0-9]{3}))*(-([0-9A-WY-Za-wy-z](-[A-Za-z0-9]{2,8})+))*(-(x(-[A-Za-z0-9]{1,8})+))?)|(x(-[A-Za-z0-9]{1,8})+))$" - ), - str, - ] = Field(..., description="Text in alternative languages") - - class ArtworkOrObject(SchemaBaseModel): class Config: extra = Extra.forbid @@ -1176,6 +1126,4 @@ class ImageDataTypeSchema(SchemaBaseModel): image_description: Optional[ImageDescription] = None provenance: Optional[List[ProvenanceSchema]] = Field(None, description="Provenance") tags: Optional[List[Tag]] = Field(None, description="Tags", title="Tags") - lda_topics: Optional[List[LdaTopic]] = Field(None, description="LDA topics", title="LDA topics") - embeddings: Optional[List[Embedding]] = Field(None, description="Word embeddings", title="Word embeddings") additional: Optional[Dict[str, Any]] = Field(None, description="Additional metadata") diff --git a/pydantic_schemas/metadata_manager.py b/pydantic_schemas/metadata_manager.py index 70f9c69..acd9434 100644 --- a/pydantic_schemas/metadata_manager.py +++ b/pydantic_schemas/metadata_manager.py @@ -4,9 +4,10 @@ from openpyxl import load_workbook from pydantic import BaseModel -from . import ( # image_schema, +from . import ( document_schema, geospatial_schema, + image_schema, indicator_schema, indicators_db_schema, microdata_schema, @@ -32,7 +33,7 @@ class MetadataManager: _TYPE_TO_SCHEMA = { "document": document_schema.ScriptSchemaDraft, "geospatial": geospatial_schema.GeospatialSchema, - # "image":image_schema.ImageDataTypeSchema, + "image": image_schema.ImageDataTypeSchema, "resource": resource_schema.Model, "script": script_schema.ResearchProjectSchemaDraft, "microdata": microdata_schema.MicrodataSchema, @@ -44,8 +45,8 @@ class MetadataManager: _TYPE_TO_WRITER = { "document": write_across_many_sheets, - # "geospatial":, - # "image":, + "geospatial": write_across_many_sheets, + "image": write_across_many_sheets, "resource": write_to_single_sheet, "script": write_across_many_sheets, "microdata": write_across_many_sheets, @@ -57,8 +58,8 @@ class MetadataManager: _TYPE_TO_READER = { "document": excel_doc_to_pydantic, - # "geospatial":, - # "image":, + "geospatial": excel_doc_to_pydantic, + "image": excel_doc_to_pydantic, "resource": excel_single_sheet_to_pydantic, "script": excel_doc_to_pydantic, "microdata": excel_doc_to_pydantic, @@ -126,8 +127,8 @@ def write_metadata_outline_to_excel( """ if isinstance(metadata_name_or_class, str): metadata_name = self.standardize_metadata_name(metadata_name_or_class) - if metadata_name == "geospatial": - raise NotImplementedError("Geospatial schema contains an infinite loop so cannot be written to excel") + # if metadata_name == "geospatial": + # raise NotImplementedError("Geospatial schema contains an infinite loop so cannot be written to excel") skeleton_object = self.create_metadata_outline(metadata_name, debug=False) writer = self._TYPE_TO_WRITER[metadata_name] if filename is None: @@ -154,6 +155,7 @@ def save_metadata_to_excel( object: BaseModel, filename: Optional[str] = None, title: Optional[str] = None, + verbose: bool = False, ) -> str: """ Save an Excel document of the given metadata object. @@ -176,8 +178,8 @@ def save_metadata_to_excel( """ if isinstance(metadata_name_or_class, str): metadata_name = self.standardize_metadata_name(metadata_name_or_class) - if metadata_name == "geospatial": - raise NotImplementedError("Geospatial schema contains an infinite loop so cannot be written to excel") + # if metadata_name == "geospatial": + # raise NotImplementedError("Geospatial schema contains an infinite loop so cannot be written to excel") schema = self.metadata_class_from_name(metadata_name) writer = self._TYPE_TO_WRITER[metadata_name] else: @@ -195,13 +197,11 @@ def save_metadata_to_excel( combined_dict = merge_dicts( skeleton_object.model_dump(), - object.model_dump(exclude_none=True, exclude_unset=True, exclude_defaults=True), + object.model_dump(exclude_none=False, exclude_unset=True, exclude_defaults=True), ) combined_dict = standardize_keys_in_dict(combined_dict) - new_ob = schema(**combined_dict) - - # writer = self._TYPE_TO_WRITER[metadata_name] - writer(filename, new_ob, metadata_name, title) + new_ob = schema.model_validate(combined_dict) + writer(filename, new_ob, metadata_name, title, verbose=verbose) return filename @staticmethod @@ -231,7 +231,9 @@ def _get_metadata_name_from_excel_file(filename: str) -> str: return cell_values[0] - def read_metadata_from_excel(self, filename: str, metadata_class: Optional[Type[BaseModel]] = None) -> BaseModel: + def read_metadata_from_excel( + self, filename: str, metadata_class: Optional[Type[BaseModel]] = None, verbose: bool = False + ) -> BaseModel: """ Read in metadata from an appropriately formatted Excel file as a pydantic object. If using standard metadata types (document, 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. @@ -255,25 +257,23 @@ def read_metadata_from_excel(self, filename: str, metadata_class: Optional[Type[ ) from e schema = metadata_class reader = excel_single_sheet_to_pydantic - read_object = reader(filename, schema) + read_object = reader(filename, schema, verbose=verbose) skeleton_object = self.create_metadata_outline(metadata_name_or_class=schema, debug=False) - read_object_dict = read_object.model_dump(exclude_none=True, exclude_unset=True, exclude_defaults=True) + read_object_dict = read_object.model_dump(exclude_none=False, exclude_unset=True, exclude_defaults=True) combined_dict = merge_dicts( skeleton_object.model_dump(), read_object_dict, ) combined_dict = standardize_keys_in_dict(combined_dict) - new_ob = schema(**combined_dict) + new_ob = schema.model_validate(combined_dict) return new_ob def _raise_if_unsupported_metadata_name(self, metadata_name: str): """ - If the type is specifically unsupported - geospatial or image - a NotImplementedError is raised + If the type is specifically unsupported a NotImplementedError is raised If the type is simply unknown then a ValueError is raised. """ - if metadata_name == "image": - raise NotImplementedError("Due to an issue with image metadata schema definition causing __root__ errors") if metadata_name not in self._TYPE_TO_SCHEMA.keys(): raise ValueError(f"'{metadata_name}' not supported. Must be: {list(self._TYPE_TO_SCHEMA.keys())}") diff --git a/pydantic_schemas/microdata_schema.py b/pydantic_schemas/microdata_schema.py index 036a238..fdf0c0a 100644 --- a/pydantic_schemas/microdata_schema.py +++ b/pydantic_schemas/microdata_schema.py @@ -38,44 +38,6 @@ class Tag(SchemaBaseModel): tag_group: Optional[str] = Field(None, title="Tag group") -class ModelInfoItem(SchemaBaseModel): - source: Optional[str] = Field(None, title="Source") - author: Optional[str] = Field(None, title="Author") - version: Optional[str] = Field(None, title="Version") - model_id: Optional[str] = Field(None, title="Model Identifier") - nb_topics: Optional[float] = Field(None, title="Number of topics") - description: Optional[str] = Field(None, title="Description") - corpus: Optional[str] = Field(None, title="Corpus name") - uri: Optional[str] = Field(None, title="URI") - - -class TopicWord(SchemaBaseModel): - word: Optional[str] = Field(None, title="Word") - word_weight: Optional[float] = Field(None, title="Word weight") - - -class TopicDescriptionItem(SchemaBaseModel): - topic_id: Optional[Union[int, str]] = Field(None, title="Topic identifier") - topic_score: Optional[Union[float, str]] = Field(None, title="Topic score") - topic_label: Optional[str] = Field(None, title="Topic label") - topic_words: Optional[List[TopicWord]] = Field(None, description="Words", title="Topic words") - - -class LdaTopic(SchemaBaseModel): - class Config: - extra = Extra.forbid - - model_info: Optional[List[ModelInfoItem]] = Field(None, title="Model information") - topic_description: Optional[List[TopicDescriptionItem]] = Field(None, title="Topic information") - - -class Embedding(SchemaBaseModel): - id: str = Field(..., title="Vector Model ID") - description: Optional[str] = Field(None, title="Vector Model Description") - date: Optional[str] = Field(None, title="Date (YYYY-MM-DD)") - vector: Dict[str, Any] = Field(..., title="Vector") - - class DatafileSchema(SchemaBaseModel): file_id: str = Field(..., title="File unique ID") file_name: str = Field(..., title="File name") @@ -1440,6 +1402,4 @@ class MicrodataSchema(DdiSchema): overwrite: Optional[Overwrite] = Field("no", description="Overwrite survey if already exists?") provenance: Optional[List[ProvenanceSchema]] = Field(None, description="Provenance") tags: Optional[List[Tag]] = Field(None, description="Tags", title="Tags (user-defined)") - lda_topics: Optional[List[LdaTopic]] = Field(None, description="LDA topics", title="LDA topics") - embeddings: Optional[List[Embedding]] = Field(None, description="Word embeddings", title="Word embeddings") additional: Optional[Dict[str, Any]] = Field(None, description="Additional metadata not covered by DDI elements") diff --git a/pydantic_schemas/script_schema.py b/pydantic_schemas/script_schema.py index 6af04b5..74b592a 100644 --- a/pydantic_schemas/script_schema.py +++ b/pydantic_schemas/script_schema.py @@ -4,7 +4,7 @@ from __future__ import annotations from enum import Enum -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Optional from pydantic import Extra, Field @@ -594,44 +594,6 @@ class Tag(SchemaBaseModel): tag_group: Optional[str] = Field(None, title="Tag group") -class ModelInfoItem(SchemaBaseModel): - source: Optional[str] = Field(None, title="Source") - author: Optional[str] = Field(None, title="Author") - version: Optional[str] = Field(None, title="Version") - model_id: Optional[str] = Field(None, title="Model Identifier") - nb_topics: Optional[float] = Field(None, title="Number of topics") - description: Optional[str] = Field(None, title="Description") - corpus: Optional[str] = Field(None, title="Corpus name") - uri: Optional[str] = Field(None, title="URI") - - -class TopicWord(SchemaBaseModel): - word: Optional[str] = Field(None, title="Word") - word_weight: Optional[float] = Field(None, title="Word weight") - - -class TopicDescriptionItem(SchemaBaseModel): - topic_id: Optional[Union[int, str]] = Field(None, title="Topic identifier") - topic_score: Optional[Union[float, str]] = Field(None, title="Topic score") - topic_label: Optional[str] = Field(None, title="Topic label") - topic_words: Optional[List[TopicWord]] = Field(None, description="Words", title="Topic words") - - -class LdaTopic(SchemaBaseModel): - class Config: - extra = Extra.forbid - - model_info: Optional[List[ModelInfoItem]] = Field(None, title="Model information") - topic_description: Optional[List[TopicDescriptionItem]] = Field(None, title="Topic information") - - -class Embedding(SchemaBaseModel): - id: str = Field(..., title="Vector Model ID") - description: Optional[str] = Field(None, title="Vector Model Description") - date: Optional[str] = Field(None, title="Date (YYYY-MM-DD)") - vector: Dict[str, Any] = Field(..., title="Vector") - - class OriginDescription(SchemaBaseModel): harvest_date: Optional[str] = Field(None, description="Harvest date using UTC date format") altered: Optional[bool] = Field( @@ -681,6 +643,4 @@ class ResearchProjectSchemaDraft(SchemaBaseModel): ) provenance: Optional[List[ProvenanceSchema]] = Field(None, description="Provenance") tags: Optional[List[Tag]] = Field(None, description="Tags", title="Tags (user-defined)") - lda_topics: Optional[List[LdaTopic]] = Field(None, description="LDA topics", title="LDA topics") - embeddings: Optional[List[Embedding]] = Field(None, description="Word embeddings", title="Word embeddings") additional: Optional[Dict[str, Any]] = Field(None, description="Additional metadata") diff --git a/pydantic_schemas/table_schema.py b/pydantic_schemas/table_schema.py index 4ea8836..172b401 100644 --- a/pydantic_schemas/table_schema.py +++ b/pydantic_schemas/table_schema.py @@ -4,7 +4,7 @@ from __future__ import annotations from enum import Enum -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Optional from pydantic import Extra, Field @@ -301,44 +301,6 @@ class Tag(SchemaBaseModel): tag_group: Optional[str] = Field(None, title="Tag group") -class ModelInfoItem(SchemaBaseModel): - source: Optional[str] = Field(None, title="Source") - author: Optional[str] = Field(None, title="Author") - version: Optional[str] = Field(None, title="Version") - model_id: Optional[str] = Field(None, title="Model Identifier") - nb_topics: Optional[float] = Field(None, title="Number of topics") - description: Optional[str] = Field(None, title="Description") - corpus: Optional[str] = Field(None, title="Corpus name") - uri: Optional[str] = Field(None, title="URI") - - -class TopicWord(SchemaBaseModel): - word: Optional[str] = Field(None, title="Word") - word_weight: Optional[float] = Field(None, title="Word weight") - - -class TopicDescriptionItem(SchemaBaseModel): - topic_id: Optional[Union[int, str]] = Field(None, title="Topic identifier") - topic_score: Optional[Union[float, str]] = Field(None, title="Topic score") - topic_label: Optional[str] = Field(None, title="Topic label") - topic_words: Optional[List[TopicWord]] = Field(None, description="Words", title="Topic words") - - -class LdaTopic(SchemaBaseModel): - class Config: - extra = Extra.forbid - - model_info: Optional[List[ModelInfoItem]] = Field(None, title="Model information") - topic_description: Optional[List[TopicDescriptionItem]] = Field(None, title="Topic information") - - -class Embedding(SchemaBaseModel): - id: str = Field(..., title="Vector Model ID") - description: Optional[str] = Field(None, title="Vector Model Description") - date: Optional[str] = Field(None, title="Date (YYYY-MM-DD)") - vector: Dict[str, Any] = Field(..., title="Vector") - - class OriginDescription(SchemaBaseModel): harvest_date: Optional[str] = Field(None, description="Harvest date using UTC date format") altered: Optional[bool] = Field( @@ -507,6 +469,4 @@ class Model(SchemaBaseModel): ) provenance: Optional[List[ProvenanceSchema]] = Field(None, description="Provenance") tags: Optional[List[Tag]] = Field(None, description="Tags", title="Tags") - lda_topics: Optional[List[LdaTopic]] = Field(None, description="LDA topics", title="LDA topics") - embeddings: Optional[List[Embedding]] = Field(None, description="Word embeddings", title="Word embeddings") additional: Optional[Dict[str, Any]] = Field(None, description="Additional metadata") diff --git a/pydantic_schemas/tests/test_metadata_manager.py b/pydantic_schemas/tests/test_metadata_manager.py index 426ab84..40a0b26 100644 --- a/pydantic_schemas/tests/test_metadata_manager.py +++ b/pydantic_schemas/tests/test_metadata_manager.py @@ -1,10 +1,147 @@ +import random +import string +from copy import copy + import pytest +from pydantic import BaseModel, ValidationError +from utils.quick_start import make_skeleton from pydantic_schemas.metadata_manager import MetadataManager +# Function to generate a random 4-character string +def random_string(length=4): + return "".join(random.choices(string.ascii_letters, k=length)) + + +# Recursive function to traverse and replace Nones or empty strings +def replace_nones_with_random(model: BaseModel): + assert isinstance(model, BaseModel), model + for field_name, field_value in model.__dict__.items(): + # If the field is None or an empty string, replace it with a random string + if field_value is None or field_value == "": + try: + show = field_value is not None or random.random() < 0.7 + setattr(model, field_name, random_string() if show else None) + except ValidationError: + continue + # If the field is another Pydantic model, recursively apply the function + elif isinstance(field_value, BaseModel): + replace_nones_with_random(field_value) + # If the field is a list of models, apply the function to each item + elif isinstance(field_value, list): + n_elements = random.choices([1, 4, 8])[0] + non_null_values = [random.random() < 0.7 for _ in range(n_elements)] + if not any(non_null_values): + continue + elif len(field_value) == 0: + try: + setattr( + model, field_name, [random_string() if non_null_values[i] else None for i in range(n_elements)] + ) + except ValidationError: + continue + elif isinstance(field_value[0], BaseModel): + try: + new_vals = [copy(field_value[0]) for i in range(n_elements)] + for v in new_vals: + replace_nones_with_random(v) + setattr( + model, + field_name, + new_vals, + ) + except ValidationError as e: + raise ValueError(f"{field_name}, {new_vals}") from e + # continue + else: + continue + # for item in field_value: + # if isinstance(item, BaseModel): + # replace_nones_with_random(item) + # If the field is a dict, apply the function to each value + elif isinstance(field_value, dict): + for key, item in field_value.items(): + if isinstance(item, BaseModel): + replace_nones_with_random(item) + + +def is_empty(m): + if isinstance(m, BaseModel): + iterabl = [v for _, v in m.model_dump().items()] + elif isinstance(m, dict): + if len(m) == 0: + return True + iterabl = [v for _, v in m.items()] + elif isinstance(m, list): + if len(m) == 0: + return True + iterabl = m + else: + return m is None + + for v in iterabl: + if isinstance(v, dict) or isinstance(v, BaseModel) or isinstance(v, list): + if is_empty(v) == False: + return False + elif v is not None: + return False + return True + + +# Recursive function to compare two Pydantic models +def compare_pydantic_models(model1: BaseModel, model2: BaseModel) -> bool: + # First, check if the two models are of the same type + if type(model1) is not type(model2): + assert False + + if not hasattr(model1, "model_fields"): + assert model1 == model2 + + # Traverse through the fields of the model + for field_name in model1.model_fields: + value1 = getattr(model1, field_name) + value2 = getattr(model2, field_name) + + # If values are different, return False + if value1 != value2: + # If both are BaseModel instances, compare recursively + if isinstance(value1, BaseModel) and isinstance(value2, BaseModel): + if not compare_pydantic_models(value1, value2): + assert False, field_name + # If both are lists, compare their elements + elif isinstance(value1, list) and isinstance(value2, list): + value1 = [v for v in value1 if is_empty(v) == False] + value2 = [v for v in value2 if is_empty(v) == False] + # remove empty basemodels + + assert len(value1) == len(value2) + for v1, v2 in zip(value1, value2): + if isinstance(v1, BaseModel) and isinstance(v2, BaseModel): + if not compare_pydantic_models(v1, v2): + assert False, field_name + elif v1 != v2: + assert False, field_name + elif isinstance(value1, list) and value2 is None: + continue + # If both are dicts, compare their items + elif isinstance(value1, dict) and isinstance(value2, dict): + assert value1.keys() == value2.keys() + for key in value1: + if isinstance(value1[key], BaseModel) and isinstance(value2[key], BaseModel): + if not compare_pydantic_models(value1[key], value2[key]): + assert False, field_name + else: + assert value1[key] == value2[key], field_name + else: + assert value1 == value2, field_name # For other types, if they are not equal, return False + + return True # All fields are equal + + @pytest.mark.parametrize( - "metadata_name", ["document", "script", "microdata", "table", "indicators_db", "indicator", "video"] + "metadata_name", + ["document", "script", "microdata", "table", "indicators_db", "indicator", "video", "geospatial", "image"], ) def test_metadata_by_name(tmpdir, metadata_name): mm = MetadataManager() @@ -15,22 +152,38 @@ def test_metadata_by_name(tmpdir, metadata_name): # Write empty metadata filename = mm.write_metadata_outline_to_excel( - metadata_name_or_class=metadata_name, filename=tmpdir.join(f"test_{metadata_name}.xlsx"), title=metadata_name + metadata_name_or_class=metadata_name, + filename=tmpdir.join(f"test_{metadata_name}_outline.xlsx"), + title=metadata_name, ) # Read the metadata back tmp = mm.read_metadata_from_excel(filename=filename) # Save the read metadata to a new file - filename2 = tmpdir.join(f"test_{metadata_name}_2.xlsx") + filename2 = tmpdir.join(f"test_{metadata_name}_save.xlsx") mm.save_metadata_to_excel(metadata_name_or_class=metadata_name, object=tmp, filename=filename2, title=metadata_name) - # make an outline object - mm.create_metadata_outline(metadata_name_or_class=metadata_name) + for i in range(10): + modl = mm.create_metadata_outline(metadata_name_or_class=metadata_name) + replace_nones_with_random(modl) + + # Write filled in metadata + filename3 = tmpdir.join(f"test_{metadata_name}_{i}.xlsx") + # filename3 = f"test_{metadata_name}_{i}.xlsx" + mm.save_metadata_to_excel( + metadata_name_or_class=metadata_name, object=modl, filename=filename3, title=metadata_name + ) + + # Read the metadata back + actual = mm.read_metadata_from_excel(filename=filename3) + compare_pydantic_models(modl, actual) + # assert modl == actual, actual @pytest.mark.parametrize( - "metadata_name", ["document", "script", "microdata", "table", "timeseries_db", "indicator", "video"] + "metadata_name", + ["document", "script", "microdata", "table", "timeseries_db", "indicator", "video", "geospatial", "image"], ) def test_metadata_by_class(tmpdir, metadata_name): mm = MetadataManager() @@ -64,6 +217,8 @@ def test_standardize_metadata_name(): "INdicator", "timeseries", "VIdeo", + "image", + "IMaGe", ] expecteds = [ @@ -79,14 +234,13 @@ def test_standardize_metadata_name(): "indicator", "indicator", "video", + "image", + "image", ] for inp, expected in zip(inputs, expecteds): actual = mm.standardize_metadata_name(inp) assert actual == expected, f"expected {expected} but got {actual}" - with pytest.raises(NotImplementedError): - mm.standardize_metadata_name("Image") - with pytest.raises(ValueError): mm.standardize_metadata_name("Bad-name") diff --git a/pydantic_schemas/tests/test_pydantic_to_excel.py b/pydantic_schemas/tests/test_pydantic_to_excel.py index e5884bc..4a24e11 100644 --- a/pydantic_schemas/tests/test_pydantic_to_excel.py +++ b/pydantic_schemas/tests/test_pydantic_to_excel.py @@ -1,17 +1,16 @@ import os from enum import Enum -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Union import pandas as pd import pytest from pydantic import BaseModel, Field from pydantic_schemas.document_schema import ScriptSchemaDraft +from pydantic_schemas.geospatial_schema import GeospatialSchema +from pydantic_schemas.image_schema import ImageDataTypeSchema from pydantic_schemas.indicator_schema import TimeseriesSchema from pydantic_schemas.indicators_db_schema import TimeseriesDatabaseSchema - -# from pydantic_schemas.definitions.geospatial_schema import GeospatialSchema -# from pydantic_schemas.definitions.image_schema import ImageDataTypeSchema from pydantic_schemas.microdata_schema import MicrodataSchema from pydantic_schemas.script_schema import ResearchProjectSchemaDraft from pydantic_schemas.table_schema import Model as TableModel @@ -263,7 +262,9 @@ class ProductionAndCountries(BaseModel): filename, example_production_and_country, "ProductionAndCountries", "Production and Countries" ) - new_pandc = excel_sheet_to_pydantic(filename=filename, sheetname="metadata", model_type=ProductionAndCountries) + new_pandc = excel_sheet_to_pydantic( + filename=filename, sheetname="metadata", model_type=ProductionAndCountries, debug=True + ) assert new_pandc.production.idno is None assert new_pandc.production.title is None assert len(new_pandc.production.authors) == 4 @@ -324,7 +325,7 @@ class ProductionAndCountries(BaseModel): filename, example_production_and_country, "ProductionAndCountries", "Production and Countries" ) - new_pandc = excel_doc_to_pydantic(filename, ProductionAndCountries) + new_pandc = excel_doc_to_pydantic(filename, ProductionAndCountries, verbose=True) assert new_pandc.production.idno == "myidno" assert new_pandc.production.title is None assert len(new_pandc.production.authors) == 4 @@ -341,6 +342,46 @@ class ProductionAndCountries(BaseModel): assert new_pandc.single_val == "single" +def test_union_list(tmpdir): + class Method(BaseModel): + """ + Methodology and processing + """ + + study_class: Optional[Union[str, List[Any]]] = Field( + None, + description=( + "Generally used to give the data archive's class or study status number, which indicates the processing" + " status of the study. May also be used as a text field to describe processing status. Example: `DDA Class" + " C`, `Study is available from http://example.com` " + ), + title="Class of the Study", + ) + + class StudyDesc(BaseModel): + """ + Study Description + """ + + method: Optional[Method] = Field( + None, description="Methodology and processing", title="Methodology and Processing" + ) + + class MicrodataSchema(BaseModel): + """ + 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"]))) + filename = tmpdir.join(f"integration_test_union_list_.xlsx") + write_across_many_sheets(filename, ms, "UnionList", "Looking at a union with a list") + + parsed_outp = excel_doc_to_pydantic(filename, MicrodataSchema) + assert parsed_outp == ms, parsed_outp + + def test_dictionaries(tmpdir): class SubDict(BaseModel): sub_additional: Optional[Dict[str, Any]] = Field(None, description="Additional metadata at a lower level") @@ -358,19 +399,120 @@ class WithDict(BaseModel): assert parsed_outp == wd, parsed_outp +def test_list_of_lists(tmpdir): + class Citation(BaseModel): + """ + A set of elements to describe a resource citation + """ + + title: Optional[str] = Field(None, description="Resource title", title="Title") + alternateTitle: Optional[List[str]] = Field( + None, description="Resource alternate title", title="Alternate Title" + ) + + class IdentificationInfo(BaseModel): + """ + Identification(s) of the resource + """ + + citation: Optional[Citation] = Field(None, description="Dataset citation", title="Citation") + + class LegalConstraints(BaseModel): + """ + Legal constraints associated to the resource + """ + + useLimitation: Optional[List[str]] = None + accessConstraints: Optional[List[str]] = Field( + None, + description=( + "A restriction to access/use a resource. e.g. 'dataset'. Recommended code following the [ISO/TS" + " 19139](http://standards.iso.org/iso/19139/resources/gmxCodelists.xml#MD_RestrictionCode) Restriction" + " codelist. Suggested values: {`copyright`, `patent`, `patentPending`, `trademark`, `license`," + " `intellectualPropertyRights`, `restricted`, `otherRestrictions`, `unrestricted`, `licenceUnrestricted`," + " `licenceEndUser`, `licenceDistributor`, `private`, `statutory`, `confidential`, `SBU`, `in-confidence`}" + ), + title="Access constraints", + ) + + class Constraints(BaseModel): + """ + Constraints associated to the resource + """ + + legalConstraints: Optional[LegalConstraints] = Field( + None, description="Legal constraints associated to the resource", title="Legal constraints" + ) + + class ServiceIdentification(BaseModel): + """ + Service identification + """ + + restrictions: Optional[List[Constraints]] = Field( + None, description="Constraints associated to the service", title="Service constraints" + ) + + class MetaDataOfVariousHierarchies(BaseModel): + citation: Optional[Citation] = None + identification_info: Optional[IdentificationInfo] = None + lst: Optional[List[str]] = (None,) + service_identification: Optional[ServiceIdentification] = None + + inp = MetaDataOfVariousHierarchies( + citation=Citation(title="topleveltitle", alternateTitle=[]), + identification_info=IdentificationInfo( + citation=Citation(title="citation_title", alternateTitle=["alt_title_1", "alt_title_2"]) + ), + lst=["a", "b", "c"], + service_identification=ServiceIdentification( + restrictions=[ + Constraints(legalConstraints=LegalConstraints(useLimitation=["s1", "s2"], accessConstraints=["s3"])) + ] + ), + ) + + # index = pd.MultiIndex.from_tuples([("identification_info", "citation", "title"), ("identification_info", "citation", "alternateTitle"), ("service_identification", "restrictions", "legalConstraints", "useLimitation"), ("service_identification", "restrictions", "legalConstraints", "accessConstraints")]) + + # expected = pd.DataFrame([["citation_title", None], ["alt_title_1", "alt_title_2"], [[], None], [[], None]], index=index) + + filename = tmpdir.join(f"integration_test_list_of_lists_.xlsx") + # filename = "integration_test_list_of_lists_.xlsx" + if os.path.exists(filename): + os.remove(filename) + write_across_many_sheets(filename, inp, "ListOfLists", "Looking at lists of lists") + + expected = inp + expected.citation.alternateTitle = None + actual = excel_doc_to_pydantic(filename, MetaDataOfVariousHierarchies, verbose=True) + # assert actual == inp, actual + + # outp = pydantic_to_dataframe(inp) + # actual = outp[0] + # list_indices = outp[1] + # enums = outp[2] + assert expected.citation == actual.citation, actual.citation + assert expected.identification_info == actual.identification_info, actual.identification_info + assert expected.service_identification == actual.service_identification, actual.service_identification + assert expected.lst == actual.lst, actual.lst + assert expected == actual, actual + # assert list_indices == [1, 2, 3], list_indices + # assert enums == {}, enums + + NAME_TO_TYPE = { "Document": (ScriptSchemaDraft, write_across_many_sheets, excel_doc_to_pydantic), - # "Geospatial":GeospatialSchema, - # "Image":ImageDataTypeSchema, - "Survey": (MicrodataSchema, write_across_many_sheets, excel_doc_to_pydantic), + "Geospatial": (GeospatialSchema, write_across_many_sheets, excel_doc_to_pydantic), + "Image": (ImageDataTypeSchema, write_across_many_sheets, excel_doc_to_pydantic), + "Microdata": (MicrodataSchema, write_across_many_sheets, excel_doc_to_pydantic), "Script": (ResearchProjectSchemaDraft, write_across_many_sheets, excel_doc_to_pydantic), "Table": (TableModel, write_across_many_sheets, excel_doc_to_pydantic), - "Timeseries_DB": ( + "Indicator_DB": ( TimeseriesDatabaseSchema, write_to_single_sheet, excel_single_sheet_to_pydantic, ), # could be one sheet - "Timeseries": (TimeseriesSchema, write_across_many_sheets, excel_doc_to_pydantic), + "Indicator": (TimeseriesSchema, write_across_many_sheets, excel_doc_to_pydantic), "Video": (VideoModel, write_to_single_sheet, excel_single_sheet_to_pydantic), # could be one sheet } @@ -380,12 +522,14 @@ def test_write_real_skeleton(tmpdir, name, type_writer_reader): type, writer, reader = type_writer_reader # folder = "excel_sheets" filename = os.path.join(tmpdir, f"{name}_metadata.xlsx") + # filename = f"{name}_metadata_real_sckele.xlsx" if os.path.exists(filename): os.remove(filename) ob = make_skeleton(type) writer(filename, ob, name, f"{name} Metadata") - reader(filename, type) + reader(filename, type, verbose=True) + # assert False def test_demo(): @@ -414,6 +558,59 @@ class SubObject(BaseModel): a: str b: str + class Citation(BaseModel): + """ + A set of elements to describe a resource citation + """ + + title: Optional[str] = Field(None, description="Resource title", title="Title") + alternateTitle: Optional[List[str]] = Field( + None, description="Resource alternate title", title="Alternate Title" + ) + + class IdentificationInfo(BaseModel): + """ + Identification(s) of the resource + """ + + citation: Optional[Citation] = Field(None, description="Dataset citation", title="Citation") + + class LegalConstraints(BaseModel): + """ + Legal constraints associated to the resource + """ + + useLimitation: Optional[List[str]] = None + accessConstraints: Optional[List[str]] = Field( + None, + description=( + "A restriction to access/use a resource. e.g. 'dataset'. Recommended code following the [ISO/TS" + " 19139](http://standards.iso.org/iso/19139/resources/gmxCodelists.xml#MD_RestrictionCode) Restriction" + " codelist. Suggested values: {`copyright`, `patent`, `patentPending`, `trademark`, `license`," + " `intellectualPropertyRights`, `restricted`, `otherRestrictions`, `unrestricted`, `licenceUnrestricted`," + " `licenceEndUser`, `licenceDistributor`, `private`, `statutory`, `confidential`, `SBU`, `in-confidence`}" + ), + title="Access constraints", + ) + + class Constraints(BaseModel): + """ + Constraints associated to the resource + """ + + legalConstraints: Optional[LegalConstraints] = Field( + None, description="Legal constraints associated to the resource", title="Legal constraints" + ) + + class ServiceIdentification(BaseModel): + """ + Service identification + """ + + restrictions: Optional[List[Constraints]] = Field( + None, description="Constraints associated to the service", title="Service constraints" + ) + class MetaDataOfVariousHierarchies(BaseModel): idno: Optional[str] = None database_name: Optional[str] = None @@ -423,6 +620,8 @@ class MetaDataOfVariousHierarchies(BaseModel): top_level_optional_list: Optional[List[str]] = None top_level_list_of_pydantic_objects: List[SubObject] dictionary: Dict[str, str] + identification_info: Optional[IdentificationInfo] = None + service_identification: Optional[ServiceIdentification] = None example = MetaDataOfVariousHierarchies( single_level_data=SingleLevelData(title="Metadata demo", author="FirstName LastName"), @@ -435,19 +634,17 @@ class MetaDataOfVariousHierarchies(BaseModel): organization="Example Org", ), top_level_list=["a", "b"], - top_level_list_of_pydantic_objects=[SubObject(a="a", b="b")], + top_level_list_of_pydantic_objects=[SubObject(a="asub", b="b")], dictionary={"example_key": "example_value"}, + identification_info=IdentificationInfo( + citation=Citation(title="citation_title", alternateTitle=["alt_title_1", "alt_title_2"]) + ), + service_identification=ServiceIdentification( + restrictions=[Constraints(legalConstraints=LegalConstraints(useLimitation=[], accessConstraints=[]))] + ), ) if os.path.exists(filename): os.remove(filename) - write_to_single_sheet(filename, example, "MetaDataOfVariousHierarchies", sheet_title) - - # current_row = create_sheet_and_write_title(filename, sheetname, sheet_title) - # current_row = write_nested_simple_pydantic_to_sheet(filename, sheetname, example, current_row + 1) - # worksheet = open_or_create_workbook(filename) - # correct_column_widths(worksheet, sheet_name=sheetname) - # shade_30_rows_and_protect_sheet(worksheet, sheetname, current_row + 1) - # shade_locked_cells(worksheet, sheetname) - # worksheet.save(filename) + write_to_single_sheet(filename, example, "MetaDataOfVariousHierarchies", sheet_title, verbose=True) diff --git a/pydantic_schemas/tests/test_pydantic_to_pandas.py b/pydantic_schemas/tests/test_pydantic_to_pandas.py new file mode 100644 index 0000000..e46696b --- /dev/null +++ b/pydantic_schemas/tests/test_pydantic_to_pandas.py @@ -0,0 +1,318 @@ +from typing import Any, Dict, List, Optional, Union + +import pandas as pd +from pydantic import BaseModel, Field +from utils.pydantic_to_excel import pydantic_to_dataframe +from utils.quick_start import make_skeleton + + +def test_simple(): + class Simple(BaseModel): + idno: str + title: Optional[str] = None + author: str + + simple_original = Simple(idno="AVal", author="CVal") + + expected = pd.DataFrame([["AVal"], [None], ["CVal"]], index=["idno", "title", "author"]) + outp = pydantic_to_dataframe(simple_original) + actual = outp[0] + list_indices = outp[1] + enums = outp[2] + assert expected.equals(actual), actual + assert list_indices == [], list_indices + assert enums == {}, enums + + +def test_simple_list(): + class Simple(BaseModel): + idno: str + title: str + authors: List[str] + + simple_original = Simple(idno="AVal", title="BVal", authors=["CVal"]) + + expected = pd.DataFrame([["AVal"], ["BVal"], ["CVal"]], index=["idno", "title", "authors"]) + outp = pydantic_to_dataframe(simple_original, debug=True) + actual = outp[0] + list_indices = outp[1] + enums = outp[2] + print("actual", actual) + assert expected.equals(actual), actual + assert list_indices == [2], list_indices + assert enums == {}, enums + + class SimpleOptional(BaseModel): + idno: str + title: str + authors: Optional[List[str]] + + simple_original_optional = SimpleOptional(idno="AVal", title="BVal", authors=None) + + expected = pd.DataFrame([["AVal"], ["BVal"], [None]], index=["idno", "title", "authors"]) + outp = pydantic_to_dataframe(simple_original_optional) + actual = outp[0] + list_indices = outp[1] + enums = outp[2] + assert expected.equals(actual), actual + assert list_indices == [2], list_indices + assert enums == {}, enums + + simple_original_empty = SimpleOptional(idno="AVal", title="BVal", authors=[]) + + expected = pd.DataFrame([["AVal"], ["BVal"], [None]], index=["idno", "title", "authors"]) + outp = pydantic_to_dataframe(simple_original_empty) + actual = outp[0] + list_indices = outp[1] + enums = outp[2] + assert expected.equals(actual), actual + assert list_indices == [2], list_indices + assert enums == {}, enums + + +def test_subfield(): + class Production(BaseModel): + idno: str + title: str + author: str + + class Country(BaseModel): + name: str + initials: str + + class ProductionAndCountries(BaseModel): + production: Production + countries: Country + + inp = ProductionAndCountries( + production=Production(idno="AVal", title="BVal", author="CVal"), + countries=Country(name="MyCountry", initials="MC"), + ) + + index = pd.MultiIndex.from_tuples( + [ + ("production", "idno"), + ("production", "title"), + ("production", "author"), + ("countries", "name"), + ("countries", "initials"), + ] + ) + expected = pd.DataFrame([["AVal"], ["BVal"], ["CVal"], ["MyCountry"], ["MC"]], index=index) + outp = pydantic_to_dataframe(inp, debug=True) + actual = outp[0] + list_indices = outp[1] + enums = outp[2] + assert expected.equals(actual), actual + assert list_indices == [] + assert enums == {}, enums + + +def test_sublists(): + class Citation(BaseModel): + """ + A set of elements to describe a resource citation + """ + + title: Optional[str] = Field(None, description="Resource title", title="Title") + alternateTitle: Optional[List[str]] = Field( + None, description="Resource alternate title", title="Alternate Title" + ) + + class IdentificationInfo(BaseModel): + """ + Identification(s) of the resource + """ + + citation: Optional[Citation] = Field(None, description="Dataset citation", title="Citation") + + class MetaDataOfVariousHierarchies(BaseModel): + citation: Optional[Citation] = None + identification_info: Optional[IdentificationInfo] = None + lst: Optional[List[str]] = None + + inp = MetaDataOfVariousHierarchies( + citation=Citation(title="topleveltitle", alternateTitle=[]), + identification_info=IdentificationInfo( + citation=Citation(title="citation_title", alternateTitle=["alt_title_1", "alt_title_2"]) + ), + lst=["a", "b", "c"], + ) + + index = pd.MultiIndex.from_tuples( + [ + ("lst",), + ("citation", "title"), + ("citation", "alternateTitle"), + ("identification_info", "citation", "title"), + ("identification_info", "citation", "alternateTitle"), + ] + ) + expected = pd.DataFrame( + [["a", "b", "c"], ["topleveltitle"], [], ["citation_title"], ["alt_title_1", "alt_title_2"]], index=index + ) + outp = pydantic_to_dataframe(inp, debug=True) + actual = outp[0] + list_indices = outp[1] + enums = outp[2] + assert "lst" in actual.index + assert "citation" in actual.index + assert "identification_info" in actual.index + print("Gordon", expected.loc["lst"]) + print(actual.loc["lst"]) + assert expected.loc["lst"].equals(actual.loc["lst"]), actual.loc["lst"] + assert expected.equals(actual), actual + assert list_indices == [0, 2, 4], list_indices + assert enums == {}, enums + + +def test_list_of_lists(): + class Citation(BaseModel): + """ + A set of elements to describe a resource citation + """ + + title: Optional[str] = Field(None, description="Resource title", title="Title") + alternateTitle: Optional[List[str]] = Field( + None, description="Resource alternate title", title="Alternate Title" + ) + + class IdentificationInfo(BaseModel): + """ + Identification(s) of the resource + """ + + citation: Optional[Citation] = Field(None, description="Dataset citation", title="Citation") + + class LegalConstraints(BaseModel): + """ + Legal constraints associated to the resource + """ + + useLimitation: Optional[List[str]] = None + accessConstraints: Optional[List[str]] = Field( + None, + description=( + "A restriction to access/use a resource. e.g. 'dataset'. Recommended code following the [ISO/TS" + " 19139](http://standards.iso.org/iso/19139/resources/gmxCodelists.xml#MD_RestrictionCode) Restriction" + " codelist. Suggested values: {`copyright`, `patent`, `patentPending`, `trademark`, `license`," + " `intellectualPropertyRights`, `restricted`, `otherRestrictions`, `unrestricted`, `licenceUnrestricted`," + " `licenceEndUser`, `licenceDistributor`, `private`, `statutory`, `confidential`, `SBU`, `in-confidence`}" + ), + title="Access constraints", + ) + + class Constraints(BaseModel): + """ + Constraints associated to the resource + """ + + legalConstraints: Optional[LegalConstraints] = Field( + None, description="Legal constraints associated to the resource", title="Legal constraints" + ) + + class ServiceIdentification(BaseModel): + """ + Service identification + """ + + restrictions: Optional[List[Constraints]] = Field( + None, description="Constraints associated to the service", title="Service constraints" + ) + + class MetaDataOfVariousHierarchies(BaseModel): + # citation: Optional[Citation] = None + identification_info: Optional[IdentificationInfo] = None + # lst: Optional[List[str]] = None, + service_identification: Optional[ServiceIdentification] = None + + inp = MetaDataOfVariousHierarchies( + # citation = Citation(title="topleveltitle", alternateTitle=[]), + identification_info=IdentificationInfo( + citation=Citation(title="citation_title", alternateTitle=["alt_title_1", "alt_title_2"]) + ), + # lst = ["a", 'b', 'c'], + service_identification=ServiceIdentification( + restrictions=[Constraints(legalConstraints=LegalConstraints(useLimitation=[], accessConstraints=[]))] + ), + ) + + index = pd.MultiIndex.from_tuples( + [ + ("identification_info", "citation", "title"), + ("identification_info", "citation", "alternateTitle"), + ("service_identification", "restrictions", "legalConstraints", "useLimitation"), + ("service_identification", "restrictions", "legalConstraints", "accessConstraints"), + ] + ) + + expected = pd.DataFrame( + [["citation_title", None], ["alt_title_1", "alt_title_2"], [[], None], [[], None]], index=index + ) + + outp = pydantic_to_dataframe(inp) + actual = outp[0] + list_indices = outp[1] + enums = outp[2] + assert expected.loc["identification_info"].equals(actual.loc["identification_info"]), actual.loc[ + "identification_info" + ] + assert expected.loc["service_identification"].equals(actual.loc["service_identification"]), actual.loc[ + "service_identification" + ] + assert expected.equals(actual), actual + assert list_indices == [1, 2, 3], list_indices + assert enums == {}, enums + + +def test_dictionary(): + class Embedding(BaseModel): + id: str = Field(..., title="Vector Model ID") + description: Optional[str] = Field(None, title="Vector Model Description") + date: Optional[str] = Field(None, title="Date (YYYY-MM-DD)") + vector: Union[Dict[str, Any], List[Any]] = Field(..., title="Vector") + + emb = make_skeleton(Embedding) + df, _, _ = pydantic_to_dataframe(emb, debug=True) + + emb = Embedding(id="sjc", description="ekjrv", date="2024-01-01", vector={"1": "a", "2": "b"}) + df, _, _ = pydantic_to_dataframe(emb, debug=True) + assert df.loc["id"].values[0][0] == "sjc", df.loc["id"] + assert df.loc["description"].values[0][0] == "ekjrv", df.loc["description"] + assert df.loc["date"].values[0][0] == "2024-01-01", df.loc["date"] + assert df.loc["vector"].loc["key"].values[0] == "1", df.loc["vector"].loc["key"] + assert df.loc["vector"].loc["key"].values[1] == "2", df.loc["vector"].loc["key"] + assert df.loc["vector"].loc["value"].values[0] == "a", df.loc["vector"].loc["value"] + assert df.loc["vector"].loc["value"].values[1] == "b", df.loc["vector"].loc["value"] + + emb = Embedding(id="sjc", description="ekjrv", date="2024-01-01", vector=[1, 2, 3]) + df, _, _ = pydantic_to_dataframe(emb, debug=True) + assert df.loc["id"].values[0] == "sjc", df.loc["id"] + assert df.loc["description"].values[0] == "ekjrv", df.loc["description"] + assert df.loc["date"].values[0] == "2024-01-01", df.loc["date"] + assert df.loc["vector"].values[0] == 1, df.loc["vector"] + assert df.loc["vector"].values[1] == 2, df.loc["vector"] + assert df.loc["vector"].values[2] == 3, df.loc["vector"] + + # # lists of embeddings + # TODO make a list of dicts work + # class Parent(BaseModel): + # embeddings: Optional[List[Embedding]] = Field(None, description="Word embeddings", title="Word embeddings") + + # emb = make_skeleton(Parent) + # df, _, _ = pydantic_to_dataframe(emb, debug=True) + + # emb = Parent(embeddings=[Embedding(id="sjc", description="ekjrv", date="2024-01-01", vector={"1": "a", "2": "b"})]) + # df, _, _ = pydantic_to_dataframe(emb, debug=True) + # assert df.loc["embeddings"].loc["id"].values[0][0] == "sjc", df.loc["embeddings"].loc["id"] + # assert df.loc["embeddings"].loc["description"].values[0][0] == "ekjrv", df.loc["embeddings"].loc["description"] + # assert df.loc["embeddings"].loc["date"].values[0][0] == "2024-01-01", df.loc["embeddings"].loc["date"] + # # assert False, df.loc["embeddings"] + # assert df.loc["embeddings"].loc["vector"].loc["key"].values[0] == "1", df.loc["embeddings"].loc["vector"].loc["key"] + # assert df.loc["embeddings"].loc["vector"].loc["key"].values[1] == "2", df.loc["embeddings"].loc["vector"].loc["key"] + # assert df.loc["embeddings"].loc["vector"].loc["value"].values[0] == "a", ( + # df.loc["embeddings"].loc["vector"].loc["value"] + # ) + # assert df.loc["embeddings"].loc["vector"].loc["value"].values[1] == "b", ( + # df.loc["embeddings"].loc["vector"].loc["value"] + # ) diff --git a/pydantic_schemas/tests/test_quick_start.py b/pydantic_schemas/tests/test_quick_start.py index 9d53dc2..8dd77fa 100644 --- a/pydantic_schemas/tests/test_quick_start.py +++ b/pydantic_schemas/tests/test_quick_start.py @@ -235,6 +235,28 @@ class BadFieldNames(BaseModel): assert actual == expected, actual +def test_limit_on_recurrence(tmpdir): + class Production(BaseModel): + idno: Optional[str] = None + title: Optional[str] = None + subtitle: Optional[str] = None + author: str + productions: Optional["Production"] = None # Forward reference + + Production.model_rebuild() + ob = make_skeleton(Production) + + class ProductionWithList(BaseModel): + idno: Optional[str] = None + title: Optional[str] = None + subtitle: Optional[str] = None + author: str + productions: Optional[List["Production"]] = None # Forward reference + + ProductionWithList.model_rebuild() + ob = make_skeleton(ProductionWithList) + + @pytest.mark.parametrize("n", [n for n in MetadataManager().metadata_type_names]) def test_actual_schemas(n): if n == "geospatial": diff --git a/pydantic_schemas/utils/excel_to_pydantic.py b/pydantic_schemas/utils/excel_to_pydantic.py index 124b2e7..aef3fd2 100644 --- a/pydantic_schemas/utils/excel_to_pydantic.py +++ b/pydantic_schemas/utils/excel_to_pydantic.py @@ -1,10 +1,11 @@ import json import warnings -from typing import Any, List, Optional, Type, Union, get_args +from typing import Annotated, Any, List, Optional, Type, Union, get_args, get_origin import numpy as np import pandas as pd from pydantic import BaseModel, create_model +from utils.pydantic_to_excel import pydantic_to_dataframe from .quick_start import make_skeleton from .utils import ( @@ -13,7 +14,9 @@ is_dict_annotation, is_list_annotation, is_optional_annotation, + is_optional_list, seperate_simple_from_pydantic, + standardize_keys_in_dict, subset_pydantic_model_type, ) @@ -72,11 +75,15 @@ def get_relevant_sub_frame(m: Type[BaseModel], df: pd.DataFrame, name_of_field: THis function obtains only that information that pertains to this model """ names = df.iloc[:, 0].values + if debug: + print(f"getting subframe for {m} or {name_of_field} given {names}") try: - name_of_class = m.model_json_schema()["title"] - + json_schema = m.model_json_schema() + if debug: + print(f"get relevant sub frame using json schema: {json_schema}") + name_of_class = json_schema["title"] idx, sze = find_string_and_count_nans(names, name_of_class) - except AttributeError: + except (AttributeError, KeyError): idx = -1 sze = 0 if idx < 0: @@ -88,9 +95,13 @@ def get_relevant_sub_frame(m: Type[BaseModel], df: pd.DataFrame, name_of_field: error_message += f"and '{name_of_field}' " error_message += f"not found in {names}" raise IndexError(error_message) + else: + if debug: + print(f"get relevant sub frame sze={sze}, idx={idx}") sub = df.iloc[idx : idx + sze + 1, 1:] - + if debug: + print(sub) sub = sub.dropna(how="all", axis=0) # drop all null rows sub = sub.dropna(how="all", axis=1) # drop all null columns if debug: @@ -104,7 +115,10 @@ def handle_optional(name, annotation, df, from_within_list: bool = False, debug= args = [a for a in get_args(annotation) if a is not type(None)] # assert len(args) == 1, f"handle_optional encountered {args}" if len(args) > 1: - if str in args: + list_args = [a for a in args if is_list_annotation(a)] + if len(list_args): + arg = list_args[0] + elif str in args: arg = str elif float in args: arg = float @@ -112,7 +126,7 @@ def handle_optional(name, annotation, df, from_within_list: bool = False, debug= arg = args[0] else: arg = args[0] - ret = annotation_switch(name, arg, df, from_within_list=from_within_list) + ret = annotation_switch(name, arg, df, from_within_list=from_within_list, debug=debug) if debug: print(f"optional ret: {ret}") print(f"isinstance(ret, list): {isinstance(ret, list)}") @@ -127,19 +141,36 @@ def handle_optional(name, annotation, df, from_within_list: bool = False, debug= def handle_list(name, anno, df, debug=False): subtype = get_subtype_of_optional_or_list(anno) + if debug: + print(f"handle_list found subtype: {subtype} from {anno} with name {name}\n{df}") if isinstance(subtype, type(BaseModel)): try: - subframe = get_relevant_sub_frame(subtype, df, name_of_field=name) + subframe = get_relevant_sub_frame(subtype, df, name_of_field=name, debug=debug) + if debug: + print(f"subframe\n{subframe}") except IndexError: return [] list_of_subs = [] - for c in subframe.columns[1:]: - subsubframe = subframe.loc[:, [subframe.columns[0], c]] + if debug: + print("handle list df received") + print(subframe) + print("handle list df expected except for the specific values") + print(pydantic_to_dataframe([make_skeleton(subtype)])[0]) + index_size = max( + [len(x) if isinstance(x, tuple) else 1 for x in pydantic_to_dataframe([make_skeleton(subtype)])[0].index] + ) + if debug: + print(f"measured index to have depth={index_size}") + ## need to figure out the index columns and the data columns rather than assuming that the zeroth column is the *only* index column + for c in list(range(len(subframe.columns)))[index_size:]: + subsubframe = subframe.iloc[ + :, list(range(index_size)) + [c] + ] # subframe.loc[:, [subframe.columns[:index_size], c]] if debug: print("subsubframe") print(subsubframe) print() - sub = instantiate_pydantic_object(model_type=subtype, df=subsubframe, from_within_list=True) + sub = instantiate_pydantic_object(model_type=subtype, df=subsubframe, from_within_list=True, debug=debug) if debug: print(f"instantiated: {sub}") list_of_subs.append(sub) @@ -156,19 +187,38 @@ def handle_list_within_list(name, anno, df, debug=False): if debug: print(f"handle_list_within_list {name}, {anno}") print(df) - values = df.set_index(df.columns[0]).loc[name, df.columns[1]] + print(df.set_index(df.columns[0]).loc[name]) + print(df.columns) + + df = df.dropna(axis=1, how="all") + if debug: + print("dropna") + print(df) + df = df.set_index(df.columns[0]) + if debug: + print("setting index") + print(df) + values = df.loc[name] + if debug: + print(f"getting entry for '{name}'") + print(values) + values = values.values[-1] # , df.columns[1] if debug: print(f"values: {values}, {type(values)}") if values is None: return [] values = json.loads(values.replace("'", '"').replace("None", "null")) + if debug: + print(f"decoded values:", values) if len(values) == 0: return [] sub_type = get_subtype_of_optional_or_list(anno) - if isinstance(values[0], dict) and annotation_contains_pydantic(sub_type): - return [sub_type(**v) for v in values] - elif not isinstance(values[0], dict) and not annotation_contains_pydantic(sub_type): - return [sub_type(v) for v in values] + is_dicts = any([isinstance(v, dict) for v in values]) + if is_dicts and annotation_contains_pydantic(sub_type): + return [sub_type(**standardize_keys_in_dict(v)) for v in values] + elif not is_dicts and not annotation_contains_pydantic(sub_type): + # return [sub_type(v) for v in values] + return values else: raise NotImplementedError(f"handle_list_within_list unexpected values - {name}, {anno}, {values}, {df}") @@ -218,30 +268,41 @@ def annotation_switch(name: str, anno, df: pd.DataFrame, from_within_list=False, if is_optional_annotation(anno): if debug: print("optional") - return handle_optional(name, anno, df, from_within_list=from_within_list) + return handle_optional(name, anno, df, from_within_list=from_within_list, debug=debug) elif is_dict_annotation(anno): return handle_dict(name, anno, df) elif is_list_annotation(anno): if from_within_list: if debug: print("list within a list") - return handle_list_within_list(name, anno, df) + return handle_list_within_list(name, anno, df, debug=debug) else: if debug: print("list") - return handle_list(name, anno, df) + return handle_list(name, anno, df, debug=debug) elif isinstance(anno, type(BaseModel)): if debug: print("pydantic") + print(anno) + print(name) + print(df) try: - sub = get_relevant_sub_frame(anno, df, name_of_field=name) + sub = get_relevant_sub_frame(anno, df, name_of_field=name, debug=debug) + if debug: + print("pydantic sub:") + print(sub) except IndexError: return make_skeleton(anno) - return instantiate_pydantic_object(anno, sub) + return instantiate_pydantic_object(anno, sub, from_within_list=from_within_list, debug=debug) elif len(get_args(anno)) == 0: if debug: print("builtin or enum") return handle_builtin_or_enum(name, anno, df) + elif get_origin(anno) is Annotated: + if debug: + print(f"got Annotated type: {anno}, treating as builtin or enum") + datatype = getattr(anno, "__origin__", None) + return handle_builtin_or_enum(name, datatype, df) else: raise NotImplementedError(anno) @@ -256,43 +317,55 @@ def instantiate_pydantic_object( anno = field_info.annotation if debug: print(f"Instantiating field {field_name}, anno {anno} and args {get_args(anno)}") - ret[field_name] = annotation_switch(field_name, anno, df, from_within_list=from_within_list) + ret[field_name] = annotation_switch(field_name, anno, df, from_within_list=from_within_list, debug=debug) if debug: print(ret[field_name]) print() - return model_type(**ret) + return model_type(**standardize_keys_in_dict(ret)) def excel_sheet_to_pydantic( filename: str, sheetname: str, model_type: Union[Type[BaseModel], Type[List[BaseModel]]], debug=False ): + if debug: + print(f"excel_sheet_to_pydantic, sheetname={sheetname}, model_type={model_type}") df = pd.read_excel(filename, sheet_name=sheetname, header=None) df = df.where(df.notnull(), None) if sheetname != "metadata": try: - df = get_relevant_sub_frame(model_type, df) + df = get_relevant_sub_frame(model_type, df, debug=debug) except (KeyError, IndexError): pass + if debug: + print("line 304", model_type) + print(df) if is_optional_annotation(model_type): - return handle_optional(df.iloc[0, 0], model_type, df) + if not annotation_contains_pydantic(model_type): + return handle_optional(df.iloc[0, 0], model_type, df, debug=debug) + else: + model_type = [x for x in get_args(model_type) if x is not type(None)][0] if is_list_annotation(model_type): - return handle_list(df.iloc[0, 0], model_type, df) + return handle_list(df.iloc[0, 0], model_type, df, debug=debug) + if debug: + print("getting children for", model_type) children = seperate_simple_from_pydantic(model_type) + if debug: + print(f"children: {children}") ret = {} if "simple" in children and len(children["simple"]): sub = get_relevant_sub_frame(model_type, df, name_of_field=df.iloc[0, 0]) simple_child_field_type = subset_pydantic_model_type(model_type, children["simple"]) - fields = instantiate_pydantic_object(simple_child_field_type, sub, debug=debug) + fields = instantiate_pydantic_object(simple_child_field_type, sub, from_within_list=False, debug=debug) for child in children["simple"]: ret[child] = getattr(fields, child) for name in children["pydantic"]: if debug: - print(f"Looking to get {name}") + print(f"sheet Looking to get {name}") anno = model_type.model_fields[name].annotation - ret[name] = annotation_switch(name, anno, df) + ret[name] = annotation_switch(name, anno, df, from_within_list=False, debug=debug) for k, v in ret.items(): if isinstance(v, list) or isinstance(v, np.ndarray): ret[k] = [elem for elem in v if elem is not None] diff --git a/pydantic_schemas/utils/pydantic_to_excel.py b/pydantic_schemas/utils/pydantic_to_excel.py index 5f621be..84a2e7b 100644 --- a/pydantic_schemas/utils/pydantic_to_excel.py +++ b/pydantic_schemas/utils/pydantic_to_excel.py @@ -3,7 +3,7 @@ import json import os from enum import Enum -from typing import List, Optional, Tuple, Union +from typing import List, Optional, Tuple, Union, get_args __version__ = importlib.metadata.version("metadataschemas") @@ -14,13 +14,16 @@ from openpyxl.worksheet.datavalidation import DataValidation from openpyxl.worksheet.protection import SheetProtection from openpyxl.worksheet.worksheet import Worksheet -from pydantic import BaseModel +from pydantic import AnyUrl, BaseModel from .utils import ( annotation_contains_dict, annotation_contains_list, assert_dict_annotation_is_strings_or_any, get_subtype_of_optional_or_list, + is_list_annotation, + is_optional_annotation, + is_union_annotation, seperate_simple_from_pydantic, subset_pydantic_model, ) @@ -150,6 +153,31 @@ def replace_row_with_multiple_rows(original_df, new_df, row_to_replace): return df_replaced +def count_lists(model_fields, idx: str): + """ + idx is a string name of a nested field seperated by dots like + "identification_info.citation.alternateTitle" + """ + n_lists = 0 + for part in idx.split("."): + try: + anno = model_fields[part].annotation + except KeyError: + raise KeyError(f"bad model fields given {idx}, for {part} of {model_fields}") + n_lists += annotation_contains_list(anno) + if is_optional_annotation(anno) or is_list_annotation(anno): + anno = get_subtype_of_optional_or_list(anno) + # if hasattr(anno, "model_fields"): + # model_fields = anno.model_fields + # else: + # break + if hasattr(anno, "model_fields"): + model_fields = anno.model_fields + else: + break + return n_lists, anno + + def pydantic_to_dataframe( ob: Union[BaseModel, List[BaseModel]], debug: bool = False, @@ -164,77 +192,137 @@ def pydantic_to_dataframe( if isinstance(ob, list): ob_dict = [elem.model_dump() for elem in ob] annotations = {k: v.annotation for k, v in ob[0].model_fields.items()} + model_fields = {k: v for k, v in ob[0].model_fields.items()} is_list_of_objects = True else: ob_dict = ob.model_dump() annotations = {k: v.annotation for k, v in ob.model_fields.items()} + model_fields = {k: v for k, v in ob.model_fields.items()} is_list_of_objects = False df = pd.json_normalize(ob_dict).T if debug: print("pydantic_to_dataframe") print(df) - # handle dictionaries - # for idx, field in ob_dict.items(): - # if annotation_contains_dict(annotations[idx]): - for fieldname, anno in annotations.items(): - if annotation_contains_dict(anno): - if debug: - print("Found a dictionary") - if is_list_of_objects: - continue - assert_dict_annotation_is_strings_or_any(anno) - field = ob_dict[fieldname] - if field is None or len(field) == 0: - dict_df = pd.DataFrame(["", ""], index=["key", "value"]) - else: - dict_df = pd.DataFrame([field.keys(), field.values()], index=["key", "value"]) - dict_df.index = dict_df.index.map(lambda x: f"{fieldname}.{x}") - df = df[~df.index.str.startswith(f"{fieldname}.")] - df = df[df.index != fieldname] - df = pd.concat([df, dict_df]) - i = 0 list_indices = [] + observed_dicts = set() enums = {} for idx in df.index: + if idx.split(".")[0] in observed_dicts: + continue if debug: - print(f"pydantic_to_dataframe::172 idx = {idx}, df = {df}") + print(f"pydantic_to_dataframe::202 idx = {idx}, df = {df}") vals = df.loc[idx] # [0] + number_of_lists, anno = count_lists(model_fields, idx) + number_of_lists = number_of_lists + int(is_list_of_objects) if debug: print(f"vals: {vals}") print(f'idx.split(".")[0]: {idx.split(".")[0]}') print(f'annotations[idx.split(".")[0]]: {annotations[idx.split(".")[0]]}') - # field = ob_dict[idx.split(".")[0]] + print(f"number of lists = {number_of_lists}") + print(f"anno = {anno}") - if annotation_contains_list(annotations[idx.split(".")[0]]) or annotation_contains_dict( - annotations[idx.split(".")[0]] - ): - if annotation_contains_list(annotations[idx.split(".")[0]]): - subtype = get_subtype_of_optional_or_list(annotations[idx.split(".")[0]]) + if annotation_contains_dict(anno): + if debug: + print(f"annotation contains dict, {ob_dict[idx.split('.')[0]]}") + fieldname = idx.split(".")[0] + # field = ob_dict[fieldname] + subdf = df[df.index.str.startswith(f"{fieldname}.")] + field = {"".join(i.split(".")[1:]): v[0] for i, v in zip(subdf.index, subdf.values)} + if debug: + print(f"field: {field}") + if is_union_annotation(anno) and (len(subdf) == 0 or (field is not None and not isinstance(field, dict))): + args = [a for a in get_args(anno) if a is not type(None)] + anno = [a for a in args if not annotation_contains_dict(a)][0] + if debug: + print(f"falling back to {anno}") else: - subtype = dict + if debug: + print("Found a dictionary") + if is_list_of_objects: + continue + assert_dict_annotation_is_strings_or_any(anno) + + if field is None or len(field) == 0: + dict_df = pd.DataFrame(["", ""], index=["key", "value"]) + else: + dict_df = pd.DataFrame([field.keys(), field.values()], index=["key", "value"]) + if debug: + print(f"created a dict_df:\n{dict_df}") + dict_df.index = dict_df.index.map(lambda x: f"{fieldname}.{x}") + df = df[~df.index.str.startswith(f"{fieldname}.")] + df = df[df.index != fieldname] + df = pd.concat([df, dict_df]) + list_indices += list(range(i, i + 2)) + i += 2 + observed_dicts.add(fieldname) + continue + + if number_of_lists >= 1: #: or annotation_contains_dict(annotations[idx.split(".")[0]]): + # if number_of_lists > 0: + subtype = anno + # else: + # subtype = dict if debug: print("subtype = ", subtype) print("isinstance(subtype, BaseModel)", isinstance(subtype, type(BaseModel))) - print("isinstance(subtype, dict)", isinstance(subtype, dict)) - if is_list_of_objects: + print("isinstance(subtype, dict)", annotation_contains_dict(subtype)) # isinstance(subtype, dict)) + if number_of_lists >= 2 or is_list_of_objects: # is_list_of_objects: if debug: print("list of lists") list_indices.append(i) i += 1 - elif isinstance(subtype, type(BaseModel)) or isinstance(subtype, dict): + # elif number_of_lists == 0: # dicts + # list_indices += list(range(i, i + 2)) + # if debug: + # print(list_indices) + # i += 2 + + elif isinstance(subtype, type(BaseModel)): # isinstance(subtype, type(dict)) + if debug: + print("list of base models", vals, vals[0]) + print("experiment:", vals[0]) + print("experiment:", pd.DataFrame(vals[0]).T) + if vals[0] is None: + vals[0] = [None] + elif isinstance(vals[0], list) and len(vals[0]) == 0: + vals[0] = [None] + sub = pd.json_normalize(vals[0]).T if debug: - print("list of base models", vals) - sub = pd.json_normalize(df.loc[idx].values[0]).reset_index(drop=True).T + print(sub) + # if len(sub.index) == 1: + # sub.index = [idx] + # else: sub.index = sub.index.map(lambda x: f"{idx}." + x) + if debug: + print(sub) df = replace_row_with_multiple_rows(df, sub, idx) + if debug: + print(df) + if debug: + print(list_indices) list_indices += list(range(i, i + len(sub))) + if debug: + print(list_indices) i += len(sub) + if debug: + print("done with basemodel subtype") else: if debug: print("list of builtins or else empty") - df = replace_row_with_multiple_rows(df, df.loc[idx].explode().to_frame().reset_index(drop=True).T, idx) + if vals[0] is None: + vals[0] = [None] + elif isinstance(vals[0], list) and len(vals[0]) == 0: + vals[0] = [None] + sub = pd.DataFrame(vals[0]).T + if len(sub.index) == 1: + sub.index = [idx] + else: + sub.index = sub.index.map(lambda x: f"{idx}." + x) + df = replace_row_with_multiple_rows(df, sub, idx) + if debug: + print("new df:", df) list_indices.append(i) i += 1 else: @@ -272,6 +360,8 @@ def stringify_cell_element(elem): return str(elem.value) elif isinstance(elem, dict): return json.dumps(elem, default=stringify_enum) + elif isinstance(elem, AnyUrl): + return elem.unicode_string() else: return elem @@ -286,7 +376,6 @@ def write_pydantic_to_excel(ws, ob, row_number, debug=False): if all(map(lambda x: x is None, r)): continue r = [stringify_cell_element(val) for val in r] - # r = [str(val) if isinstance(val, list) else str(val.value) if isinstance(val, Enum) else val for val in r ] r = [""] + r if debug: print("about to append", r) @@ -376,7 +465,7 @@ def write_pydantic_to_sheet(worksheet: Worksheet, ob: BaseModel, current_row: in current_row += 1 child_object = getattr(ob, mfield) current_row, sub_list_rows, sub_list_enums = write_pydantic_to_excel( - ws=worksheet, ob=child_object, row_number=current_row + ws=worksheet, ob=child_object, row_number=current_row, debug=debug ) list_rows.update(sub_list_rows) enum_list_rows.update(sub_list_enums) diff --git a/pydantic_schemas/utils/quick_start.py b/pydantic_schemas/utils/quick_start.py index 13d11bd..f5155f7 100644 --- a/pydantic_schemas/utils/quick_start.py +++ b/pydantic_schemas/utils/quick_start.py @@ -9,6 +9,7 @@ from .utils import standardize_keys_in_dict DEFAULT_URL = "http://www.example.com" +MAX_DEPTH = 12 def _is_typing_annotation(annotation): @@ -43,79 +44,83 @@ def _filter_list_for_condition(args: List[Any], condition: Callable[[Any], bool] return [a for a in args if condition(a)] -def _is_pydantic_annotated_string(p, debug=False, indentation=""): +def _is_pydantic_annotated_string(p, debug=False, recursion_level=0): if typing.get_origin(p) is typing.Annotated: args = typing.get_args(p) if args[0] is str: if debug: - print(indentation, "Is Annotated String") + print(" " * recursion_level, "Is Annotated String") return True if debug: - print(indentation, f"Is Annotated but not a string {p}") + print(" " * recursion_level, f"Is Annotated but not a string {p}") return False -def _is_pydantic_annotated_float(p, debug=False, indentation=""): +def _is_pydantic_annotated_float(p, debug=False, recursion_level=0): if typing.get_origin(p) is typing.Annotated: args = typing.get_args(p) if args[0] is float: if debug: - print(indentation, "Is Annotated float") + print(" " * recursion_level, "Is Annotated float") return True if debug: - print(indentation, f"Is Annotated but not a float {p}") + print(" " * recursion_level, f"Is Annotated but not a float {p}") return False def _create_default_class_from_annotation( - p: Any, is_optional: bool = False, debug: bool = False, indentation: str = "" + p: Any, is_optional: bool = False, debug: bool = False, recursion_level: int = 0 ): if p is str: if debug: - print(indentation, "STR") + print(" " * recursion_level, "STR") if is_optional: return None else: return "" elif p is float: if debug: - print(indentation, "STR") + print(" " * recursion_level, "STR") if is_optional: return None else: raise ValueError("Cannot create default float as it's not optional") elif _is_enum_type(p): if debug: - print(indentation, "ENUM") + print(" " * recursion_level, "ENUM") if is_optional: return None else: return list(p)[0].value # get first value of the enum - elif _is_pydantic_subclass(p): + elif _is_pydantic_subclass(p) and recursion_level < MAX_DEPTH: if debug: - print(indentation, "pydantic CLASS") - return make_skeleton(p, debug=debug, indentation=indentation + " ") + print(" " * recursion_level, "pydantic CLASS") + return make_skeleton(p, debug=debug, recursion_level=recursion_level + 1) + elif _is_pydantic_subclass(p) and is_optional: + return None elif isinstance(p, type(AnyUrl)): return DEFAULT_URL else: raise ValueError(f"Unknown annotation: {p}") -def _create_default_from_list_of_args(args: List[Any], is_optional=True, debug=False, indentation=""): +def _create_default_from_list_of_args(args: List[Any], is_optional=True, debug=False, recursion_level=0): """ return None for built in types and enums, but create skeletons of pydantic or typed parameters """ + if is_optional and recursion_level >= MAX_DEPTH: + return None args = _filter_list_for_condition(args, lambda a: a is not type(None)) typed_args = _filter_list_for_condition(args, _is_typing_annotation) # _filter_list_for_typing_args(args) pydantic_args = _filter_list_for_condition(args, _is_pydantic_subclass) # _filter_for_pydantic_args(args) if debug: print( - indentation, + " " * recursion_level, f"LIST OF ARGS: {args}, LIST OF TYPED ARGS: {typed_args}, LIST_OF_PYDANTIC_ARGS: {pydantic_args}", ) if len(typed_args): if debug: - print(indentation, "moving to _create_default_from_typing_annotation") + print(" " * recursion_level, "moving to _create_default_from_typing_annotation") # because dicts are more complicated than lists, we should default to dicts typed_dicts = _filter_list_for_condition(typed_args, lambda p: getattr(p, "__origin__", None) is dict) typed_lists = _filter_list_for_condition(typed_args, lambda p: getattr(p, "__origin__", None) is list) @@ -126,25 +131,25 @@ def _create_default_from_list_of_args(args: List[Any], is_optional=True, debug=F else: chosen_type = typed_args[0] return _create_default_from_typing_annotation( - chosen_type, is_optional=is_optional, debug=debug, indentation=indentation + chosen_type, is_optional=is_optional, debug=debug, recursion_level=recursion_level ) elif len(pydantic_args): - return make_skeleton(pydantic_args[0], debug=debug, indentation=indentation + " ") + return make_skeleton(pydantic_args[0], debug=debug, recursion_level=recursion_level + 1) elif len(_filter_list_for_condition(args, lambda a: _is_builtin_type(a) or _is_enum_type(a))): if debug: - print(indentation, "all builtins or enums") + print(" " * recursion_level, "all builtins or enums") if is_optional: return None elif len(_filter_list_for_condition(args, lambda a: a is str)): return "" else: raise ValueError(f"Can't create a default of {args}") - elif len(args) == 1 and _is_pydantic_annotated_string(args[0], debug=debug, indentation=indentation): + elif len(args) == 1 and _is_pydantic_annotated_string(args[0], debug=debug, recursion_level=recursion_level): if is_optional: return None else: return "" - elif len(args) == 1 and _is_pydantic_annotated_float(args[0], debug=debug, indentation=indentation): + elif len(args) == 1 and _is_pydantic_annotated_float(args[0], debug=debug, recursion_level=recursion_level): if is_optional: return None else: @@ -158,9 +163,9 @@ def _create_default_from_list_of_args(args: List[Any], is_optional=True, debug=F raise ValueError(f"Can't create a default of {args}") -def _create_default_from_typing_annotation(p: Any, is_optional: bool = False, debug: bool = False, indentation=""): +def _create_default_from_typing_annotation(p: Any, is_optional: bool = False, debug: bool = False, recursion_level=0): if debug: - print(indentation, "_create_default_from_typing_annotation") + print(" " * recursion_level, "_create_default_from_typing_annotation") if p is typing.Any: return "" args = typing.get_args(p) @@ -169,46 +174,54 @@ def _create_default_from_typing_annotation(p: Any, is_optional: bool = False, de isOptional = type(None) in args if isOptional: if debug: - print(indentation, "isOPTIONAL") - return _create_default_from_list_of_args(args, is_optional=True, debug=debug, indentation=indentation) + print(" " * recursion_level, "isOPTIONAL") + if recursion_level >= MAX_DEPTH: + return None + return _create_default_from_list_of_args(args, is_optional=True, debug=debug, recursion_level=recursion_level) elif getattr(p, "__origin__", None) is list: if debug: - print(indentation, "isLIST") + print(" " * recursion_level, "isLIST") if _is_pydantic_subclass(args[0]): - return [make_skeleton(args[0], debug=debug, indentation=indentation + " ")] + return [make_skeleton(args[0], debug=debug, recursion_level=recursion_level + 1)] else: if is_optional: return [] else: - return [_create_default(args[0], is_optional=False, debug=debug, indentation=indentation + " ")] + return [_create_default(args[0], is_optional=False, debug=debug, recursion_level=recursion_level + 1)] elif getattr(p, "__origin__", None) is dict: if debug: - print(indentation, "isDICT") - k = _create_default(args[0], debug=debug, indentation=indentation + " ") - v = _create_default(args[1], debug=debug, indentation=indentation + " ") + print(" " * recursion_level, "isDICT") + k = _create_default(args[0], debug=debug, recursion_level=recursion_level + 1) + v = _create_default(args[1], debug=debug, recursion_level=recursion_level + 1) return {k: v} elif len(args) > 1: if debug: - print(indentation, "isUNION") - return _create_default_from_list_of_args(args, is_optional=is_optional, debug=debug, indentation=indentation) + print(" " * recursion_level, "isUNION") + return _create_default_from_list_of_args( + args, is_optional=is_optional, debug=debug, recursion_level=recursion_level + ) else: raise ValueError(f"Unknown typing {p}") -def _create_default(p: inspect.Parameter, is_optional: bool = False, debug: bool = False, indentation: str = ""): +def _create_default(p: inspect.Parameter, is_optional: bool = False, debug: bool = False, recursion_level: int = 0): if hasattr(p, "annotation"): p = p.annotation if inspect.isclass(p) and not _is_typing_annotation(p): if debug: - print(indentation, "CLASS") - return _create_default_class_from_annotation(p, is_optional=is_optional, debug=debug, indentation=indentation) + print(" " * recursion_level, "CLASS") + return _create_default_class_from_annotation( + p, is_optional=is_optional, debug=debug, recursion_level=recursion_level + ) elif _is_typing_annotation(p): if debug: - print(indentation, "TYPED") - return _create_default_from_typing_annotation(p, is_optional=is_optional, debug=debug, indentation=indentation) - elif _is_pydantic_annotated_string(p, debug=debug, indentation=indentation): + print(" " * recursion_level, "TYPED") + return _create_default_from_typing_annotation( + p, is_optional=is_optional, debug=debug, recursion_level=recursion_level + ) + elif _is_pydantic_annotated_string(p, debug=debug, recursion_level=recursion_level): if debug: - print(indentation, "ANNOTATED STRING") + print(" " * recursion_level, "ANNOTATED STRING") if is_optional: return None else: @@ -217,15 +230,15 @@ def _create_default(p: inspect.Parameter, is_optional: bool = False, debug: bool raise ValueError(f"Unknown parameter {p}") -def make_skeleton(cl: Type[BaseModel], debug=False, indentation=""): +def make_skeleton(cl: Type[BaseModel], debug=False, recursion_level=0): parameter_map = inspect.signature(cl).parameters # {'name': } param_values = {} for name, param in parameter_map.items(): if debug: - print(indentation, f"{param.name}: {param.annotation}") - param_values[name] = _create_default(param, debug=debug, indentation=indentation + " ") + print(" " * recursion_level, f"{param.name}: {param.annotation}") + param_values[name] = _create_default(param, debug=debug, recursion_level=recursion_level + 1) if debug: - print(indentation, f"Parameter: {name}, value: {param_values[name]}") + print(" " * recursion_level, f"Parameter: {name}, value: {param_values[name]}") param_values = standardize_keys_in_dict(param_values) return cl(**param_values) diff --git a/pydantic_schemas/utils/utils.py b/pydantic_schemas/utils/utils.py index a7b3d02..0ef2a71 100644 --- a/pydantic_schemas/utils/utils.py +++ b/pydantic_schemas/utils/utils.py @@ -51,6 +51,11 @@ def get_subtype_of_optional_or_list(anno: typing._UnionGenericAlias, debug=False return get_subtype_of_optional_or_list(arg, debug=debug) if len(args) == 1: return args[0] + elif len(args) > 1: + if str in args: + return str + else: + return args[0] else: raise NotImplementedError("Only optional lists optional builtin types implemented") @@ -68,7 +73,7 @@ def _annotation_contains_generic( return True if is_optional_annotation(anno) or is_list_annotation(anno): # optional check is pointless given union check above subtype = get_subtype_of_optional_or_list(anno) - return checker(subtype) + return _annotation_contains_generic(subtype, checker=checker) return False @@ -85,6 +90,10 @@ def annotation_contains_pydantic(anno: typing._UnionGenericAlias) -> bool: def assert_dict_annotation_is_strings_or_any(anno): + if is_union_annotation(anno): + args = [a for a in typing.get_args(anno) if a is not type(None)] + args = [a for a in args if is_dict_annotation(a)] + anno = args[0] if is_dict_annotation(anno): args = typing.get_args(anno) for a in args: diff --git a/pydantic_schemas/video_schema.py b/pydantic_schemas/video_schema.py index fc23af7..42c0932 100644 --- a/pydantic_schemas/video_schema.py +++ b/pydantic_schemas/video_schema.py @@ -4,7 +4,7 @@ from __future__ import annotations from enum import Enum -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Optional from pydantic import Extra, Field @@ -246,44 +246,6 @@ class Tag(SchemaBaseModel): tag_group: Optional[str] = Field(None, title="Tag group") -class ModelInfoItem(SchemaBaseModel): - source: Optional[str] = Field(None, title="Source") - author: Optional[str] = Field(None, title="Author") - version: Optional[str] = Field(None, title="Version") - model_id: Optional[str] = Field(None, title="Model Identifier") - nb_topics: Optional[float] = Field(None, title="Number of topics") - description: Optional[str] = Field(None, title="Description") - corpus: Optional[str] = Field(None, title="Corpus name") - uri: Optional[str] = Field(None, title="URI") - - -class TopicWord(SchemaBaseModel): - word: Optional[str] = Field(None, title="Word") - word_weight: Optional[float] = Field(None, title="Word weight") - - -class TopicDescriptionItem(SchemaBaseModel): - topic_id: Optional[Union[int, str]] = Field(None, title="Topic identifier") - topic_score: Optional[Union[float, str]] = Field(None, title="Topic score") - topic_label: Optional[str] = Field(None, title="Topic label") - topic_words: Optional[List[TopicWord]] = Field(None, description="Words", title="Topic words") - - -class LdaTopic(SchemaBaseModel): - class Config: - extra = Extra.forbid - - model_info: Optional[List[ModelInfoItem]] = Field(None, title="Model information") - topic_description: Optional[List[TopicDescriptionItem]] = Field(None, title="Topic information") - - -class Embedding(SchemaBaseModel): - id: str = Field(..., title="Vector Model ID") - description: Optional[str] = Field(None, title="Vector Model Description") - date: Optional[str] = Field(None, title="Date (YYYY-MM-DD)") - vector: Dict[str, Any] = Field(..., title="Vector") - - class OriginDescription(SchemaBaseModel): harvest_date: Optional[str] = Field(None, description="Harvest date using UTC date format") altered: Optional[bool] = Field( @@ -331,6 +293,4 @@ class Model(SchemaBaseModel): ) provenance: Optional[List[ProvenanceSchema]] = Field(None, description="Provenance") tags: Optional[List[Tag]] = Field(None, description="Tags", title="Tags") - lda_topics: Optional[List[LdaTopic]] = Field(None, description="LDA topics", title="LDA topics") - embeddings: Optional[List[Embedding]] = Field(None, description="Word embeddings", title="Word embeddings") additional: Optional[Dict[str, Any]] = Field(None, description="Additional metadata") diff --git a/schemas/document-schema.json b/schemas/document-schema.json index e90581c..fe492d4 100644 --- a/schemas/document-schema.json +++ b/schemas/document-schema.json @@ -973,145 +973,6 @@ "tag" ] }, - "lda_topics": { - "type": "array", - "title": "LDA topics", - "description": "LDA topics", - "items": { - "type": "object", - "properties": { - "model_info": { - "type": "array", - "title": "Model information", - "items": { - "type": "object", - "properties": { - "source": { - "title": "Source", - "type": "string" - }, - "author": { - "title": "Author", - "type": "string" - }, - "version": { - "title": "Version", - "type": "string" - }, - "model_id": { - "title": "Model Identifier", - "type": "string" - }, - "nb_topics": { - "title": "Number of topics", - "type": "number" - }, - "description": { - "title": "Description", - "type": "string" - }, - "corpus": { - "title": "Corpus name", - "type": "string" - }, - "uri": { - "title": "URI", - "type": "string" - } - } - }, - "required": [ - "model_id" - ] - }, - "topic_description": { - "type": "array", - "title": "Topic information", - "items": { - "type": "object", - "properties": { - "topic_id": { - "title": "Topic identifier", - "type": [ - "integer", - "string" - ] - }, - "topic_score": { - "title": "Topic score", - "type": [ - "number", - "string" - ] - }, - "topic_label": { - "title": "Topic label", - "type": "string" - }, - "topic_words": { - "type": "array", - "title": "Topic words", - "description": "Words", - "items": { - "type": "object", - "properties": { - "word": { - "title": "Word", - "type": "string" - }, - "word_weight": { - "title": "Word weight", - "type": "number" - } - } - }, - "required": [ - "word" - ] - } - } - }, - "required": [ - "topic_id" - ] - } - }, - "additionalProperties": false - } - }, - "embeddings": { - "type": "array", - "title": "Word embeddings", - "description": "Word embeddings", - "items": { - "type": "object", - "properties": { - "id": { - "title": "Vector Model ID", - "type": "string" - }, - "description": { - "title": "Vector Model Description", - "type": "string" - }, - "date": { - "title": "Date (YYYY-MM-DD)", - "type": "string" - }, - "vector": { - "title": "Vector", - "type": [ - "object", - "array" - ] - } - }, - "required": [ - "id", - "vector" - ] - } - }, "additional": { "type": "object", "description": "Additional metadata", diff --git a/schemas/geospatial-schema.json b/schemas/geospatial-schema.json index 5f3082f..33ac6fe 100644 --- a/schemas/geospatial-schema.json +++ b/schemas/geospatial-schema.json @@ -2103,136 +2103,6 @@ "tag" ] }, - "lda_topics": { - "type": "array", - "title": "LDA topics", - "description": "LDA topics", - "items": { - "type": "object", - "properties": { - "model_info": { - "type": "array", - "title": "Model information", - "items": { - "type": "object", - "properties": { - "source": { - "title": "Source", - "type": "string" - }, - "author": { - "title": "Author", - "type": "string" - }, - "version": { - "title": "Version", - "type": "string" - }, - "model_id": { - "title": "Model Identifier", - "type": "string" - }, - "nb_topics": { - "title": "Number of topics", - "type": "number" - }, - "description": { - "title": "Description", - "type": "string" - }, - "corpus": { - "title": "Corpus name", - "type": "string" - }, - "uri": { - "title": "URI", - "type": "string" - } - } - }, - "required": [ - "model_id" - ] - }, - "topic_description": { - "type": "array", - "title": "Topic information", - "items": { - "type": "object", - "properties": { - "topic_id": { - "title": "Topic identifier", - "type": ["integer", "string"] - }, - "topic_score": { - "title": "Topic score", - "type": ["number", "string"] - }, - "topic_label": { - "title": "Topic label", - "type": "string" - }, - "topic_words": { - "type": "array", - "title": "Topic words", - "description": "Words", - "items": { - "type": "object", - "properties": { - "word": { - "title": "Word", - "type": "string" - }, - "word_weight": { - "title": "Word weight", - "type": "number" - } - } - }, - "required": [ - "word" - ] - } - } - }, - "required": [ - "topic_id" - ] - } - - }, - "additionalProperties": false - } - }, - "embeddings":{ - "type": "array", - "title": "Word embeddings", - "description": "Word embeddings", - "items": { - "type": "object", - "properties": { - "id": { - "title": "Vector Model ID", - "type": "string" - }, - "description": { - "title": "Vector Model Description", - "type": "string" - }, - "date": { - "title": "Date (YYYY-MM-DD)", - "type": "string" - }, - "vector": { - "title": "Vector", - "type": "object" - } - }, - "required": [ - "id","vector" - ] - } - }, "additional": { "title": "Additional metadata", "description": "Any additional metadata", diff --git a/schemas/image-schema.json b/schemas/image-schema.json index f161be6..b185bb3 100644 --- a/schemas/image-schema.json +++ b/schemas/image-schema.json @@ -196,136 +196,6 @@ "tag" ] }, - "lda_topics": { - "type": "array", - "title": "LDA topics", - "description": "LDA topics", - "items": { - "type": "object", - "properties": { - "model_info": { - "type": "array", - "title": "Model information", - "items": { - "type": "object", - "properties": { - "source": { - "title": "Source", - "type": "string" - }, - "author": { - "title": "Author", - "type": "string" - }, - "version": { - "title": "Version", - "type": "string" - }, - "model_id": { - "title": "Model Identifier", - "type": "string" - }, - "nb_topics": { - "title": "Number of topics", - "type": "number" - }, - "description": { - "title": "Description", - "type": "string" - }, - "corpus": { - "title": "Corpus name", - "type": "string" - }, - "uri": { - "title": "URI", - "type": "string" - } - } - }, - "required": [ - "model_id" - ] - }, - "topic_description": { - "type": "array", - "title": "Topic information", - "items": { - "type": "object", - "properties": { - "topic_id": { - "title": "Topic identifier", - "type": ["integer", "string"] - }, - "topic_score": { - "title": "Topic score", - "type": ["number", "string"] - }, - "topic_label": { - "title": "Topic label", - "type": "string" - }, - "topic_words": { - "type": "array", - "title": "Topic words", - "description": "Words", - "items": { - "type": "object", - "properties": { - "word": { - "title": "Word", - "type": "string" - }, - "word_weight": { - "title": "Word weight", - "type": "number" - } - } - }, - "required": [ - "word" - ] - } - } - }, - "required": [ - "topic_id" - ] - } - - }, - "additionalProperties": false - } - }, - "embeddings":{ - "type": "array", - "title": "Word embeddings", - "description": "Word embeddings", - "items": { - "type": "object", - "properties": { - "id": { - "title": "Vector Model ID", - "type": "string" - }, - "description": { - "title": "Vector Model Description", - "type": "string" - }, - "date": { - "title": "Date (YYYY-MM-DD)", - "type": "string" - }, - "vector": { - "title": "Vector", - "type": "object" - } - }, - "required": [ - "id","vector" - ] - } - }, "additional": { "type": "object", "description": "Additional metadata", diff --git a/schemas/iptc-phovidmdshared-schema.json b/schemas/iptc-phovidmdshared-schema.json index 52014c8..c059772 100644 --- a/schemas/iptc-phovidmdshared-schema.json +++ b/schemas/iptc-phovidmdshared-schema.json @@ -30,18 +30,6 @@ ], "additionalProperties": false }, - "AltLangObject": { - "description": "Text in alternative languages", - "type": "object", - "patternProperties": { - "^(((([A-Za-z]{2,3}(-([A-Za-z]{3}(-[A-Za-z]{3}){0,2}))?)|[A-Za-z]{4}|[A-Za-z]{5,8})(-([A-Za-z]{4}))?(-([A-Za-z]{2}|[0-9]{3}))?(-([A-Za-z0-9]{5,8}|[0-9][A-Za-z0-9]{3}))*(-([0-9A-WY-Za-wy-z](-[A-Za-z0-9]{2,8})+))*(-(x(-[A-Za-z0-9]{1,8})+))?)|(x(-[A-Za-z0-9]{1,8})+))$": { - "title": "Language tagged text", - "description": "Property name is a BCP47 language tag, property value a text using this language", - "type": "string" - } - }, - "additionalProperties": false - }, "AltLang": { "description": "Text in alternative languages", "type": "string" diff --git a/schemas/microdata-schema.json b/schemas/microdata-schema.json index e427c90..3a9bc5b 100644 --- a/schemas/microdata-schema.json +++ b/schemas/microdata-schema.json @@ -78,142 +78,6 @@ "tag" ] }, - "lda_topics": { - "type": "array", - "title": "LDA topics", - "description": "LDA topics", - "items": { - "type": "object", - "properties": { - "model_info": { - "type": "array", - "title": "Model information", - "items": { - "type": "object", - "properties": { - "source": { - "title": "Source", - "type": "string" - }, - "author": { - "title": "Author", - "type": "string" - }, - "version": { - "title": "Version", - "type": "string" - }, - "model_id": { - "title": "Model Identifier", - "type": "string" - }, - "nb_topics": { - "title": "Number of topics", - "type": "number" - }, - "description": { - "title": "Description", - "type": "string" - }, - "corpus": { - "title": "Corpus name", - "type": "string" - }, - "uri": { - "title": "URI", - "type": "string" - } - } - }, - "required": [ - "model_id" - ] - }, - "topic_description": { - "type": "array", - "title": "Topic information", - "items": { - "type": "object", - "properties": { - "topic_id": { - "title": "Topic identifier", - "type": [ - "integer", - "string" - ] - }, - "topic_score": { - "title": "Topic score", - "type": [ - "number", - "string" - ] - }, - "topic_label": { - "title": "Topic label", - "type": "string" - }, - "topic_words": { - "type": "array", - "title": "Topic words", - "description": "Words", - "items": { - "type": "object", - "properties": { - "word": { - "title": "Word", - "type": "string" - }, - "word_weight": { - "title": "Word weight", - "type": "number" - } - } - }, - "required": [ - "word" - ] - } - } - }, - "required": [ - "topic_id" - ] - } - }, - "additionalProperties": false - } - }, - "embeddings": { - "type": "array", - "title": "Word embeddings", - "description": "Word embeddings", - "items": { - "type": "object", - "properties": { - "id": { - "title": "Vector Model ID", - "type": "string" - }, - "description": { - "title": "Vector Model Description", - "type": "string" - }, - "date": { - "title": "Date (YYYY-MM-DD)", - "type": "string" - }, - "vector": { - "title": "Vector", - "type": "object" - } - }, - "required": [ - "id", - "vector" - ] - } - }, "additional": { "type": "object", "description": "Additional metadata not covered by DDI elements", diff --git a/schemas/script-schema.json b/schemas/script-schema.json index d2dbfc4..be1a0ac 100644 --- a/schemas/script-schema.json +++ b/schemas/script-schema.json @@ -1104,136 +1104,6 @@ "tag" ] }, - "lda_topics": { - "type": "array", - "title": "LDA topics", - "description": "LDA topics", - "items": { - "type": "object", - "properties": { - "model_info": { - "type": "array", - "title": "Model information", - "items": { - "type": "object", - "properties": { - "source": { - "title": "Source", - "type": "string" - }, - "author": { - "title": "Author", - "type": "string" - }, - "version": { - "title": "Version", - "type": "string" - }, - "model_id": { - "title": "Model Identifier", - "type": "string" - }, - "nb_topics": { - "title": "Number of topics", - "type": "number" - }, - "description": { - "title": "Description", - "type": "string" - }, - "corpus": { - "title": "Corpus name", - "type": "string" - }, - "uri": { - "title": "URI", - "type": "string" - } - } - }, - "required": [ - "model_id" - ] - }, - "topic_description": { - "type": "array", - "title": "Topic information", - "items": { - "type": "object", - "properties": { - "topic_id": { - "title": "Topic identifier", - "type": ["integer", "string"] - }, - "topic_score": { - "title": "Topic score", - "type": ["number", "string"] - }, - "topic_label": { - "title": "Topic label", - "type": "string" - }, - "topic_words": { - "type": "array", - "title": "Topic words", - "description": "Words", - "items": { - "type": "object", - "properties": { - "word": { - "title": "Word", - "type": "string" - }, - "word_weight": { - "title": "Word weight", - "type": "number" - } - } - }, - "required": [ - "word" - ] - } - } - }, - "required": [ - "topic_id" - ] - } - - }, - "additionalProperties": false - } - }, - "embeddings":{ - "type": "array", - "title": "Word embeddings", - "description": "Word embeddings", - "items": { - "type": "object", - "properties": { - "id": { - "title": "Vector Model ID", - "type": "string" - }, - "description": { - "title": "Vector Model Description", - "type": "string" - }, - "date": { - "title": "Date (YYYY-MM-DD)", - "type": "string" - }, - "vector": { - "title": "Vector", - "type": "object" - } - }, - "required": [ - "id","vector" - ] - } - }, "additional": { "type": "object", "description": "Additional metadata", diff --git a/schemas/table-schema.json b/schemas/table-schema.json index 530f9ca..ae532a4 100644 --- a/schemas/table-schema.json +++ b/schemas/table-schema.json @@ -987,142 +987,6 @@ "tag" ] }, - "lda_topics": { - "type": "array", - "title": "LDA topics", - "description": "LDA topics", - "items": { - "type": "object", - "properties": { - "model_info": { - "type": "array", - "title": "Model information", - "items": { - "type": "object", - "properties": { - "source": { - "title": "Source", - "type": "string" - }, - "author": { - "title": "Author", - "type": "string" - }, - "version": { - "title": "Version", - "type": "string" - }, - "model_id": { - "title": "Model Identifier", - "type": "string" - }, - "nb_topics": { - "title": "Number of topics", - "type": "number" - }, - "description": { - "title": "Description", - "type": "string" - }, - "corpus": { - "title": "Corpus name", - "type": "string" - }, - "uri": { - "title": "URI", - "type": "string" - } - } - }, - "required": [ - "model_id" - ] - }, - "topic_description": { - "type": "array", - "title": "Topic information", - "items": { - "type": "object", - "properties": { - "topic_id": { - "title": "Topic identifier", - "type": [ - "integer", - "string" - ] - }, - "topic_score": { - "title": "Topic score", - "type": [ - "number", - "string" - ] - }, - "topic_label": { - "title": "Topic label", - "type": "string" - }, - "topic_words": { - "type": "array", - "title": "Topic words", - "description": "Words", - "items": { - "type": "object", - "properties": { - "word": { - "title": "Word", - "type": "string" - }, - "word_weight": { - "title": "Word weight", - "type": "number" - } - } - }, - "required": [ - "word" - ] - } - } - }, - "required": [ - "topic_id" - ] - } - }, - "additionalProperties": false - } - }, - "embeddings": { - "type": "array", - "title": "Word embeddings", - "description": "Word embeddings", - "items": { - "type": "object", - "properties": { - "id": { - "title": "Vector Model ID", - "type": "string" - }, - "description": { - "title": "Vector Model Description", - "type": "string" - }, - "date": { - "title": "Date (YYYY-MM-DD)", - "type": "string" - }, - "vector": { - "title": "Vector", - "type": "object" - } - }, - "required": [ - "id", - "vector" - ] - } - }, "additional": { "type": "object", "description": "Additional metadata", diff --git a/schemas/video-schema.json b/schemas/video-schema.json index 4fd3aba..591de52 100644 --- a/schemas/video-schema.json +++ b/schemas/video-schema.json @@ -632,137 +632,6 @@ "tag" ] }, - "lda_topics": { - "type": "array", - "title": "LDA topics", - "description": "LDA topics", - "items": { - "type": "object", - "properties": { - "model_info": { - "type": "array", - "title": "Model information", - "items": { - "type": "object", - "properties": { - "source": { - "title": "Source", - "type": "string" - }, - "author": { - "title": "Author", - "type": "string" - }, - "version": { - "title": "Version", - "type": "string" - }, - "model_id": { - "title": "Model Identifier", - "type": "string" - }, - "nb_topics": { - "title": "Number of topics", - "type": "number" - }, - "description": { - "title": "Description", - "type": "string" - }, - "corpus": { - "title": "Corpus name", - "type": "string" - }, - "uri": { - "title": "URI", - "type": "string" - } - } - }, - "required": [ - "model_id" - ] - }, - "topic_description": { - "type": "array", - "title": "Topic information", - "items": { - "type": "object", - "properties": { - "topic_id": { - "title": "Topic identifier", - "type": ["integer", "string"] - }, - "topic_score": { - "title": "Topic score", - "type": ["number", "string"] - }, - "topic_label": { - "title": "Topic label", - "type": "string" - }, - "topic_words": { - "type": "array", - "title": "Topic words", - "description": "Words", - "items": { - "type": "object", - "properties": { - "word": { - "title": "Word", - "type": "string" - }, - "word_weight": { - "title": "Word weight", - "type": "number" - } - } - }, - "required": [ - "word" - ] - } - } - }, - "required": [ - "topic_id" - ] - } - - }, - "additionalProperties": false - } - }, - "embeddings":{ - "type": "array", - "title": "Word embeddings", - "description": "Word embeddings", - "items": { - "type": "object", - "properties": { - "id": { - "title": "Vector Model ID", - "type": "string" - }, - "description": { - "title": "Vector Model Description", - "type": "string" - }, - "date": { - "title": "Date (YYYY-MM-DD)", - "type": "string" - }, - "vector": { - "title": "Vector", - "type": "object" - } - }, - "required": [ - "id","vector" - ] - } - }, - "additional": { "type": "object", "description": "Additional metadata",