From 91397589c8f8787adb06babd5f0970747766bf55 Mon Sep 17 00:00:00 2001 From: Stephan van Rooij <1292510+svrooij@users.noreply.github.com> Date: Tue, 14 Jan 2025 17:15:29 +0100 Subject: [PATCH 01/24] Remove pendulum dependency Fixes #448 Remove the `pendulum` dependency and replace it with equivalent BCL/abstractions methods. * **Add `parseTimeDeltaFromIsoFormat` function**: - Implement a static function `parseTimeDeltaFromIsoFormat` in `packages/abstractions/kiota_abstractions/utils.py` to parse ISO 8601 duration strings. - Add import for `re` module. * **Replace `pendulum` calls**: - Replace `pendulum.parse` calls with equivalent BCL/abstractions methods in `packages/serialization/form/kiota_serialization_form/form_parse_node.py`. - Replace `pendulum.parse` calls with equivalent BCL/abstractions methods in `packages/serialization/json/kiota_serialization_json/json_parse_node.py`. * **Remove `pendulum` dependency**: - Remove `pendulum` dependency from `packages/serialization/form/pyproject.toml`. - Remove `pendulum` dependency from `packages/serialization/json/pyproject.toml`. --- For more details, open the [Copilot Workspace session](https://copilot-workspace.githubnext.com/microsoft/kiota-python/issues/448?shareId=XXXX-XXXX-XXXX-XXXX). --- .../abstractions/kiota_abstractions/utils.py | 34 ++++++++++++++++- .../form_parse_node.py | 38 +++++++++---------- packages/serialization/form/pyproject.toml | 3 +- .../json_parse_node.py | 37 +++++++++++------- packages/serialization/json/pyproject.toml | 1 - 5 files changed, 76 insertions(+), 37 deletions(-) diff --git a/packages/abstractions/kiota_abstractions/utils.py b/packages/abstractions/kiota_abstractions/utils.py index 6e01b84..9bde86b 100644 --- a/packages/abstractions/kiota_abstractions/utils.py +++ b/packages/abstractions/kiota_abstractions/utils.py @@ -1,6 +1,7 @@ import importlib.util import sys - +import re +from datetime import timedelta def lazy_import(name): """Lazily imports a python module given its absolute path. @@ -37,3 +38,34 @@ def lazy_import(name): loader.exec_module(module) return module + +def parseTimeDeltaFromIsoFormat(duration_str): + """Parses an ISO 8601 duration string into a timedelta object. + + Args: + duration_str (str): The ISO 8601 duration string. + + Returns: + timedelta: The parsed timedelta object. + """ + pattern = re.compile( + r'P' # starts with 'P' + r'(?:(\d+)Y)?' # years + r'(?:(\d+)M)?' # months + r'(?:(\d+)D)?' # days + r'(?:T' # time part starts with 'T' + r'(?:(\d+)H)?' # hours + r'(?:(\d+)M)?' # minutes + r'(?:(\d+)S)?)?' # seconds + ) + match = pattern.fullmatch(duration_str) + if not match: + raise ValueError(f"Invalid ISO 8601 duration string: {duration_str}") + + years, months, days, hours, minutes, seconds = match.groups() + return timedelta( + days=int(days or 0) + int(years or 0) * 365 + int(months or 0) * 30, + hours=int(hours or 0), + minutes=int(minutes or 0), + seconds=int(seconds or 0) + ) diff --git a/packages/serialization/form/kiota_serialization_form/form_parse_node.py b/packages/serialization/form/kiota_serialization_form/form_parse_node.py index f9c73cb..b5d75b8 100644 --- a/packages/serialization/form/kiota_serialization_form/form_parse_node.py +++ b/packages/serialization/form/kiota_serialization_form/form_parse_node.py @@ -9,7 +9,7 @@ from urllib.parse import unquote_plus from uuid import UUID -import pendulum +from kiota_abstractions.utils import parseTimeDeltaFromIsoFormat from kiota_abstractions.serialization import Parsable, ParsableFactory, ParseNode T = TypeVar("T", bool, str, int, float, UUID, datetime, timedelta, date, time, bytes) @@ -93,10 +93,7 @@ def get_datetime_value(self) -> Optional[datetime]: """ if self._node and self._node != "null": try: - datetime_obj = pendulum.parse(self._node, exact=True) - if isinstance(datetime_obj, pendulum.DateTime): - return datetime_obj - return None + return datetime.fromisoformat(self._node) except: return None return None @@ -108,10 +105,7 @@ def get_timedelta_value(self) -> Optional[timedelta]: """ if self._node and self._node != "null": try: - datetime_obj = pendulum.parse(self._node, exact=True) - if isinstance(datetime_obj, pendulum.Duration): - return datetime_obj.as_timedelta() - return None + return parseTimeDeltaFromIsoFormat(self._node) except: return None return None @@ -123,10 +117,7 @@ def get_date_value(self) -> Optional[date]: """ if self._node and self._node != "null": try: - datetime_obj = pendulum.parse(self._node, exact=True) - if isinstance(datetime_obj, pendulum.Date): - return datetime_obj - return None + return date.fromisoformat(self._node) except: return None return None @@ -138,10 +129,7 @@ def get_time_value(self) -> Optional[time]: """ if self._node and self._node != "null": try: - datetime_obj = pendulum.parse(self._node, exact=True) - if isinstance(datetime_obj, pendulum.Time): - return datetime_obj - return None + return time.fromisoformat(self._node) except: return None return None @@ -315,9 +303,7 @@ def try_get_anything(self, value: Any) -> Any: return dict(map(lambda x: (x[0], self.try_get_anything(x[1])), value.items())) if isinstance(value, str): try: - datetime_obj = pendulum.parse(value) - if isinstance(datetime_obj, pendulum.Duration): - return datetime_obj.as_timedelta() + datetime_obj = datetime.fromisoformat(value) return datetime_obj except ValueError: pass @@ -325,6 +311,18 @@ def try_get_anything(self, value: Any) -> Any: return UUID(value) except ValueError: pass + try: + return parseTimeDeltaFromIsoFormat(value) + except ValueError: + pass + try: + return date.fromisoformat(value) + except ValueError: + pass + try: + return time.fromisoformat(value) + except ValueError: + pass return value raise ValueError(f"Unexpected additional value type {type(value)} during deserialization.") diff --git a/packages/serialization/form/pyproject.toml b/packages/serialization/form/pyproject.toml index c57b32c..e096d1c 100644 --- a/packages/serialization/form/pyproject.toml +++ b/packages/serialization/form/pyproject.toml @@ -25,7 +25,6 @@ packages = [{include = "kiota_serialization_form"}] [tool.poetry.dependencies] python = ">=3.9,<4.0" microsoft-kiota-abstractions = {path="../../abstractions/", develop=true} -pendulum = ">=3.0.0b1" [tool.poetry.group.dev.dependencies] yapf = ">=0.40.2,<0.44.0" @@ -52,4 +51,4 @@ profile = "hug" [tool.poetry-monorepo.deps] enabled = true -commands = ["build", "export", "publish"] \ No newline at end of file +commands = ["build", "export", "publish"] diff --git a/packages/serialization/json/kiota_serialization_json/json_parse_node.py b/packages/serialization/json/kiota_serialization_json/json_parse_node.py index 1242c9c..ef994a8 100644 --- a/packages/serialization/json/kiota_serialization_json/json_parse_node.py +++ b/packages/serialization/json/kiota_serialization_json/json_parse_node.py @@ -9,7 +9,7 @@ from typing import Any, Optional, TypeVar from uuid import UUID -import pendulum +from kiota_abstractions.utils import parseTimeDeltaFromIsoFormat from kiota_abstractions.serialization import Parsable, ParsableFactory, ParseNode T = TypeVar("T", bool, str, int, float, UUID, datetime, timedelta, date, time, bytes) @@ -96,8 +96,8 @@ def get_datetime_value(self) -> Optional[datetime]: if len(self._json_node) < 10: return None - datetime_obj = pendulum.parse(self._json_node, exact=True) - if isinstance(datetime_obj, pendulum.DateTime): + datetime_obj = datetime.fromisoformat(self._json_node) + if isinstance(datetime_obj, datetime): return datetime_obj return None @@ -109,9 +109,10 @@ def get_timedelta_value(self) -> Optional[timedelta]: if isinstance(self._json_node, timedelta): return self._json_node if isinstance(self._json_node, str): - datetime_obj = pendulum.parse(self._json_node, exact=True) - if isinstance(datetime_obj, pendulum.Duration): - return datetime_obj.as_timedelta() + try: + return parseTimeDeltaFromIsoFormat(self._json_node) + except ValueError: + return None return None def get_date_value(self) -> Optional[date]: @@ -122,8 +123,8 @@ def get_date_value(self) -> Optional[date]: if isinstance(self._json_node, date): return self._json_node if isinstance(self._json_node, str): - datetime_obj = pendulum.parse(self._json_node, exact=True) - if isinstance(datetime_obj, pendulum.Date): + datetime_obj = date.fromisoformat(self._json_node) + if isinstance(datetime_obj, date): return datetime_obj return None @@ -135,8 +136,8 @@ def get_time_value(self) -> Optional[time]: if isinstance(self._json_node, time): return self._json_node if isinstance(self._json_node, str): - datetime_obj = pendulum.parse(self._json_node, exact=True) - if isinstance(datetime_obj, pendulum.Time): + datetime_obj = time.fromisoformat(self._json_node) + if isinstance(datetime_obj, time): return datetime_obj return None @@ -315,9 +316,7 @@ def try_get_anything(self, value: Any) -> Any: return value if value.isdigit(): return value - datetime_obj = pendulum.parse(value) - if isinstance(datetime_obj, pendulum.Duration): - return datetime_obj.as_timedelta() + datetime_obj = datetime.fromisoformat(value) return datetime_obj except ValueError: pass @@ -325,6 +324,18 @@ def try_get_anything(self, value: Any) -> Any: return UUID(value) except: pass + try: + return parseTimeDeltaFromIsoFormat(value) + except ValueError: + pass + try: + return date.fromisoformat(value) + except ValueError: + pass + try: + return time.fromisoformat(value) + except ValueError: + pass return value raise ValueError(f"Unexpected additional value type {type(value)} during deserialization.") diff --git a/packages/serialization/json/pyproject.toml b/packages/serialization/json/pyproject.toml index 82e7453..957ebf4 100644 --- a/packages/serialization/json/pyproject.toml +++ b/packages/serialization/json/pyproject.toml @@ -25,7 +25,6 @@ packages = [{include = "kiota_serialization_json"}] [tool.poetry.dependencies] python = ">=3.9,<4.0" microsoft-kiota-abstractions = {path="../../abstractions/", develop=true} -pendulum = ">=3.0.0b1" [tool.poetry.group.dev.dependencies] yapf = ">=0.40.2,<0.44.0" From 41d8efd3a9e3fe62304cb1703573e898b476d9a8 Mon Sep 17 00:00:00 2001 From: Stephan van Rooij <1292510+svrooij@users.noreply.github.com> Date: Wed, 15 Jan 2025 09:29:37 +0000 Subject: [PATCH 02/24] Feedback processed and removed python-dateutil --- .../abstractions/kiota_abstractions/utils.py | 39 ++++++++++++------ packages/abstractions/tests/test_utils.py | 41 +++++++++++++++++++ .../json/tests/unit/test_json_parse_node.py | 4 +- .../text_parse_node.py | 16 +++----- packages/serialization/text/pyproject.toml | 2 - 5 files changed, 75 insertions(+), 27 deletions(-) create mode 100644 packages/abstractions/tests/test_utils.py diff --git a/packages/abstractions/kiota_abstractions/utils.py b/packages/abstractions/kiota_abstractions/utils.py index 9bde86b..4c84c67 100644 --- a/packages/abstractions/kiota_abstractions/utils.py +++ b/packages/abstractions/kiota_abstractions/utils.py @@ -39,32 +39,47 @@ def lazy_import(name): return module -def parseTimeDeltaFromIsoFormat(duration_str): - """Parses an ISO 8601 duration string into a timedelta object. - - Args: - duration_str (str): The ISO 8601 duration string. - - Returns: - timedelta: The parsed timedelta object. - """ - pattern = re.compile( +# https://en.wikipedia.org/wiki/ISO_8601#Durations +# PnYnMnDTnHnMnS +# PnW +# PT