Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 30 additions & 22 deletions kubernetes/base/config/dateutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,29 +53,37 @@ def parse_rfc3339(s):
if not s.tzinfo:
return s.replace(tzinfo=UTC)
return s
groups = _re_rfc3339.search(s).groups()
dt = [0] * 7
for x in range(6):
dt[x] = int(groups[x])
us = 0
if groups[6] is not None:
partial_sec = float(groups[6].replace(",", "."))
us = int(MICROSEC_PER_SEC * partial_sec)
tz = UTC
if groups[7] is not None and groups[7] != 'Z' and groups[7] != 'z':
tz_groups = _re_timezone.search(groups[7]).groups()
hour = int(tz_groups[1])
minute = 0
if tz_groups[0] == "-":
hour *= -1
if tz_groups[2]:
minute = int(tz_groups[2])
tz = TimezoneInfo(hour, minute)
return datetime.datetime(
year=dt[0], month=dt[1], day=dt[2],
hour=dt[3], minute=dt[4], second=dt[5],
microsecond=us, tzinfo=tz)

match = _re_rfc3339.search(s)

if match is None:
raise ValueError(f"Error in RFC339 Date Formatting {s}")
try:
groups = match.groups()

dt = [0] * 7
for x in range(6):
dt[x] = int(groups[x])
us = 0
if groups[6] is not None:
partial_sec = float(groups[6].replace(",", "."))
us = int(MICROSEC_PER_SEC * partial_sec)
tz = UTC
if groups[7] is not None and groups[7] != 'Z' and groups[7] != 'z':
tz_groups = _re_timezone.search(groups[7]).groups()
hour = int(tz_groups[1])
minute = 0
if tz_groups[0] == "-":
hour *= -1
if tz_groups[2]:
minute = int(tz_groups[2])
tz = TimezoneInfo(hour, minute)
return datetime.datetime(
year=dt[0], month=dt[1], day=dt[2],
hour=dt[3], minute=dt[4], second=dt[5],
microsecond=us, tzinfo=tz)
except Exception:
raise ValueError(f"Error in RFC339 Date Formatting {s}")
Copy link

@afshin-deriv afshin-deriv Oct 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think in the current implementation of parse_rfc3339, the _re_rfc3339 regex accept several things that RFC 3339 forbids, also make some required pieces optional - as a result, in some cases datetime.datetime will crash with a meaningful messages:

for example, _re_rfc3339 accept 25 as hour although RFC 3339 ABNF says:
time-hour = 2DIGIT ; 00-23:

>>> from kubernetes.base.config import dateutil

>>> s = "2025-10-04t25:00:00"
>>> dateutil.parse_rfc3339(s)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/afshinpaydar/go/src/k8s.io/python/kubernetes/base/config/dateutil.py", line 74, in parse_rfc3339
    return datetime.datetime(
           ^^^^^^^^^^^^^^^^^^
ValueError: hour must be in 0..23
>>>

but in this changes, the catch all section only shows Error in RFC339 Date Formatting .. message, it seems misleading to me as the regex is not quite strict/correct:

>>> from kubernetes.base.config import dateutil
>>> s = "2025-10-04Z20:00:00"
>>> dateutil.parse_rfc3339(s)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/afshinpaydar/go/src/k8s.io/python/kubernetes/base/config/dateutil.py", line 60, in parse_rfc3339
    raise ValueError(f"Error in RFC339 Date Formatting {s}")
ValueError: Error in RFC339 Date Formatting 2025-10-04Z20:00:00

maybe something like below output would be more helpful for users:

ValueError: Invalid RFC 3339 ... , Expected ...

IMHO, I really like the idea of doing less and enable more:

-    groups = _re_rfc3339.search(s).groups()
+    m = _re_rfc3339.fullmatch(s.strip())
+    if m is None:
+        raise ValueError(
+            f"Invalid RFC3339 datetime: {s!r} "
+            "(expected YYYY-MM-DDTHH:MM:SS[.frac][Z|±HH:MM])"
+        )
+    groups = m.groups()


def format_rfc3339(date_time):
if date_time.tzinfo is None:
Expand Down
29 changes: 29 additions & 0 deletions kubernetes/test/test_api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import kubernetes
from kubernetes.client.configuration import Configuration
import urllib3
import datetime
from kubernetes.config import dateutil as kube_dateutil

class TestApiClient(unittest.TestCase):

Expand Down Expand Up @@ -49,3 +51,30 @@ def test_rest_proxycare(self):
# test
client = kubernetes.client.ApiClient(configuration=config)
self.assertEqual( expected_pool, type(client.rest_client.pool_manager) )

def test_1_parse_rfc3339(self):
dt = datetime.datetime(2023, 1, 1, 12, 0, 0)
result = kube_dateutil.parse_rfc3339(dt)
self.assertIsNotNone(result.tzinfo)

def test_2_parse_rfc3339(self):
"""Test that invalid RFC3339 strings raise ValueError with descriptive message"""
invalid_inputs = [
"invalid-datetime-string",
"",
"not-a-date",
"2023", # incomplete
"random text",
"2023-13-01T12:00:00Z", # invalid month
"not-rfc3339-format"
]

for invalid_input in invalid_inputs:
with self.subTest(input=invalid_input):
with self.assertRaises(ValueError) as context:
kube_dateutil.parse_rfc3339(invalid_input)

# Check that the error message includes the invalid input
error_message = str(context.exception)
self.assertIn("Error in RFC339 Date Formatting", error_message)
self.assertIn(invalid_input, error_message)