1818# However, if you have executed another commercial license agreement
1919# with Crate these terms will supersede the license and you may use the
2020# software solely pursuant to the terms of the relevant commercial agreement.
21+ from datetime import datetime , timedelta , timezone
2122
23+ from .converter import DataType
2224import warnings
25+ import typing as t
2326
2427from .converter import Converter
2528from .exceptions import ProgrammingError
@@ -32,13 +35,15 @@ class Cursor(object):
3235 """
3336 lastrowid = None # currently not supported
3437
35- def __init__ (self , connection , converter : Converter ):
38+ def __init__ (self , connection , converter : Converter , ** kwargs ):
3639 self .arraysize = 1
3740 self .connection = connection
3841 self ._converter = converter
3942 self ._closed = False
4043 self ._result = None
4144 self .rows = None
45+ self ._time_zone = None
46+ self .time_zone = kwargs .get ("time_zone" )
4247
4348 def execute (self , sql , parameters = None , bulk_parameters = None ):
4449 """
@@ -241,3 +246,72 @@ def _convert_rows(self):
241246 convert (value )
242247 for convert , value in zip (converters , row )
243248 ]
249+
250+ @property
251+ def time_zone (self ):
252+ """
253+ Get the current time zone.
254+ """
255+ return self ._time_zone
256+
257+ @time_zone .setter
258+ def time_zone (self , tz ):
259+ """
260+ Set the time zone.
261+
262+ Different data types are supported. Available options are:
263+
264+ - ``datetime.timezone.utc``
265+ - ``datetime.timezone(datetime.timedelta(hours=7), name="MST")``
266+ - ``pytz.timezone("Australia/Sydney")``
267+ - ``zoneinfo.ZoneInfo("Australia/Sydney")``
268+ - ``+0530`` (UTC offset in string format)
269+
270+ When `time_zone` is `None`, the returned `datetime` objects are
271+ "naive", without any `tzinfo`, converted using ``datetime.utcfromtimestamp(...)``.
272+
273+ When `time_zone` is given, the returned `datetime` objects are "aware",
274+ with `tzinfo` set, converted using ``datetime.fromtimestamp(..., tz=...)``.
275+ """
276+
277+ # Do nothing when time zone is reset.
278+ if tz is None :
279+ self ._time_zone = None
280+ return
281+
282+ # Requesting datetime-aware `datetime` objects needs the data type converter.
283+ # Implicitly create one, when needed.
284+ if self ._converter is None :
285+ self ._converter = Converter ()
286+
287+ # When the time zone is given as a string, assume UTC offset format, e.g. `+0530`.
288+ if isinstance (tz , str ):
289+ tz = self ._timezone_from_utc_offset (tz )
290+
291+ self ._time_zone = tz
292+
293+ def _to_datetime_with_tz (value : t .Optional [float ]) -> t .Optional [datetime ]:
294+ """
295+ Convert CrateDB's `TIMESTAMP` value to a native Python `datetime`
296+ object, with timezone-awareness.
297+ """
298+ if value is None :
299+ return None
300+ return datetime .fromtimestamp (value / 1e3 , tz = self ._time_zone )
301+
302+ # Register converter function for `TIMESTAMP` type.
303+ self ._converter .set (DataType .TIMESTAMP_WITH_TZ , _to_datetime_with_tz )
304+ self ._converter .set (DataType .TIMESTAMP_WITHOUT_TZ , _to_datetime_with_tz )
305+
306+ @staticmethod
307+ def _timezone_from_utc_offset (tz ) -> timezone :
308+ """
309+ Convert UTC offset in string format (e.g. `+0530`) into `datetime.timezone` object.
310+ """
311+ assert len (tz ) == 5 , f"Time zone '{ tz } ' is given in invalid UTC offset format"
312+ try :
313+ hours = int (tz [:3 ])
314+ minutes = int (tz [0 ] + tz [3 :])
315+ return timezone (timedelta (hours = hours , minutes = minutes ), name = tz )
316+ except Exception as ex :
317+ raise ValueError (f"Time zone '{ tz } ' is given in invalid UTC offset format: { ex } " )
0 commit comments