2626
2727"""
2828
29+ # =============================================================================
30+ # Imports
31+ # =============================================================================
32+
2933import datetime
3034import io
3135from typing import Any
3640from pendulum .datetime import DateTime
3741from semantic_version import Version
3842
39- from cardinal_pythonlib .datetimefunc import pendulum_to_datetime
43+
44+ # =============================================================================
45+ # Constants
46+ # =============================================================================
47+
48+ # ISO 8601, e.g. 2013-07-24T20:04:07+0100)
49+ ISO8601_STRFTIME_FORMAT = "%Y-%m-%dT%H:%M:%S%z"
50+
51+
52+ # =============================================================================
53+ # Conversion functions
54+ # =============================================================================
4055
4156
4257def excel_to_bytes (wb : Workbook ) -> bytes :
@@ -63,9 +78,38 @@ def convert_for_openpyxl(x: Any) -> Any:
6378
6479 Returns:
6580 the same thing, or a more suitable value!
81+
82+ 2025-03-06 update: We were doing this:
83+
84+ .. code-block:: python
85+
86+ if isinstance(x, DateTime):
87+ return pendulum_to_datetime(x)
88+
89+ However, conversion of pendulum.datetime.Datetime to datetime.datetime is
90+ insufficient, because with openpyxl==3.0.7 you can still end up with this
91+ error from openpyxl/utils/datetime.py, line 97, in to_excel:
92+
93+ .. code-block:: python
94+
95+ days = (dt - epoch).days
96+ # TypeError: can't subtract offset-naive and offset-aware datetimes
97+
98+ The "epoch" variable does NOT have a timezone attribute. So we need to
99+ ensure that what we produce here doesn't, either. In principle, there are
100+ three alternatives: (a) convert to a standard timezone (UTC), making things
101+ slightly and silently unhappier for those working outside UTC; (b) strip
102+ timezone information, causing errors if datetime values are subtracted; or
103+ (c) convert to a standard textual representation, including timezone
104+ information, preserving all data but letting the user sort out the meaning.
105+ Since ``convert_for_pyexcel_ods3`` was already converting
106+ pendulum.datetime.DateTime and datetime.datetime values to a standard
107+ string, via strftime, let's do that too. Note that this also anticipates
108+ the deprecation of timezone-aware dates from openpyxl==3.0.7
109+ (https://foss.heptapod.net/openpyxl/openpyxl/-/issues/1645).
66110 """
67- if isinstance (x , DateTime ):
68- return pendulum_to_datetime ( x )
111+ if isinstance (x , ( DateTime , datetime . datetime ) ):
112+ return x . strftime ( ISO8601_STRFTIME_FORMAT )
69113 elif isinstance (x , (Version , uuid .UUID )):
70114 return str (x )
71115 else :
@@ -83,22 +127,33 @@ def convert_for_pyexcel_ods3(x: Any) -> Any:
83127 - ``None``
84128 - :class:`numpy.float64`
85129 - :class:`uuid.UUID`
130+ - subclasses of `str`
86131
87132 Args:
88133 x: a data value
89134
90135 Returns:
91136 the same thing, or a more suitable value!
92- """
93137
138+ 2025-03-06 update: With pyexcel-ods3==0.6.0, we were getting a KeyError
139+ from pyexcel_ods3/odsw.py, in ODSSheetWriter.write_row. It does this:
140+
141+ .. code-block:: python
142+
143+ value_type = service.ODS_WRITE_FORMAT_COVERSION[type(cell)]
144+
145+ and we had a cell that looked like 'aq' but had the type <class
146+ 'sqlalchemy.sql.elements.quoted_name'>, a subclass of str.
147+ """
94148 if isinstance (x , (DateTime , datetime .datetime )):
95- # ISO 8601, e.g. 2013-07-24T20:04:07+0100)
96- return x .strftime ("%Y-%m-%dT%H:%M:%S%z" )
149+ return x .strftime (ISO8601_STRFTIME_FORMAT )
97150 elif x is None :
98151 return ""
99152 elif isinstance (x , (Version , uuid .UUID )):
100153 return str (x )
101154 elif isinstance (x , float64 ):
102155 return float (x )
156+ elif isinstance (x , str ):
157+ return str (x )
103158 else :
104159 return x
0 commit comments