Skip to content

Commit 16e3f94

Browse files
Fix nested sanitization of datetime and NaN values (#127)
datetime and NaN values nested inside dicts or lists were passed through unsanitized because _sanitize_value() only handled flat values. Add recursion for dict (via _sanitize()) and list types.
1 parent 147793a commit 16e3f94

2 files changed

Lines changed: 75 additions & 5 deletions

File tree

customerio/client_base.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,10 @@ def _sanitize_value(self, value):
120120
return self._datetime_to_timestamp(value)
121121
if isinstance(value, float) and math.isnan(value):
122122
return None
123+
if isinstance(value, dict):
124+
return self._sanitize(value)
125+
if isinstance(value, list):
126+
return [self._sanitize_value(item) for item in value]
123127
return value
124128

125129
def _datetime_to_timestamp(self, dt):

tests/test_customerio.py

Lines changed: 71 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@ def test_keepalive_socket_options_are_configured_on_adapter(self):
100100
self.assertIn((tcp_protocol, tcp_keepidle, TCP_KEEPALIVE_IDLE_TIMEOUT), socket_options)
101101
if hasattr(socket, "TCP_KEEPINTVL"):
102102
self.assertIn(
103-
(tcp_protocol, socket.TCP_KEEPINTVL, TCP_KEEPALIVE_INTERVAL), socket_options
103+
(tcp_protocol, socket.TCP_KEEPINTVL, TCP_KEEPALIVE_INTERVAL),
104+
socket_options,
104105
)
105106
self.assertEqual(HTTPConnection.default_socket_options, default_socket_options)
106107

@@ -221,7 +222,11 @@ def test_track_with_id_and_timestamp(self):
221222
)
222223

223224
self.cio.track(
224-
1, "purchase", {"type": "socks"}, id="01HB4HBDKTFWYZCK01DMRSWRFD", timestamp=1561231234
225+
1,
226+
"purchase",
227+
{"type": "socks"},
228+
id="01HB4HBDKTFWYZCK01DMRSWRFD",
229+
timestamp=1561231234,
225230
)
226231

227232
def test_track_with_invalid_timestamp(self):
@@ -444,14 +449,21 @@ def test_device_call_last_used(self):
444449
"content_type": "application/json",
445450
"url_suffix": "/customers/1/devices",
446451
"body": {
447-
"device": {"id": "device_2", "platform": "android", "last_used": 1234567890}
452+
"device": {
453+
"id": "device_2",
454+
"platform": "android",
455+
"last_used": 1234567890,
456+
}
448457
},
449458
},
450459
)
451460
)
452461

453462
self.cio.add_device(
454-
customer_id=1, device_id="device_2", platform="android", last_used=1234567890
463+
customer_id=1,
464+
device_id="device_2",
465+
platform="android",
466+
last_used=1234567890,
455467
)
456468

457469
def test_device_call_valid_platform(self):
@@ -566,6 +578,57 @@ def test_sanitize(self):
566578
data_out = self.cio._sanitize(data_in)
567579
self.assertEqual(data_out, dict(dt=1234567890))
568580

581+
def test_sanitize_nested_dict_datetime(self):
582+
from datetime import timezone
583+
584+
data_in = {"event": {"created_at": datetime(2009, 2, 13, 23, 31, 30, 0, timezone.utc)}}
585+
data_out = self.cio._sanitize(data_in)
586+
self.assertEqual(data_out, {"event": {"created_at": 1234567890}})
587+
588+
def test_sanitize_list_datetime(self):
589+
from datetime import timezone
590+
591+
data_in = {
592+
"dates": [
593+
datetime(2009, 2, 13, 23, 31, 30, 0, timezone.utc),
594+
datetime(2024, 1, 1, 0, 0, 0, 0, timezone.utc),
595+
]
596+
}
597+
data_out = self.cio._sanitize(data_in)
598+
self.assertEqual(data_out, {"dates": [1234567890, 1704067200]})
599+
600+
def test_sanitize_nested_dict_nan(self):
601+
data_in = {"metrics": {"score": float("nan"), "count": 5}}
602+
data_out = self.cio._sanitize(data_in)
603+
self.assertEqual(data_out, {"metrics": {"score": None, "count": 5}})
604+
605+
def test_sanitize_deeply_nested(self):
606+
from datetime import timezone
607+
608+
data_in = {
609+
"outer": {
610+
"items": [
611+
{
612+
"ts": datetime(2009, 2, 13, 23, 31, 30, 0, timezone.utc),
613+
"val": float("nan"),
614+
},
615+
{"ts": datetime(2024, 1, 1, 0, 0, 0, 0, timezone.utc), "val": 42},
616+
],
617+
},
618+
}
619+
data_out = self.cio._sanitize(data_in)
620+
self.assertEqual(
621+
data_out,
622+
{
623+
"outer": {
624+
"items": [
625+
{"ts": 1234567890, "val": None},
626+
{"ts": 1704067200, "val": 42},
627+
],
628+
},
629+
},
630+
)
631+
569632
def test_sanitize_naive_datetime(self):
570633
"""Naive datetimes are assumed UTC (backward compatible)."""
571634
data_in = dict(dt=datetime(2009, 2, 13, 23, 31, 30))
@@ -647,7 +710,10 @@ def test_merge_customers_call(self):
647710
"authorization": _basic_auth_str("siteid", "apikey"),
648711
"content_type": "application/json",
649712
"url_suffix": "/merge_customers",
650-
"body": {"primary": {"cio_id": "CIO456"}, "secondary": {"id": "MyCustomId"}},
713+
"body": {
714+
"primary": {"cio_id": "CIO456"},
715+
"secondary": {"id": "MyCustomId"},
716+
},
651717
},
652718
)
653719
)

0 commit comments

Comments
 (0)