Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove pendulum dependency #450

Merged
merged 26 commits into from
Jan 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9139758
Remove pendulum dependency
svrooij Jan 14, 2025
41d8efd
Feedback processed and removed python-dateutil
svrooij Jan 15, 2025
f2ad78e
Merge branch 'main' into remove-pendulum-dependency
svrooij Jan 15, 2025
fc6c719
Some linting
svrooij Jan 15, 2025
431b036
parse_timedelta acts exacty the same as previous library
svrooij Jan 15, 2025
3812082
Merge branch 'main' into remove-pendulum-dependency
baywet Jan 15, 2025
74fbeeb
Fix issues caused by auto format
svrooij Jan 15, 2025
e6eb346
More pendulem removed
svrooij Jan 15, 2025
e61eabb
Reduce complexity and raise error instead of return none
svrooij Jan 15, 2025
5a2205a
ci: disables fail fast to get all feedback at once
baywet Jan 15, 2025
d7af3e3
chore: minor formatting fixes
baywet Jan 15, 2025
4540b25
chore: additional formatting issue
baywet Jan 15, 2025
58a8121
chore: additional formatting issues
baywet Jan 15, 2025
f9d9e84
fix: replace calls to parser by iso format
baywet Jan 15, 2025
f4a5692
chore: additional formatting
baywet Jan 15, 2025
fedf34c
fix: multiple text failing tests
baywet Jan 15, 2025
42f1b5b
chore: additional fixture data corrections
baywet Jan 15, 2025
e9021c9
fix: additional fixes for unit test setup
baywet Jan 15, 2025
f0ed780
Support additional timedeltas
svrooij Jan 16, 2025
293ea59
Make P mandatory
svrooij Jan 16, 2025
3bea953
chore: fixes formatting
baywet Jan 16, 2025
8aaeff1
fromisoformat compatibility
svrooij Jan 17, 2025
190f9dd
chore: linting
baywet Jan 17, 2025
15b5a31
Hopefully try 50 fixes it 💣
svrooij Jan 17, 2025
1373232
More 🕑 compatibility
svrooij Jan 17, 2025
f1b515f
Fixing datetime parsing in test 🧪
svrooij Jan 17, 2025
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
1 change: 1 addition & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ jobs:
timeout-minutes: 40
strategy:
max-parallel: 10
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12"]
library :
Expand Down
119 changes: 119 additions & 0 deletions packages/abstractions/kiota_abstractions/date_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
from sys import version_info as sys_version_info
import re
from datetime import datetime, time, timedelta

_ISO8601_DURATION_PATTERN = re.compile(
"^P" # Duration P indicator
# Weeks
"(?P<w>"
r" (?P<weeks>\d+(?:[.,]\d+)?W)"
svrooij marked this conversation as resolved.
Show resolved Hide resolved
")?"
# Years, Months, Days
"(?P<ymd>"
r" (?P<years>\d+(?:[.,]\d+)?Y)?"
r" (?P<months>\d+(?:[.,]\d+)?M)?"
r" (?P<days>\d+(?:[.,]\d+)?D)?"
")?"
# Time
"(?P<hms>"
" (?P<timesep>T)" # Separator (T)
r" (?P<hours>\d+(?:[.,]\d+)?H)?"
r" (?P<minutes>\d+(?:[.,]\d+)?M)?"
r" (?P<seconds>\d+(?:[.,]\d+)?S)?"
")?"
"$",
re.VERBOSE,
)


def parse_timedelta_from_iso_format(text: str) -> timedelta:
"""Parses a ISO8601 duration string into a timedelta object."""

m = _ISO8601_DURATION_PATTERN.match(text)
if not m:
raise ValueError(f"Invalid ISO8601 duration string: {text}")

weeks = float(m.group("weeks").replace(",", ".").replace("W", "")) if m.group("weeks") else 0
years = float(m.group("years").replace(",", ".").replace("Y", "")) if m.group("years") else 0
months = float(m.group("months").replace(",", ".").replace("M", "")) if m.group("months") else 0
days = float(m.group("days").replace(",", ".").replace("D", "")) if m.group("days") else 0
hours = float(m.group("hours").replace(",", ".").replace("H", "")) if m.group("hours") else 0
minutes = float(m.group("minutes").replace(",", ".").replace("M", "")
) if m.group("minutes") else 0
seconds = float(m.group("seconds").replace(",", ".").replace("S", "")
) if m.group("seconds") else 0
_have_date = years or months or days
_have_time = hours or minutes or seconds
if weeks and (_have_date or _have_time):
raise ValueError("Combining weeks with other date/time parts is not supported")

