Skip to content

Commit 13e6d98

Browse files
Support serializing datetimes with datetime.timezone (#914)
* Support serializing datetimes with datetime.timezone This enables users to use python's standard `datetime.timezone` for constant utc-offset timezones (only for serialization, data coming back from the server will continue to use `pytz`). This change also enables pandas 2 as pandas switched over to `datetime.timezone` for constant offset timezones https://pandas.pydata.org/docs/dev/whatsnew/v2.0.0.html#utc-and-fixed-offset-timezones-default-to-standard-library-tzinfo-objects * Fix installation failure The driver may not depend on 3rd party libraries when just being loaded. This has to be done dynamically when needed to not break pip install which loads the driver to figure out the version number (and potentially other meta data). * Add unit tests Signed-off-by: Rouven Bauer <[email protected]> Co-authored-by: Grant Lodge <[email protected]>
1 parent a6d9050 commit 13e6d98

File tree

6 files changed

+82
-3
lines changed

6 files changed

+82
-3
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ Homepage = "https://github.com/neo4j/neo4j-python-driver"
4747
[project.optional-dependencies]
4848
numpy = ["numpy >= 1.7.0, < 2.0.0"]
4949
pandas = [
50-
"pandas >= 1.1.0, < 2.0.0",
50+
"pandas >= 1.1.0, < 3.0.0",
5151
"numpy >= 1.7.0, < 2.0.0",
5252
]
5353

src/neo4j/_codec/hydration/v1/temporal.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
datetime,
2121
time,
2222
timedelta,
23+
timezone,
2324
)
2425

2526
from ...._optional_deps import (
@@ -38,6 +39,9 @@
3839
from ...packstream import Structure
3940

4041

42+
ANY_BUILTIN_DATETIME = datetime(1970, 1, 1)
43+
44+
4145
def get_date_unix_epoch():
4246
return Date(1970, 1, 1)
4347

@@ -172,10 +176,15 @@ def seconds_and_nanoseconds(dt):
172176
seconds, nanoseconds = seconds_and_nanoseconds(value)
173177
return Structure(b"f", seconds, nanoseconds, tz.key)
174178
else:
179+
if isinstance(tz, timezone):
180+
# offset of the timezone is constant, so any date will do
181+
offset = tz.utcoffset(ANY_BUILTIN_DATETIME)
182+
else:
183+
offset = tz.utcoffset(value)
175184
# with time offset
176185
seconds, nanoseconds = seconds_and_nanoseconds(value)
177186
return Structure(b"F", seconds, nanoseconds,
178-
int(tz.utcoffset(value).total_seconds()))
187+
int(offset.total_seconds()))
179188

180189

181190
if np is not None:

src/neo4j/_codec/hydration/v2/temporal.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,12 @@ def seconds_and_nanoseconds(dt):
8383
return Structure(b"i", seconds, nanoseconds, tz.key)
8484
else:
8585
# with time offset
86+
if isinstance(tz, timezone):
87+
# offset of the timezone is constant, so any date will do
88+
offset = tz.utcoffset(datetime(1970, 1, 1))
89+
else:
90+
offset = tz.utcoffset(value)
8691
seconds, nanoseconds = seconds_and_nanoseconds(value)
87-
offset = tz.utcoffset(value)
8892
if offset.microseconds:
8993
raise ValueError("Bolt protocol does not support sub-second "
9094
"UTC offsets.")

tests/unit/common/codec/hydration/v1/test_temporal_dehydration.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,18 @@ def test_native_date_time_fixed_offset(self, assert_transforms):
129129
pytz.FixedOffset(60))
130130
assert_transforms(dt, Structure(b"F", 1539344261, 474716000, 3600))
131131

132+
def test_date_time_fixed_native_offset(self, assert_transforms):
133+
dt = DateTime(2018, 10, 12, 11, 37, 41, 474716862,
134+
datetime.timezone(datetime.timedelta(minutes=60)))
135+
assert_transforms(dt, Structure(b"F", 1539344261, 474716862, 3600))
136+
137+
def test_native_date_time_fixed_native_offset(self, assert_transforms):
138+
dt = datetime.datetime(
139+
2018, 10, 12, 11, 37, 41, 474716,
140+
datetime.timezone(datetime.timedelta(minutes=60))
141+
)
142+
assert_transforms(dt, Structure(b"F", 1539344261, 474716000, 3600))
143+
132144
def test_pandas_date_time_fixed_offset(self, assert_transforms):
133145
dt = pd.Timestamp("2018-10-12T11:37:41.474716862+0100")
134146
assert_transforms(dt, Structure(b"F", 1539344261, 474716862, 3600))
@@ -143,6 +155,19 @@ def test_native_date_time_fixed_negative_offset(self, assert_transforms):
143155
pytz.FixedOffset(-60))
144156
assert_transforms(dt, Structure(b"F", 1539344261, 474716000, -3600))
145157

