From 9721b45f8639e6b7a0c9913b298594d3494903b4 Mon Sep 17 00:00:00 2001 From: Stephen Young Date: Thu, 21 May 2026 23:34:21 -0400 Subject: [PATCH 1/2] Fix _datetime_to_timestamp to correctly convert tz-aware datetimes Previously, dt.replace(tzinfo=timezone.utc) silently discarded any existing timezone info, treating e.g. a UTC-5 datetime as if it were UTC. Now tz-aware datetimes use astimezone() for proper conversion, while naive datetimes continue to be assumed UTC for backward compat. Co-Authored-By: Claude Opus 4.6 (1M context) --- customerio/client_base.py | 2 ++ tests/test_customerio.py | 22 ++++++++++++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/customerio/client_base.py b/customerio/client_base.py index e333b08..db5b70a 100644 --- a/customerio/client_base.py +++ b/customerio/client_base.py @@ -123,6 +123,8 @@ def _sanitize_value(self, value): return value def _datetime_to_timestamp(self, dt): + if dt.tzinfo is not None: + return int(dt.astimezone(timezone.utc).timestamp()) return int(dt.replace(tzinfo=timezone.utc).timestamp()) def _stringify_list(self, customer_ids): diff --git a/tests/test_customerio.py b/tests/test_customerio.py index 77beb98..c6117b1 100644 --- a/tests/test_customerio.py +++ b/tests/test_customerio.py @@ -1,7 +1,7 @@ import json import socket import unittest -from datetime import datetime +from datetime import datetime, timedelta, timezone from functools import partial import urllib3 @@ -562,12 +562,30 @@ def test_unsuppress_call(self): self.cio.unsuppress(None) def test_sanitize(self): - from datetime import timezone + data_in = dict(dt=datetime(2009, 2, 13, 23, 31, 30, 0, timezone.utc)) + data_out = self.cio._sanitize(data_in) + self.assertEqual(data_out, dict(dt=1234567890)) + def test_sanitize_naive_datetime(self): + """Naive datetimes are assumed UTC (backward compatible).""" + data_in = dict(dt=datetime(2009, 2, 13, 23, 31, 30)) + data_out = self.cio._sanitize(data_in) + self.assertEqual(data_out, dict(dt=1234567890)) + + def test_sanitize_aware_utc_datetime(self): + """Tz-aware UTC datetimes produce the correct timestamp.""" data_in = dict(dt=datetime(2009, 2, 13, 23, 31, 30, 0, timezone.utc)) data_out = self.cio._sanitize(data_in) self.assertEqual(data_out, dict(dt=1234567890)) + def test_sanitize_aware_non_utc_datetime(self): + """Tz-aware non-UTC datetimes are converted, not silently replaced.""" + # 2009-02-13 18:31:30 at UTC-5 is 2009-02-13 23:31:30 UTC + tz_minus_5 = timezone(timedelta(hours=-5)) + data_in = dict(dt=datetime(2009, 2, 13, 18, 31, 30, 0, tz_minus_5)) + data_out = self.cio._sanitize(data_in) + self.assertEqual(data_out, dict(dt=1234567890)) + def test_ids_are_encoded_in_url(self): self.cio.http.hooks = dict( response=partial( From 2f41089ceb0480362c9ddc816e7ca2494a23bc10 Mon Sep 17 00:00:00 2001 From: Stephen Young Date: Thu, 21 May 2026 23:42:10 -0400 Subject: [PATCH 2/2] Remove duplicate test_sanitize_aware_utc_datetime --- tests/test_customerio.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/test_customerio.py b/tests/test_customerio.py index c6117b1..82272ef 100644 --- a/tests/test_customerio.py +++ b/tests/test_customerio.py @@ -572,12 +572,6 @@ def test_sanitize_naive_datetime(self): data_out = self.cio._sanitize(data_in) self.assertEqual(data_out, dict(dt=1234567890)) - def test_sanitize_aware_utc_datetime(self): - """Tz-aware UTC datetimes produce the correct timestamp.""" - data_in = dict(dt=datetime(2009, 2, 13, 23, 31, 30, 0, timezone.utc)) - data_out = self.cio._sanitize(data_in) - self.assertEqual(data_out, dict(dt=1234567890)) - def test_sanitize_aware_non_utc_datetime(self): """Tz-aware non-UTC datetimes are converted, not silently replaced.""" # 2009-02-13 18:31:30 at UTC-5 is 2009-02-13 23:31:30 UTC