From c64d4b21e72bd5bb112e0440519f965a57cf0f4a Mon Sep 17 00:00:00 2001
From: Paul Gerber
Date: Tue, 30 Jun 2026 15:35:13 +0200
Subject: [PATCH 1/6] Fix: Handle XSD end-of-day midnight notation 24:00:00
Before, parsing XSD timestamps with an hour of 24 causes a ValueError.
Now, the parser in `sdk/basyx/aas/model/datatypes.py` correctly identifies 24:00:00 as midnight, handles the day rollover for `DateTime`, and normalizes `Time` to 00:00:00.
Unit tests for valid and invalid edge cases are added to `sdk/test/model/test_datatypes.py` inside `TestDateTimeTypes`.
Fixes #564
---
sdk/basyx/aas/model/datatypes.py | 19 ++++++++++++++++---
sdk/test/model/test_datatypes.py | 20 ++++++++++++++++++++
2 files changed, 36 insertions(+), 3 deletions(-)
diff --git a/sdk/basyx/aas/model/datatypes.py b/sdk/basyx/aas/model/datatypes.py
index d5acc6d45..8ae5af0d5 100644
--- a/sdk/basyx/aas/model/datatypes.py
+++ b/sdk/basyx/aas/model/datatypes.py
@@ -615,16 +615,29 @@ def _parse_xsd_datetime(value: str) -> DateTime:
if match[1]:
raise ValueError("Negative Dates are not supported by Python")
microseconds = int(float(match[8]) * 1e6) if match[8] else 0
- return DateTime(int(match[2]), int(match[3]), int(match[4]), int(match[5]), int(match[6]), int(match[7]),
+ hour = int(match[5])
+ is_midnight_24 = False
+ if hour == 24:
+ if int(match[6]) != 0 or int(match[7]) != 0 or microseconds != 0:
+ raise ValueError("Invalid time: 24:00:00.000000 is the only valid representation of midnight")
+ hour = 0
+ is_midnight_24 = True
+
+ res = DateTime(int(match[2]), int(match[3]), int(match[4]), hour, int(match[6]), int(match[7]),
microseconds, _parse_xsd_date_tzinfo(match[9]))
-
+ return res + datetime.timedelta(days=1) if is_midnight_24 else res
def _parse_xsd_time(value: str) -> Time:
match = TIME_RE.match(value)
if not match:
raise ValueError("Value is not a valid XSD datetime string")
microseconds = int(float(match[4]) * 1e6) if match[4] else 0
- return Time(int(match[1]), int(match[2]), int(match[3]), microseconds, _parse_xsd_date_tzinfo(match[5]))
+ hour = int(match[1])
+ if hour == 24:
+ if int(match[2]) != 0 or int(match[3]) != 0 or microseconds != 0:
+ raise ValueError("Invalid time: 24:00:00.000000 is the only valid representation of midnight")
+ hour = 0
+ return Time(hour, int(match[2]), int(match[3]), microseconds, _parse_xsd_date_tzinfo(match[5]))
def _parse_xsd_bool(value: str) -> Boolean:
diff --git a/sdk/test/model/test_datatypes.py b/sdk/test/model/test_datatypes.py
index b83c5e5fb..d06a47ba7 100644
--- a/sdk/test/model/test_datatypes.py
+++ b/sdk/test/model/test_datatypes.py
@@ -243,7 +243,27 @@ def test_serialize_time(self) -> None:
datetime.time(15, 25, 17, tzinfo=datetime.timezone.utc)))
self.assertEqual("15:25:17.250000+01:00", model.datatypes.xsd_repr(
datetime.time(15, 25, 17, 250000, tzinfo=datetime.timezone(datetime.timedelta(hours=1)))))
+
+ def test_parse_datetime_midnight_24(self) -> None:
+ res = model.datatypes.from_xsd("2020-01-24T24:00:00", model.datatypes.DateTime)
+ self.assertEqual(datetime.datetime(2020, 1, 25, 0, 0, 0), res)
+ res_tz = model.datatypes.from_xsd("2020-01-24T24:00:00Z", model.datatypes.DateTime)
+ self.assertEqual(datetime.datetime(2020, 1, 25, 0, 0, 0, tzinfo=datetime.timezone.utc), res_tz)
+
+ def test_parse_datetime_midnight_24_invalid(self) -> None:
+ with self.assertRaises(ValueError) as cm:
+ model.datatypes.from_xsd("2020-01-24T24:01:00", model.datatypes.DateTime)
+ self.assertEqual("Invalid time: 24:00:00.000000 is the only valid representation of midnight", str(cm.exception))
+
+ def test_parse_time_midnight_24(self) -> None:
+ res = model.datatypes.from_xsd("24:00:00", model.datatypes.Time)
+ self.assertEqual(datetime.time(0, 0, 0), res)
+ def test_parse_time_midnight_24_invalid(self) -> None:
+ with self.assertRaises(ValueError) as cm:
+ model.datatypes.from_xsd("24:00:01", model.datatypes.Time)
+ self.assertEqual("Invalid time: 24:00:00.000000 is the only valid representation of midnight", str(cm.exception))
+
def test_trivial_cast(self) -> None:
val = model.datatypes.trivial_cast(datetime.date(2017, 11, 13), model.datatypes.Date)
self.assertEqual(model.datatypes.Date(2017, 11, 13), val)
From 1ceb325f53149fd0e424aec13ef9c54689747d0f Mon Sep 17 00:00:00 2001
From: Paul Gerber
Date: Tue, 30 Jun 2026 15:58:41 +0200
Subject: [PATCH 2/6] Fixed Too Long Lines Issues
---
sdk/basyx/aas/model/datatypes.py | 5 +++--
sdk/test/model/test_datatypes.py | 14 ++++++++++----
2 files changed, 13 insertions(+), 6 deletions(-)
diff --git a/sdk/basyx/aas/model/datatypes.py b/sdk/basyx/aas/model/datatypes.py
index 8ae5af0d5..2c696ff30 100644
--- a/sdk/basyx/aas/model/datatypes.py
+++ b/sdk/basyx/aas/model/datatypes.py
@@ -622,11 +622,12 @@ def _parse_xsd_datetime(value: str) -> DateTime:
raise ValueError("Invalid time: 24:00:00.000000 is the only valid representation of midnight")
hour = 0
is_midnight_24 = True
-
+
res = DateTime(int(match[2]), int(match[3]), int(match[4]), hour, int(match[6]), int(match[7]),
- microseconds, _parse_xsd_date_tzinfo(match[9]))
+ microseconds, _parse_xsd_date_tzinfo(match[9]))
return res + datetime.timedelta(days=1) if is_midnight_24 else res
+
def _parse_xsd_time(value: str) -> Time:
match = TIME_RE.match(value)
if not match:
diff --git a/sdk/test/model/test_datatypes.py b/sdk/test/model/test_datatypes.py
index d06a47ba7..0f7b71e27 100644
--- a/sdk/test/model/test_datatypes.py
+++ b/sdk/test/model/test_datatypes.py
@@ -243,7 +243,7 @@ def test_serialize_time(self) -> None:
datetime.time(15, 25, 17, tzinfo=datetime.timezone.utc)))
self.assertEqual("15:25:17.250000+01:00", model.datatypes.xsd_repr(
datetime.time(15, 25, 17, 250000, tzinfo=datetime.timezone(datetime.timedelta(hours=1)))))
-
+
def test_parse_datetime_midnight_24(self) -> None:
res = model.datatypes.from_xsd("2020-01-24T24:00:00", model.datatypes.DateTime)
self.assertEqual(datetime.datetime(2020, 1, 25, 0, 0, 0), res)
@@ -253,7 +253,10 @@ def test_parse_datetime_midnight_24(self) -> None:
def test_parse_datetime_midnight_24_invalid(self) -> None:
with self.assertRaises(ValueError) as cm:
model.datatypes.from_xsd("2020-01-24T24:01:00", model.datatypes.DateTime)
- self.assertEqual("Invalid time: 24:00:00.000000 is the only valid representation of midnight", str(cm.exception))
+ self.assertEqual(
+ "Invalid time: 24:00:00.000000 is the only valid representation of midnight",
+ str(cm.exception)
+ )
def test_parse_time_midnight_24(self) -> None:
res = model.datatypes.from_xsd("24:00:00", model.datatypes.Time)
@@ -262,8 +265,11 @@ def test_parse_time_midnight_24(self) -> None:
def test_parse_time_midnight_24_invalid(self) -> None:
with self.assertRaises(ValueError) as cm:
model.datatypes.from_xsd("24:00:01", model.datatypes.Time)
- self.assertEqual("Invalid time: 24:00:00.000000 is the only valid representation of midnight", str(cm.exception))
-
+ self.assertEqual(
+ "Invalid time: 24:00:00.000000 is the only valid representation of midnight",
+ str(cm.exception)
+ )
+
def test_trivial_cast(self) -> None:
val = model.datatypes.trivial_cast(datetime.date(2017, 11, 13), model.datatypes.Date)
self.assertEqual(model.datatypes.Date(2017, 11, 13), val)
From 9bccf03b0ded72ce0f94abef91d8ef3d5167c835 Mon Sep 17 00:00:00 2001
From: Paul Gerber
Date: Mon, 6 Jul 2026 15:15:33 +0200
Subject: [PATCH 3/6] Update sdk/basyx/aas/model/datatypes.py
Co-authored-by: s-heppner
---
sdk/basyx/aas/model/datatypes.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/sdk/basyx/aas/model/datatypes.py b/sdk/basyx/aas/model/datatypes.py
index 2c696ff30..4e4915b0a 100644
--- a/sdk/basyx/aas/model/datatypes.py
+++ b/sdk/basyx/aas/model/datatypes.py
@@ -619,7 +619,7 @@ def _parse_xsd_datetime(value: str) -> DateTime:
is_midnight_24 = False
if hour == 24:
if int(match[6]) != 0 or int(match[7]) != 0 or microseconds != 0:
- raise ValueError("Invalid time: 24:00:00.000000 is the only valid representation of midnight")
+ raise ValueError(f"{value} is not a valid xsd:datetime.")
hour = 0
is_midnight_24 = True
From 26506f899e0f9d97c1a3c8e2b9e6c0861595b5e0 Mon Sep 17 00:00:00 2001
From: Paul Gerber
Date: Mon, 6 Jul 2026 16:26:43 +0200
Subject: [PATCH 4/6] Fix tests according to review and resolve conflicts
cleanly
---
sdk/test/model/test_datatypes.py | 11 ++++++-----
1 file changed, 6 insertions(+), 5 deletions(-)
diff --git a/sdk/test/model/test_datatypes.py b/sdk/test/model/test_datatypes.py
index 0f7b71e27..85051bf6b 100644
--- a/sdk/test/model/test_datatypes.py
+++ b/sdk/test/model/test_datatypes.py
@@ -210,7 +210,9 @@ def test_parse_datetime(self) -> None:
model.datatypes.from_xsd("2020-01-24T15:25:17-00:20", model.datatypes.DateTime))
with self.assertRaises(ValueError) as cm:
model.datatypes.from_xsd("--2020-01-24T15:25:17-00:20", model.datatypes.DateTime)
- self.assertEqual("Value is not a valid XSD datetime string", str(cm.exception))
+ self.assertEqual(f"--2020-01-24T15:25:17-00:20 is not a valid XSD datetime.", str(cm.exception))
+ with self.assertRaises(NotImplementedError):
+ model.datatypes.from_xsd("-2020-01-24T15:25:17+01:00", model.datatypes.DateTime)
def test_serialize_datetime(self) -> None:
self.assertEqual("2020-01-24T15:25:17",
@@ -254,9 +256,8 @@ def test_parse_datetime_midnight_24_invalid(self) -> None:
with self.assertRaises(ValueError) as cm:
model.datatypes.from_xsd("2020-01-24T24:01:00", model.datatypes.DateTime)
self.assertEqual(
- "Invalid time: 24:00:00.000000 is the only valid representation of midnight",
- str(cm.exception)
- )
+ "2020-01-24T24:01:00 is not a valid xsd:datetime.",
+ str(cm.exception))
def test_parse_time_midnight_24(self) -> None:
res = model.datatypes.from_xsd("24:00:00", model.datatypes.Time)
@@ -266,7 +267,7 @@ def test_parse_time_midnight_24_invalid(self) -> None:
with self.assertRaises(ValueError) as cm:
model.datatypes.from_xsd("24:00:01", model.datatypes.Time)
self.assertEqual(
- "Invalid time: 24:00:00.000000 is the only valid representation of midnight",
+ "24:00:01 is not a valid xsd:time.",
str(cm.exception)
)
From 683f5bcb6588ff75e0d3653f4308783cc3e628b1 Mon Sep 17 00:00:00 2001
From: Paul Gerber
Date: Thu, 9 Jul 2026 09:52:05 +0200
Subject: [PATCH 5/6] adapt error messages
---
sdk/basyx/aas/model/datatypes.py | 6 +++---
sdk/test/model/test_datatypes.py | 2 +-
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/sdk/basyx/aas/model/datatypes.py b/sdk/basyx/aas/model/datatypes.py
index b6cf7ae01..4492e4392 100644
--- a/sdk/basyx/aas/model/datatypes.py
+++ b/sdk/basyx/aas/model/datatypes.py
@@ -612,7 +612,7 @@ def _parse_xsd_date(value: str) -> Date:
def _parse_xsd_datetime(value: str) -> DateTime:
match = DATETIME_RE.match(value)
if not match:
- raise ValueError("Value is not a valid XSD datetime string")
+ raise ValueError(f"{value} is not a valid XSD datetime string")
if match[1]:
raise NotImplementedError("Negative dates are not supported: Python stdlib datetime requires year >= 1. "
"Report at https://github.com/eclipse-basyx/basyx-python-sdk/issues")
@@ -633,12 +633,12 @@ def _parse_xsd_datetime(value: str) -> DateTime:
def _parse_xsd_time(value: str) -> Time:
match = TIME_RE.match(value)
if not match:
- raise ValueError("Value is not a valid XSD datetime string")
+ raise ValueError(f"{value} is not a valid XSD time string")
microseconds = int(float(match[4]) * 1e6) if match[4] else 0
hour = int(match[1])
if hour == 24:
if int(match[2]) != 0 or int(match[3]) != 0 or microseconds != 0:
- raise ValueError("Invalid time: 24:00:00.000000 is the only valid representation of midnight")
+ raise ValueError(f"{value} is not a valid xsd:time.")
hour = 0
return Time(hour, int(match[2]), int(match[3]), microseconds, _parse_xsd_date_tzinfo(match[5]))
diff --git a/sdk/test/model/test_datatypes.py b/sdk/test/model/test_datatypes.py
index 6ec86c6a1..0b7541110 100644
--- a/sdk/test/model/test_datatypes.py
+++ b/sdk/test/model/test_datatypes.py
@@ -212,7 +212,7 @@ def test_parse_datetime(self) -> None:
model.datatypes.from_xsd("2020-01-24T15:25:17-00:20", model.datatypes.DateTime))
with self.assertRaises(ValueError) as cm:
model.datatypes.from_xsd("--2020-01-24T15:25:17-00:20", model.datatypes.DateTime)
- self.assertEqual("Value is not a valid XSD datetime string", str(cm.exception))
+ self.assertEqual("--2020-01-24T15:25:17-00:20 is not a valid XSD datetime string", str(cm.exception))
with self.assertRaises(NotImplementedError):
model.datatypes.from_xsd("-2020-01-24T15:25:17+01:00", model.datatypes.DateTime)
From fe83cae36404b7a0afdc575ac012868ef7fe80db Mon Sep 17 00:00:00 2001
From: Paul Gerber
Date: Thu, 9 Jul 2026 09:59:21 +0200
Subject: [PATCH 6/6] Add comments
---
sdk/basyx/aas/model/datatypes.py | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/sdk/basyx/aas/model/datatypes.py b/sdk/basyx/aas/model/datatypes.py
index 4492e4392..6bd72f39e 100644
--- a/sdk/basyx/aas/model/datatypes.py
+++ b/sdk/basyx/aas/model/datatypes.py
@@ -618,13 +618,16 @@ def _parse_xsd_datetime(value: str) -> DateTime:
"Report at https://github.com/eclipse-basyx/basyx-python-sdk/issues")
microseconds = int(float(match[8]) * 1e6) if match[8] else 0
hour = int(match[5])
+ # xsd_datetime allows for hour=24 to represent midnight,
+ # Python's datetime.DateTime doesn't.
+ # If we get an hour=24, we accept and parse it as hour=0 of the next day.
+ # See: https://github.com/eclipse-basys/basys-python-sdk/issues/564
is_midnight_24 = False
if hour == 24:
if int(match[6]) != 0 or int(match[7]) != 0 or microseconds != 0:
raise ValueError(f"{value} is not a valid xsd:datetime.")
hour = 0
is_midnight_24 = True
-
res = DateTime(int(match[2]), int(match[3]), int(match[4]), hour, int(match[6]), int(match[7]),
microseconds, _parse_xsd_date_tzinfo(match[9]))
return res + datetime.timedelta(days=1) if is_midnight_24 else res
@@ -636,6 +639,10 @@ def _parse_xsd_time(value: str) -> Time:
raise ValueError(f"{value} is not a valid XSD time string")
microseconds = int(float(match[4]) * 1e6) if match[4] else 0
hour = int(match[1])
+ # xsd_time allows for hour=24 to represent midnight,
+ # Python's datetime.Time doesn't.
+ # If we get an hour=24, we accept and parse it as hour=0.
+ # See: https://github.com/eclipse-basys/basys-python-sdk/issues/564
if hour == 24:
if int(match[2]) != 0 or int(match[3]) != 0 or microseconds != 0:
raise ValueError(f"{value} is not a valid xsd:time.")