From d3f68577c8740d8151486f6714377dded6d89a88 Mon Sep 17 00:00:00 2001 From: David Glenck Date: Sat, 13 Jul 2024 19:15:54 +0200 Subject: [PATCH] fix: ods export/import for datetime and time values --- HISTORY.md | 6 ++++++ src/tablib/formats/_ods.py | 10 +++++++--- tests/files/book.ods | Bin 8833 -> 10275 bytes tests/test_tablib.py | 22 ++++++++++++++++++++++ 4 files changed, 35 insertions(+), 3 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 6a1985e3..9513bfd3 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,11 @@ # History +## Unreleased + +### Bugfixes + +- Fix time and datetime export in ODS format (#595). + ## 3.6.1 (2024-04-04) ### Bugfixes diff --git a/src/tablib/formats/_ods.py b/src/tablib/formats/_ods.py index 34b47a0d..cb5fbdab 100644 --- a/src/tablib/formats/_ods.py +++ b/src/tablib/formats/_ods.py @@ -92,7 +92,11 @@ def convert_date(val): return convert_date(date_value) if value_type == 'time': time_value = cell.getAttribute('timevalue') - return dt.datetime.strptime(time_value, "%H:%M:%S").time() + try: + return dt.datetime.strptime(time_value, "PT%HH%MM%SS").time() + except ValueError: + # backwards compatibility for times exported with older tablib versions + return dt.datetime.strptime(time_value, "%H:%M:%S").time() if value_type == 'boolean': bool_value = cell.getAttribute('booleanvalue') return bool_value == 'true' @@ -158,12 +162,12 @@ def dset_sheet(cls, dataset, ws): cell = table.TableCell(valuetype="float", value=col) elif isinstance(col, dt.datetime): cell = table.TableCell( - valuetype="date", value=col.strftime('%Y-%m-%dT%H:%M:%S') + valuetype="date", datevalue=col.strftime('%Y-%m-%dT%H:%M:%S') ) elif isinstance(col, dt.date): cell = table.TableCell(valuetype="date", datevalue=col.strftime('%Y-%m-%d')) elif isinstance(col, dt.time): - cell = table.TableCell(valuetype="time", timevalue=col.strftime('%H:%M:%S')) + cell = table.TableCell(valuetype="time", timevalue=col.strftime('PT%HH%MM%SS')) elif col is None: cell = table.TableCell(valuetype="void") else: diff --git a/tests/files/book.ods b/tests/files/book.ods index a26976805478cf9a8dbd28f51acd15fe94a43162..4c2b49f95b491b9b0b886529a802fffa61211467 100644 GIT binary patch delta 5911 zcmbuDc|6oxAIE=V8`;awV5}iaX|a_3$})|RP$L>Oc4L<%_h(7Akj9>+taoII6s44q zHGA2vEu`#3_IhUOiahmPJ)PHUe!rPN&gXoWGw1t0UthH}9UC14DH#($97h6WYbf9_uV6d0L+qfNGu%Gk@?f+?@D!b*Ba(Wba4;;LBrEO)Usz zxK)h}fJZvA%YPA=Yd(tke*|#(-0Z?TRk#6Rg1{qtIe%>ePM(MJH;jY`G3XZsqj9po4{K#VY93j8# zk+n%*R4Al0G4je{RdwIK|Q?yxTFlO(o5UiQ|)m(jy&R z5xk#dYVHj!mPmLA{x7)f>Bw+?-PY9TFl`Ky+inj(*%fZMfM*uJO38h^!jL4(ufu&L{`&gGTLNX*xU+KZ9 zpukuReb`jf*=GeQRj9kbRb#rM^U3kM$< zX@?o3!I4WNnBiN0b6l#;u>tUZg{0^8c7yX7={MrV!RNU~?QH`q$oHfvTsG8h84Hn3 zYH0bpVy(*}6i&((IHh+@qn)%5;L?81cI;wBs8~zBii>6LaV3R??H2CXyXhI%yL0Da zU+6=*L-7omRY3v)SD|Ob>w;Sio4O`C6)YGtn5TmhxiHjI89~976EI%V9@$qH^X`^E zDm1>V4&FbcaOqHqXsBqvp6pTffwH3_@lm?T4WR`}4>>40H9E#PG%buNV;3iFb^75{ zm#9S=D1`+NsO<`S_~>bFuxowjO3zFQ>eKPh{Gath)%Bi{Qv|m>cX~m8soqpWq&=T5 z)5J)^Qx@_#&Usn5vqw_b^+~3mP2I&u$afQxrHhblOA=rFm(0^mP(_>*KHZm|G0==4kCi zqK*n5%0z|HUT)gsJPN&Y-kEDO=Ber^taxCkzUo?|Txj$klQ&eMq zeIT{wTDkcvoA$v*G7?CcDfN-zB+u4(xlYJoIEv>1R7_Rg5$dbhkBKJtHDi~i~3#*&jh}} zh*=1s_>!zUqCU7~3A7%ooUFbT#CgSg_0ei(-HhzLQ%Or<=DdoH3B7A<_jH^a#XUVy zM>s)LBrII@#I9Uf-J@E(cU_wfO2&osjB|-(Bk$Ps4C-3cg^)QGX27^VhzwHQD0>kX zE@4H%A>fBb=wZyW4zwrF3J6K*R=b(nclT2mrA)Rk%%jkY?U@UU!wLJG$*i;dGV?pi ziX^6GThjTGb0y<$OS%V?)vt96xSvcC;)Q}M1ESoz<}dtx&o#e7)?P7Wdat@mvcn}y1i0wJW_Q`OsX0*Jpu``I-;D=4PI_$NDN zcqA4-lje8Npn`^QxZ9zzR^}LI7YV0}=HJT1 z2?BfkfIseekiZ~{+k`X;LX<=yj0^f(i6EB9pNSNa7D7yj@Up!`?jool)%2CcHMNiL zkcGB;B}N7S%!Hzs4`YGWkbXWYY^5!5cBf` z6D$S4rpFoM;$nrhaNbd5`F4Adscd)VBM$&j5hCmC;$e%~nNAQn7sYvkegBmqA`R$U zy4smx(N?z3|7VV>DFA@TVf1|Ln_%*o>4(I7KiXTT^%ATak3|_O9NO2|zTequ*=Jup z)$Xjfb=RV<#*e7dzkLoprhAG3lYx`fIA@~X=9lDR?T$YMVZMRTny+d2^ESDocDd|| zhyp`u+}c&p)$a3ao|M%;r(E+(!RaUc{fcFnmc0?(Sch)FzucBToJ*G6KUecX>$~3dsbq>a_ z_{cjs(|PzkEfv~Zp)-Ck#1{OBp6RQR`#2-PVZj>o4jbz>Rad#p_Sj529~p$YWl{Oe z_sm?qZAG8Hkc`QE;d1Zov;>vamRFe}nd>Uusbn%*hUS)GYrGpiYte(S55I2_=O zGW3%x)sta|l2K7cJkWZQLbeohYk{sMXL24@94S6W#m*7{tF2GZh&S>e4Zan?=%#L5 zJ*YK)M_-M%AdIV;lXW?%5z1YpJ#B8tdH@E6^stD<%I60!6v3>?n3dyoCn}#7M`TI% zy+n2K?XhQ#dP(i@WNaVEr7AR~!t%DZ(l3lB)CVd(X0XDzU%~!n<23`BfaTa)BSACP zmFAJ!%Z6#io&$FCH-)?{QaD@ehZKv0>pqH~URU5NPWf<7%12r3sIWq=?2>E?&0#K@ zbwR^r*BQ;yCvO<*OZVJ>L@!d(B_>#gxnK^deGG%prYY)9znJ0411Yk`S!ib6uK6!o z)0H@ew=OnmRGnLy!;Hn-;94K8EoLPJNfoyCHarcsid$)E_RMiqPFqLQRq4MQ3CUOS zKSiAf=MR2E`A`P`w2TBAiM-=YQt;uuY=ErxOwLWpKXmcJ&9TXiB4(1SUNnE~V$DOtR-g?5V!pZY>?dWm54bd!IIQNDeQ0&_~SKw+>O2mL9v* z+CTD$VRG8Wq5s8|xA;q0 zC0>_FdT6KV>A4IA=4VsrVVC6@-w2GykZ_<<%`TLZ`=cj(W^QMJtd5;yIWI@9$b8as zTA(>EJ+I&BL{`?{lOj-&-E~dhc>k;Bzd8Mt;S*YnYsY!i_tCWX&+ikHvMWoYIRO^* z2T=`3c&CY)rDAK^bmu&KA2d{%G^aCbPkPmhQftd9#kjmG#UxWeH5yfPBI}B8RwgjR zO3L}fyXhv53{t%2&s|LbW;l`BWXEGs4t5 zb1qY)7)UpG*W;Tqhk)A@%g<=8!@_75;PeUS;!91Nt-G?57Sfed*)1`DSW9wVKp&V6 zh&Hvg>D_HG)@JzrfupID1|Dggfnq!MaQe|p(P9Y$bToJb4Mu|kM#~nA2FvQt1D8@v zmKZ*Rw4mqUy|NbYipSYe;fa!cv-#@j3Z~-`kWflRk93jwwFMADEFx$0)`n;aHJ*5! z>L4H_Or$?=G!V~n;#gNEJ~!JAx;yPVY|3wC2%;F_BqzSUjCE!0NPAAiw{rQ@F2qjd z(s8-moT59|BXlKb;r|#z^mZk(hwIm-(`}uu2f-cVft}d-`oMRHMKpwP11G+|jCJK~ z2<`-g`?mzYk2e_!u91i1H(WeR6kyTYX?J5YaogdI(BJsSO=92+NpQw+_OEvR&t}Jl zAhF%?UF@Nx1O?vT3f1?qzetKZK*GIA9RIR8i5Nv}hHOP?BK~ZQBHuCs`2Ip`F?ZKO%GR2YkOFOPszvzeG6S z80*RdG{CQ*`qwOPSQ`|){`mka!Q6jneM1%~^4&{|%Y(6QCg~@4hz&zG6t@8N7jWND zEb=X-Bly2VjQ^&JB8U*rImP7I<_m-y#RiOT%F5FNz^|bBH)#N;%=>-rR~QLWJ7)6@ zA)+bUQ?zw^t}CD60Di{mNLj$+U|z2kk6* z)`$ZD9{`x2aXEZCcvm6<007PdEddxb28E9bMR|pW2BUqv@aPb%_7SWvBE$=a#vwvN zQCQy)p9l;Ji$~x>!%<$oxBwIiznT-G`Ey7@Z>AwwKeT^D_#zuIPmy2lyZG~urTCTr z#0PM3Vh~(*)dCkl4B?WIUBZ0X$e$je7g>O4s3iY=FGM)^YBvxooLfRU)C-FW*8VoP z0ss(OxFv~59=`85kcCijJaSvU@bK~REhc+qi~`UETLaxUXN~>-Dl+3SR*( zo(c-@g^0ui7jEY-;8OY;-3EvbhmQ(I;TCZw;&SW2ui^M1oKFZA&pAMduE^#v3n`vp}>e@f1ZfV;e&EvW$3R*KfV+ z1BMK?h*LkAC+j-oATOBVT1yr|$_!mF#?Io}K6rwqCAj2yUaJ8h=C1zpdq7eQZ!cl5`S*z&m_P&XLJQ<)3e1 zQY|Y{QhPvTNBn?}VPI>gvz-3UE7^MI3(%nA8yVcPxSH4dR-wXQUgI=2UXj9 z8I!|EUIYy#i}GBO1S|VqHqU#)y1FS>`$t>L>b0hk*lL7APmQ%;opnRbK7m6qhl-yf zuu`_A&&A#q4!Qx&`C^D2rUhG(%G@)wy_bTKA*Ki>c9ZUV5v!XgZV40*l2RS6^kh{E zwqY)WyYYnwOmA-=-og8B)9)&4EluEsyRw~2bvnUxNdxtH|7nx~T9Hz8Ig!>p6cYs# z(kdvk;JcdL?o}8_U`3bIu>3U|Z(J-f&WrUldwu;n=G{A1Ra3}t{v}%20f830d+DdUQp_xc*o@O+B!>A|5!ym-l+;FwrMLGeh?ti3F?cmQ_bo-dx}2QmhdDmc z4MHhEXv>fJM9F_I2{_eZ}A7IjUE!w?10$W{bzx z0Ds$-ov#>qo@N&Xu9|XLxFEW_o7&D2Ybw)t;nxTOTrQYiFaN|hwRxKu=3CPUg^I3k z{k%@k^%y6%j75m-ZQOdTWuJ&k`x7-AdiS^}JmD(Y;*FY&ruL6_TisJHA71~a=P^02 z>&|)bO@H96kz0a}rq-JTyf>UXUcuI|EVh51jNmOhA7LNc9lkJvi%qJvJ+jZJEzR#i z6Md+>h(6DC+MAd>OMIGkUY#nO-b9(qWsiQCfXXH>->#jtW{PV8U=#)b-@n;$C_J7I zjrIR>8xw@1)@)j;xK0l9{gib8z>!Fykv*E)GDN9;{-`?KaJa@}`d0ccF}sljW66oP-M!Rv>1^=< z(_N$4dYc<(@lk`t|4PGy7-oiMFCt$vDW{9_aC46L#HUFtvP!lnb79c)pkv$TkpVZS zNdo2|jQT0+C61{Rn9S&leVIS5pF5TN^M zhNsNsg)z0aJ3eVt6AnDqtVj*pk^E(b3l7;ebpT zqYl|uzYs1gck9NbmJT^l4NK6EC2BS5^t4VPM?~#Na)e7)5ceC;QLGIR9Y;gJpefSP zXYLGEy6G>KS?QUz*~HG+J%@829>|?8E|b}Dui)dq$k$?%BwMJ0_pLL@WBrHKS^Bw* zw%sANWcPP+eZkY<^`i^o6_s)&31cPp_1ysvTc-EUoIfOur9{z`XNxZw-5K_07Fgs? z_7D8{*&w-Xq_K_H#5nNh0E8vBa(`o`M?>~Gdv??mC!^HEW5M`icxhPJ1$p_{_zFWU zLU4L4NS@PGduCgbgix?m%Z%IEo`er9OBq$4sxD_P@Tb)lP2~nkfOF?1t3}R^ExOrQ03~e}&QO>?{6g zJJMn>@b*af(JCyJ9;JM2I%EFSb1){_kKVTnJkt5h?~-cLTALb4lGMFA4IUKhZgNtfy>SdPzy6?+yu47nn|ALL zyEbOb`H=-u6gc)Uff4*-`-FjB=8Q^bY_dU0W3WMEHeD)Yx}GsLP&&Nr11Z7SZ%0%$ zSe^|JImCDtE>p^A3oI5IvYu^AhcQ{DvCd7i)fn5bbXM3SWF0fQBxaiZpmZ?0(A`ZQ zdA7FUbLF)OVHSPhpg|NH*2yX)p~Yy;u#eq$K5tFW#u4nPrTvI|v(#|OiKOn99QLP$ z&v$<=U`Y&oQU}=;qwIx_T4Bmtk~L^FUz)mBszA{+Uh`=xoPV3|o_GPLRJbY3_vr}% z&N}o00XXab4*0k`}-pSUxHYL_=kYy6#hq!IYEFc zll)c0-va#YV*>xdNsobLy}$tl?3>5oPqm=x0qi%oG0QXYJn`)UDFLCZ%^mk>?YG&h z&Cx(ac{RSmXhBmh4=!$T;M?MqL%%$hq^h0eYO7_$Vu4*TbtIA%Wwic#d?ElqVsyi@ z0Y{Dst{?<{BL>3d{sItQ37Cc|JS0{%mN~vAS65nWB^oFx{EZ+37lrFzqg)lljW60( zcS+lRJ718xT`^ukc7JHy?*;^^-4#QEdr|wEEp2&8h89f)yjaTx04JfQhNFI$hb{MN zExc?={6SS*X&DFrXCY(gABsgaybQabE)K3lc|uLvvKn5-eUafS+c=IMULNVtbcH(p ztD!NO^xS6+00gN1bqCWM4Iz(E)3?;~Et!6!>sxjST#{Jq0X3H_meO@5VS%j!rArfX h$oa)Aj|;LyVYN*#JIl3D3kd)s&<6#9*2!~b{|1@g&Kv*$ diff --git a/tests/test_tablib.py b/tests/test_tablib.py index d557d25e..aad6ae3c 100755 --- a/tests/test_tablib.py +++ b/tests/test_tablib.py @@ -5,6 +5,7 @@ import doctest import json import pickle +import re import tempfile import unittest from decimal import Decimal @@ -13,6 +14,7 @@ from uuid import uuid4 import xlrd +from odf import opendocument, table from openpyxl.reader.excel import load_workbook import tablib @@ -1214,6 +1216,26 @@ def test_ods_unknown_value_type(self): dataset = tablib.Dataset().load(fh, 'ods') self.assertEqual(dataset.pop(), ('abcd',)) + def test_ods_export_dates(self): + """test against odf specification""" + date = dt.date(2019, 10, 4) + date_time = dt.datetime(2019, 10, 4, 12, 30, 8) + time = dt.time(14, 30) + data.append((date, time, date_time)) + data.headers = ('date', 'time', 'date/time') + _ods = data.ods + ods_book = opendocument.load(BytesIO(_ods)) + cells = ods_book.spreadsheet.getElementsByType(table.TableRow)[1].childNodes + # date value + self.assertEqual(cells[0].getAttribute('datevalue'), '2019-10-04') + # time value + duration_exp = re.compile(r"^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)D)?" + r"(?:T(?:(\d+)H)?(?:(\d+)M)?(?:([\d.]+)S)?)?$") + duration = duration_exp.match(cells[1].getAttribute('timevalue')).groups() + self.assertListEqual([0, 0, 0, 14, 30, 0], [int(v or 0) for v in duration]) + # datetime value + self.assertEqual(cells[2].getAttribute('datevalue'), '2019-10-04T12:30:08') + class XLSTests(BaseTestCase): def test_xls_format_detect(self):