158+
def test_date_time_fixed_negative_native_offset(self, assert_transforms):
159+
dt = DateTime(2018, 10, 12, 11, 37, 41, 474716862,
160+
datetime.timezone(datetime.timedelta(minutes=-60)))
161+
assert_transforms(dt, Structure(b"F", 1539344261, 474716862, -3600))
162+
163+
def test_native_date_time_fixed_negative_native_offset(self,
164+
assert_transforms):
165+
dt = datetime.datetime(
166+
2018, 10, 12, 11, 37, 41, 474716,
167+
datetime.timezone(datetime.timedelta(minutes=-60))
168+
)
169+
assert_transforms(dt, Structure(b"F", 1539344261, 474716000, -3600))
170+
146171
def test_pandas_date_time_fixed_negative_offset(self, assert_transforms):
147172
dt = pd.Timestamp("2018-10-12T11:37:41.474716862-0100")
148173
assert_transforms(dt, Structure(b"F", 1539344261, 474716862, -3600))

tests/unit/common/codec/hydration/v1/test_temporal_dehydration_utc_patch.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,13 @@ def __new__(mcs, name, bases, attrs):
3434
for test_func in (
3535
"test_date_time_fixed_offset",
3636
"test_native_date_time_fixed_offset",
37+
"test_date_time_fixed_native_offset",
38+
"test_native_date_time_fixed_native_offset",
3739
"test_pandas_date_time_fixed_offset",
3840
"test_date_time_fixed_negative_offset",
3941
"test_native_date_time_fixed_negative_offset",
42+
"test_date_time_fixed_negative_native_offset",
43+
"test_native_date_time_fixed_negative_native_offset",
4044
"test_pandas_date_time_fixed_negative_offset",
4145
"test_date_time_zone_id",
4246
"test_native_date_time_zone_id",

tests/unit/common/codec/hydration/v2/test_temporal_dehydration.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,24 @@ def test_native_date_time_fixed_offset(self, assert_transforms):
5252
Structure(b"I", 1539340661, 474716000, 3600)
5353
)
5454

55+
def test_date_time_fixed_native_offset(self, assert_transforms):
56+
dt = DateTime(2018, 10, 12, 11, 37, 41, 474716862,
57+
datetime.timezone(datetime.timedelta(minutes=60)))
58+
assert_transforms(
59+
dt,
60+
Structure(b"I", 1539340661, 474716862, 3600)
61+
)
62+
63+
def test_native_date_time_fixed_native_offset(self, assert_transforms):
64+
dt = datetime.datetime(
65+
2018, 10, 12, 11, 37, 41, 474716,
66+
datetime.timezone(datetime.timedelta(minutes=60))
67+
)
68+
assert_transforms(
69+
dt,
70+
Structure(b"I", 1539340661, 474716000, 3600)
71+
)
72+
5573
def test_pandas_date_time_fixed_offset(self, assert_transforms):
5674
dt = pd.Timestamp("2018-10-12T11:37:41.474716862+0100")
5775
assert_transforms(dt, Structure(b"I", 1539340661, 474716862, 3600))
@@ -72,6 +90,25 @@ def test_native_date_time_fixed_negative_offset(self, assert_transforms):
7290
Structure(b"I", 1539347861, 474716000, -3600)
7391
)
7492

93+
def test_date_time_fixed_negative_native_offset(self, assert_transforms):
94+
dt = DateTime(2018, 10, 12, 11, 37, 41, 474716862,
95+
datetime.timezone(datetime.timedelta(minutes=-60)))
96+
assert_transforms(
97+
dt,
98+
Structure(b"I", 1539347861, 474716862, -3600)
99+
)
100+
101+
def test_native_date_time_fixed_negative_native_offset(self,
102+
assert_transforms):
103+
dt = datetime.datetime(
104+
2018, 10, 12, 11, 37, 41, 474716,
105+
datetime.timezone(datetime.timedelta(minutes=-60))
106+
)
107+
assert_transforms(
108+
dt,
109+
Structure(b"I", 1539347861, 474716000, -3600)
110+
)
111+
75112
def test_pandas_date_time_fixed_negative_offset(self, assert_transforms):
76113
dt = pd.Timestamp("2018-10-12T11:37:41.474716862-0100")
77114
assert_transforms(dt, Structure(b"I", 1539347861, 474716862, -3600))

0 commit comments

Comments
 (0)