From b2fc46e6ac29bccbbc9a75ecfbc2659db0bc5b95 Mon Sep 17 00:00:00 2001 From: hpoeche Date: Mon, 24 Aug 2026 18:54:40 +0200 Subject: [PATCH 1/4] sdk: Allow 4+ digit year in xs:dateTime Previously, the deserialization required values in formates `xs:dateTime` and `xs:date` to consists of exactly four digits for the year. However, the XML Schema 1.0, which is defined as reference, allow for more digits if the leading digit is non-zero. These changes adapt the regex used for deserialization of both datatypes to also allow more than four digits for year numbers if no leading zeros are used. If only four digits are uesd, leading zeros are still allowed. Fixes #616 --- sdk/basyx/aas/model/datatypes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/basyx/aas/model/datatypes.py b/sdk/basyx/aas/model/datatypes.py index 5c59db17..8ade5e3e 100644 --- a/sdk/basyx/aas/model/datatypes.py +++ b/sdk/basyx/aas/model/datatypes.py @@ -695,10 +695,10 @@ def from_xsd( r"^(-?)P(\d+Y)?(\d+M)?(\d+D)?(T(\d+H)?(\d+M)?((\d+)(\.\d+)?S)?)?$" ) DATETIME_RE = re.compile( - r"^(-?)(\d\d\d\d)-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)(\.\d+)?([+\-](\d\d):(\d\d)|Z)?$" + r"^(-?)([1-9]\d{4,}|\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)(\.\d+)?([+\-](\d\d):(\d\d)|Z)?$" ) TIME_RE = re.compile(r"^(\d\d):(\d\d):(\d\d)(\.\d+)?([+\-](\d\d):(\d\d)|Z)?$") -DATE_RE = re.compile(r"^(-?)(\d\d\d\d)-(\d\d)-(\d\d)([+\-](\d\d):(\d\d)|Z)?$") +DATE_RE = re.compile(r"^(-?)([1-9]\d{4,}|\d{4})-(\d\d)-(\d\d)([+\-](\d\d):(\d\d)|Z)?$") def _parse_xsd_duration(value: str) -> Duration: From 0176bd438c936dc8eda3c8f0177474a0a7e24ce5 Mon Sep 17 00:00:00 2001 From: hpoeche Date: Thu, 27 Aug 2026 08:52:08 +0200 Subject: [PATCH 2/4] raise `NotImplementedError` on out-of-range year Similar to handling of negative dates in #586 --- sdk/basyx/aas/model/datatypes.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/sdk/basyx/aas/model/datatypes.py b/sdk/basyx/aas/model/datatypes.py index 8ade5e3e..61554fb2 100644 --- a/sdk/basyx/aas/model/datatypes.py +++ b/sdk/basyx/aas/model/datatypes.py @@ -739,6 +739,12 @@ def _parse_xsd_date(value: str) -> Date: "Negative dates are not supported: Python stdlib datetime requires year >= 1. " "Report at https://github.com/eclipse-basyx/basyx-python-sdk/issues" ) + year = int(match[2]) + if year > datetime.MAXYEAR: + raise NotImplementedError( + "Year of date exceeds Python datetime.MAXYEAR " + "Report at https://github.com/eclipse-basyx/basyx-python-sdk/issues" + ) return Date( year=int(match[2]), month=int(match[3]), @@ -756,6 +762,12 @@ def _parse_xsd_datetime(value: str) -> DateTime: "Negative dates are not supported: Python stdlib datetime requires year >= 1. " "Report at https://github.com/eclipse-basyx/basyx-python-sdk/issues" ) + year = int(match[2]) + if year > datetime.MAXYEAR: + raise NotImplementedError( + "Year of date exceeds Python datetime.MAXYEAR. " + "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, From 2de75de61ff63b85fdfafbc76e41f91e2715f22e Mon Sep 17 00:00:00 2001 From: hpoeche Date: Thu, 27 Aug 2026 08:54:39 +0200 Subject: [PATCH 3/4] add unit tests for out-of-range year --- sdk/test/model/test_datatypes.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/sdk/test/model/test_datatypes.py b/sdk/test/model/test_datatypes.py index df533833..baf86439 100644 --- a/sdk/test/model/test_datatypes.py +++ b/sdk/test/model/test_datatypes.py @@ -232,8 +232,13 @@ def test_parse_date(self) -> None: with self.assertRaises(ValueError) as cm: model.datatypes.from_xsd("2020-01-24+11", model.datatypes.Date) self.assertEqual("Value is not a valid XSD date string", str(cm.exception)) + with self.assertRaises(ValueError) as cm: + model.datatypes.from_xsd("02020-01-24", model.datatypes.Date) + self.assertEqual("Value is not a valid XSD date string", str(cm.exception)) with self.assertRaises(NotImplementedError): model.datatypes.from_xsd("-2020-01-24", model.datatypes.Date) + with self.assertRaises(NotImplementedError): + model.datatypes.from_xsd(f"{datetime.MAXYEAR+1}-01-24", model.datatypes.Date) def test_serialize_date(self) -> None: self.assertEqual( @@ -413,10 +418,14 @@ def test_parse_datetime(self) -> None: "--2020-01-24T15:25:17-00:20 is not a valid XSD datetime string", str(cm.exception), ) + with self.assertRaises(ValueError): + model.datatypes.from_xsd("02020-01-24T15:25:17", model.datatypes.DateTime) with self.assertRaises(NotImplementedError): model.datatypes.from_xsd( "-2020-01-24T15:25:17+01:00", model.datatypes.DateTime ) + with self.assertRaises(NotImplementedError): + model.datatypes.from_xsd(f"{datetime.MAXYEAR+1}-01-24T15:25:17", model.datatypes.DateTime) def test_serialize_datetime(self) -> None: self.assertEqual( From 372f3fd98ae9921d9bf9647ad6ee7d5457abb676 Mon Sep 17 00:00:00 2001 From: hpoeche Date: Thu, 27 Aug 2026 09:08:21 +0200 Subject: [PATCH 4/4] adapt long year handling to GYear and GYearMonth The `GYear` and `GYearMonth` lexical representation follows the one of the year part of `xs:dateTime`. Therefore the regex needs to be changed to prohibit leading zeros for 4+ digit year. The error handling in the `into_date()` method of both classes for years that exceed Python's `datetime.MAXYEAR` is similar to the one for negative dates (introduced in #586). --- sdk/basyx/aas/model/datatypes.py | 8 ++++++-- sdk/test/model/test_datatypes.py | 12 ++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/sdk/basyx/aas/model/datatypes.py b/sdk/basyx/aas/model/datatypes.py index 61554fb2..fb4f82e9 100644 --- a/sdk/basyx/aas/model/datatypes.py +++ b/sdk/basyx/aas/model/datatypes.py @@ -121,6 +121,8 @@ def into_date(self, day: int = 1) -> Date: raise ValueError( "Negative years are not supported by Python's `datetime` library." ) from e + if self.year > datetime.MAXYEAR: + raise ValueError("Year of date exceeds Python's datetime.MAXYEAR.") from e raise e @classmethod @@ -157,6 +159,8 @@ def into_date(self, month: int = 1, day: int = 1) -> Date: raise ValueError( "Negative years are not supported by Python's `datetime` library." ) from e + if self.year > datetime.MAXYEAR: + raise ValueError("Year of date exceeds Python's datetime.MAXYEAR.") from e raise e @classmethod @@ -825,10 +829,10 @@ def _parse_xsd_bool(value: str) -> Boolean: raise ValueError("Invalid literal for XSD bool type") -GYEAR_RE = re.compile(r"^(-?)(\d{4,})([+\-]\d\d:\d\d|Z)?$") +GYEAR_RE = re.compile(r"^(-?)([1-9]\d{4,}|\d{4})([+\-]\d\d:\d\d|Z)?$") GMONTH_RE = re.compile(r"^--(\d\d)([+\-]\d\d:\d\d|Z)?$") GDAY_RE = re.compile(r"^---(\d\d)([+\-]\d\d:\d\d|Z)?$") -GYEARMONTH_RE = re.compile(r"^(-?)(\d{4,})-(\d\d)([+\-]\d\d:\d\d|Z)?$") +GYEARMONTH_RE = re.compile(r"^(-?)([1-9]\d{4,}|\d{4})-(\d\d)([+\-]\d\d:\d\d|Z)?$") GMONTHDAY_RE = re.compile(r"^--(\d\d)-(\d\d)([+\-]\d\d:\d\d|Z)?$") diff --git a/sdk/test/model/test_datatypes.py b/sdk/test/model/test_datatypes.py index baf86439..7308ba7e 100644 --- a/sdk/test/model/test_datatypes.py +++ b/sdk/test/model/test_datatypes.py @@ -308,9 +308,15 @@ def test_parse_partial_dates(self) -> None: with self.assertRaises(ValueError) as cm: model.datatypes.from_xsd("10", model.datatypes.GYear) self.assertEqual("Value is not a valid XSD GYear string", str(cm.exception)) + with self.assertRaises(ValueError) as cm: + model.datatypes.from_xsd("02010", model.datatypes.GYear) + self.assertEqual("Value is not a valid XSD GYear string", str(cm.exception)) with self.assertRaises(ValueError) as cm: model.datatypes.from_xsd("25-10", model.datatypes.GMonthDay) self.assertEqual("Value is not a valid XSD GMonthDay string", str(cm.exception)) + with self.assertRaises(ValueError) as cm: + model.datatypes.from_xsd("02025-10", model.datatypes.GMonthDay) + self.assertEqual("Value is not a valid XSD GMonthDay string", str(cm.exception)) with self.assertRaises(ValueError) as cm: model.datatypes.from_xsd("10-10", model.datatypes.GYearMonth) self.assertEqual( @@ -331,6 +337,12 @@ def test_partial_dates_negative_year_into_date(self) -> None: str(cm.exception), ) + def test_partial_dates_max_year_into_date(self) -> None: + for value in (model.datatypes.GYear(datetime.MAXYEAR+1), model.datatypes.GYearMonth(datetime.MAXYEAR+1, 1)): + with self.assertRaises(ValueError) as cm: + value.into_date() + self.assertEqual("Year of date exceeds Python's datetime.MAXYEAR.", str(cm.exception)) + def test_copy_date(self) -> None: date = model.datatypes.Date(2020, 1, 24) date_copy_shallow = copy.copy(date)