_total_days = (years * 365) + (months * 30) + days
return timedelta(
days=_total_days,
hours=hours,
minutes=minutes,
seconds=seconds,
weeks=weeks,
)


_TIMEDELTA_PATTERN = re.compile(r"^(?P<hours>\d+):(?P<minutes>\d+)(?::(?P<seconds>\d+))?$")


def parse_timedelta_string(text: str) -> timedelta:
"""Checks if a given string is a valid ISO8601 duration string. Or hh:mm:ss format."""
try:
return parse_timedelta_from_iso_format(text)
except ValueError as exc:
# The previous library also supported hh:mm:ss format
m = _TIMEDELTA_PATTERN.match(text)
if not m:
raise ValueError(f"Invalid timedelta string: {text}") from exc

hours = int(m.group("hours"))
minutes = int(m.group("minutes"))
seconds = int(m.group("seconds") or 0)
return timedelta(hours=hours, minutes=minutes, seconds=seconds)


_TIME_REPLACEMENT_PATTERN = re.compile(r'(\d)([.,])(\d+)')


def datetime_from_iso_format_compat(text: str) -> datetime:
"""Parses a ISO8601 formatted string into a datetime object."""
try:
# Try regular first (faster for most cases)
return datetime.fromisoformat(text)
except ValueError as exc:
# Python 3.10 and below only support fractions of seconds in either 3 or 6 digits
# Python 3.11+ supports any number of digits

if sys_version_info[:3] < (3, 11):
# The following code is a workaround for Python 3.10 and below
fixed_time = re.sub(
_TIME_REPLACEMENT_PATTERN,
lambda x: x.group(1) + "." + x.group(3).ljust(6, '0')[:6], text
).replace("Z", "+00:00")
return datetime.fromisoformat(fixed_time)

raise exc


def time_from_iso_format_compat(text: str) -> time:
"""Parses a ISO8601 formatted string into a time object."""
try:
# Try regular first (faster for most cases)
return time.fromisoformat(text)
except ValueError as exc:
# Python 3.10 and below only support fractions of seconds in either 3 or 6 digits
# Python 3.11+ supports any number of digits

if sys_version_info[:3] < (3, 11):
# The following code is a workaround for Python 3.10 and below
fixed_time = re.sub(
_TIME_REPLACEMENT_PATTERN,
lambda x: x.group(1) + "." + x.group(3).ljust(6, '0')[:6], text
).replace("Z", "+00:00")
return time.fromisoformat(fixed_time)

raise exc
86 changes: 86 additions & 0 deletions packages/abstractions/tests/test_date_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import pytest

from kiota_abstractions.date_utils import (
parse_timedelta_from_iso_format,
parse_timedelta_string,
time_from_iso_format_compat,
datetime_from_iso_format_compat
)


@pytest.mark.parametrize("text", ["08:00:00", "08:00:00.0", "08:00:00.00","08:00:00.000",
"08:00:00.0000","08:00:00.00000","08:00:00.000000", "08:00:00.0000000", "08:00:00,0000000",
"08:00:00,0000000Z", "08:00:00.00Z", "08:00:00.00+00:00" ])
def test_time_from_iso_format_compat(text: str):
result = time_from_iso_format_compat(text)
assert result.hour == 8
assert result.minute == 0
assert result.second == 0

@pytest.mark.parametrize("text", ["1986-07-28T08:00:00", "1986-07-28T08:00:00.0", "1986-07-28T08:00:00.00",
"1986-07-28T08:00:00.000", "1986-07-28T08:00:00.0000", "1986-07-28T08:00:00.00000",
"1986-07-28T08:00:00.000000", "1986-07-28T08:00:00.0000000", "1986-07-28T08:00:00,0000000",
"1986-07-28T08:00:00.0000000Z", "1986-07-28T08:00:00.00Z", "1986-07-28T08:00:00.00+00:00" ])
def test_datetime_from_iso_format_compat(text: str):
result = datetime_from_iso_format_compat(text)
assert result.hour == 8
assert result.minute == 0
assert result.second == 0


def test_parse_timedelta_from_iso_format_weeks():
result = parse_timedelta_from_iso_format("P3W")
assert result.days == 21


def test_parse_timedelta_from_iso_format_days():
result = parse_timedelta_from_iso_format("P3D")
assert result.days == 3


def test_parse_timedelta_from_iso_format_hours():
result = parse_timedelta_from_iso_format("PT3H")
assert result.seconds == 10800


def test_parse_timedelta_from_iso_format_minutes():
result = parse_timedelta_from_iso_format("PT3M")
assert result.seconds == 180


def test_parse_timedelta_from_iso_format_seconds():
result = parse_timedelta_from_iso_format("PT3S")
assert result.seconds == 3


def test_parse_timedelta_from_iso_format_years():
result = parse_timedelta_from_iso_format("P3Y")
assert result.days == 1095


def test_parse_timedelta_from_iso_format_months():
result = parse_timedelta_from_iso_format("P3M")
assert result.days == 90


def test_parse_timedelta_from_iso_format_days_and_time():
result = parse_timedelta_from_iso_format("P3DT3H3M3S")
assert result.days == 3
assert result.seconds == 10983

def test_parse_timedelta_from_iso_format_time_without_p():
with pytest.raises(ValueError):
parse_timedelta_from_iso_format("T3H3M3S")

@pytest.mark.parametrize("text", ["P3W3Y", "P3W3Y3D", "P3W3Y3DT3H3M3S"])
def test_parse_timedelta_from_iso_format_must_raise(text: str):
# assert this raises a ValueError
with pytest.raises(ValueError):
parse_timedelta_from_iso_format(text)


@pytest.mark.parametrize("text, expected_hours", [("PT3H", 3), ("2:00:00", 2)])
def test_parse_timedelta_string_valid(text:str, expected_hours:int):
result = parse_timedelta_string(text)
assert result.days == 0
assert result.seconds == expected_hours * 3600
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
from urllib.parse import unquote_plus
from uuid import UUID

import pendulum
from kiota_abstractions.date_utils import (
parse_timedelta_string, datetime_from_iso_format_compat, time_from_iso_format_compat
)
from kiota_abstractions.serialization import Parsable, ParsableFactory, ParseNode

T = TypeVar("T", bool, str, int, float, UUID, datetime, timedelta, date, time, bytes)
Expand Down Expand Up @@ -93,10 +95,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_from_iso_format_compat(self._node)
except:
return None
return None
Expand All @@ -108,10 +107,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 parse_timedelta_string(self._node)
except:
return None
return None
Expand All @@ -123,10 +119,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
Expand All @@ -138,10 +131,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_from_iso_format_compat(self._node)
except:
return None
return None
Expand Down Expand Up @@ -315,16 +305,26 @@ 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_from_iso_format_compat(value)
return datetime_obj
except ValueError:
pass
try:
return UUID(value)
except ValueError:
pass
try:
return parse_timedelta_string(value)
except ValueError:
pass
try:
return date.fromisoformat(value)
except ValueError:
pass
try:
return time_from_iso_format_compat(value)
except ValueError:
pass
return value
raise ValueError(f"Unexpected additional value type {type(value)} during deserialization.")

Expand Down
3 changes: 1 addition & 2 deletions packages/serialization/form/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.0"

[tool.poetry.group.dev.dependencies]
yapf = ">=0.40.2,<0.44.0"
Expand All @@ -52,4 +51,4 @@ profile = "hug"

[tool.poetry-monorepo.deps]
enabled = true
commands = ["build", "export", "publish"]
commands = ["build", "export", "publish"]
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
from urllib.parse import unquote_plus
import pytest

import pendulum
from datetime import datetime, timedelta, date, time
from kiota_serialization_form.form_serialization_writer import FormSerializationWriter
from ..helpers import TestEntity, TestEnum
Expand All @@ -11,7 +10,7 @@
@pytest.fixture
def user_1():
user = TestEntity()
user.created_date_time = pendulum.parse("2022-01-27T12:59:45.596117")
user.created_date_time = datetime.fromisoformat("2022-01-27T12:59:45.596117+00:00")
user.work_duration = timedelta(seconds=7200)
user.birthday = date(year=2000,month=9,day=4)
user.start_work_time = time(hour=8, minute=0, second=0)
Expand Down Expand Up @@ -112,7 +111,7 @@ def test_write_time_value():
form_serialization_writer = FormSerializationWriter()
form_serialization_writer.write_time_value(
"time",
pendulum.parse('2022-01-27T12:59:45.596117').time()
datetime.fromisoformat('2022-01-27T12:59:45.596117').time()
)
content = form_serialization_writer.get_serialized_content()
content_string = content.decode('utf-8')
Expand Down
Loading
Loading