Skip to content

Commit 66d9a09

Browse files
committed
Replace pendulum with whenever
1 parent 1a6f3cd commit 66d9a09

File tree

8 files changed

+255
-170
lines changed

8 files changed

+255
-170
lines changed

infrahub_sdk/ctl/branch.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
from rich.table import Table
66

77
from ..async_typer import AsyncTyper
8-
from ..ctl.client import initialize_client
9-
from ..ctl.utils import calculate_time_diff, catch_exception
8+
from ..utils import calculate_time_diff
9+
from .client import initialize_client
1010
from .parameters import CONFIG_PARAM
11+
from .utils import catch_exception
1112

1213
app = AsyncTyper()
1314
console = Console()

infrahub_sdk/ctl/utils.py

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,9 @@
88
from pathlib import Path
99
from typing import TYPE_CHECKING, Any, Callable, NoReturn, Optional, TypeVar
1010

11-
import pendulum
1211
import typer
1312
from click.exceptions import Exit
1413
from httpx import HTTPError
15-
from pendulum.datetime import DateTime
1614
from rich.console import Console
1715
from rich.logging import RichHandler
1816
from rich.markup import escape
@@ -152,20 +150,6 @@ def parse_cli_vars(variables: Optional[list[str]]) -> dict[str, str]:
152150
return {var.split("=")[0]: var.split("=")[1] for var in variables if "=" in var}
153151

154152

155-
def calculate_time_diff(value: str) -> str | None:
156-
"""Calculate the time in human format between a timedate in string format and now."""
157-
try:
158-
time_value = pendulum.parse(value)
159-
except pendulum.parsing.exceptions.ParserError:
160-
return None
161-
162-
if not isinstance(time_value, DateTime):
163-
return None
164-
165-
pendulum.set_locale("en")
166-
return time_value.diff_for_humans(other=pendulum.now(), absolute=True)
167-
168-
169153
def find_graphql_query(name: str, directory: str | Path = ".") -> str:
170154
if isinstance(directory, str):
171155
directory = Path(directory)

infrahub_sdk/timestamp.py

Lines changed: 63 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
from __future__ import annotations
22

33
import re
4+
import warnings
5+
from datetime import datetime, timezone
46

5-
import pendulum
6-
from pendulum.datetime import DateTime
7+
from whenever import Date, Instant, LocalDateTime, Time, ZonedDateTime
8+
9+
UTC = timezone.utc # Required for older versions of Python
710

811
REGEX_MAPPING = {
912
"seconds": r"(\d+)(s|sec|second|seconds)",
@@ -16,76 +19,107 @@ class TimestampFormatError(ValueError): ...
1619

1720

1821
class Timestamp:
19-
def __init__(self, value: str | DateTime | Timestamp | None = None):
20-
if value and isinstance(value, DateTime):
21-
self.obj = value
22+
_obj: ZonedDateTime
23+
24+
def __init__(self, value: str | ZonedDateTime | Timestamp | None = None):
25+
if value and isinstance(value, ZonedDateTime):
26+
self._obj = value
2227
elif value and isinstance(value, self.__class__):
23-
self.obj = value.obj
28+
self._obj = value._obj
2429
elif isinstance(value, str):
25-
self.obj = self._parse_string(value)
30+
self._obj = self._parse_string(value)
2631
else:
27-
self.obj = DateTime.now(tz="UTC")
32+
self._obj = ZonedDateTime.now("UTC")
33+
34+
@property
35+
def obj(self) -> ZonedDateTime:
36+
warnings.warn(
37+
"Direct access to obj property is deprecated. Use to_string(), to_timestamp(), or to_datetime() instead.",
38+
UserWarning,
39+
stacklevel=2,
40+
)
41+
return self._obj
2842

2943
@classmethod
30-
def _parse_string(cls, value: str) -> DateTime:
44+
def _parse_string(cls, value: str) -> ZonedDateTime:
3145
try:
32-
parsed_date = pendulum.parse(value)
33-
if isinstance(parsed_date, DateTime):
34-
return parsed_date
35-
except (pendulum.parsing.exceptions.ParserError, ValueError):
46+
zoned_date = ZonedDateTime.parse_common_iso(value)
47+
return zoned_date
48+
except ValueError:
3649
pass
3750

38-
params = {}
51+
try:
52+
instant_date = Instant.parse_common_iso(value)
53+
return instant_date.to_tz("UTC")
54+
except ValueError:
55+
pass
56+
57+
try:
58+
local_date_time = LocalDateTime.parse_common_iso(value)
59+
return local_date_time.assume_utc().to_tz("UTC")
60+
except ValueError:
61+
pass
62+
63+
try:
64+
date = Date.parse_common_iso(value)
65+
local_date = date.at(Time(12, 00))
66+
return local_date.assume_tz("UTC", disambiguate="compatible")
67+
except ValueError:
68+
pass
69+
70+
params: dict[str, float] = {}
3971
for key, regex in REGEX_MAPPING.items():
4072
match = re.search(regex, value)
4173
if match:
42-
params[key] = int(match.group(1))
74+
params[key] = float(match.group(1))
4375

44-
if not params:
45-
raise TimestampFormatError(f"Invalid time format for {value}")
76+
if params:
77+
return ZonedDateTime.now("UTC").subtract(**params) # type: ignore[call-overload]
4678

47-
return DateTime.now(tz="UTC").subtract(**params)
79+
raise TimestampFormatError(f"Invalid time format for {value}")
4880

4981
def __repr__(self) -> str:
5082
return f"Timestamp: {self.to_string()}"
5183

5284
def to_string(self, with_z: bool = True) -> str:
53-
iso8601_string = self.obj.to_iso8601_string()
54-
if not with_z and iso8601_string[-1] == "Z":
55-
iso8601_string = iso8601_string[:-1] + "+00:00"
56-
return iso8601_string
85+
if with_z:
86+
return self._obj.instant().format_common_iso()
87+
return self.to_datetime().isoformat()
5788

5889
def to_timestamp(self) -> int:
59-
return self.obj.int_timestamp
90+
return self._obj.timestamp()
91+
92+
def to_datetime(self) -> datetime:
93+
return self._obj.py_datetime()
6094

6195
def __eq__(self, other: object) -> bool:
6296
if not isinstance(other, Timestamp):
6397
return NotImplemented
64-
return self.obj == other.obj
98+
return self._obj == other._obj
6599

66100
def __lt__(self, other: object) -> bool:
67101
if not isinstance(other, Timestamp):
68102
return NotImplemented
69-
return self.obj < other.obj
103+
return self._obj < other._obj
70104

71105
def __gt__(self, other: object) -> bool:
72106
if not isinstance(other, Timestamp):
73107
return NotImplemented
74-
return self.obj > other.obj
108+
return self._obj > other._obj
75109

76110
def __le__(self, other: object) -> bool:
77111
if not isinstance(other, Timestamp):
78112
return NotImplemented
79-
return self.obj <= other.obj
113+
return self._obj <= other._obj
80114

81115
def __ge__(self, other: object) -> bool:
82116
if not isinstance(other, Timestamp):
83117
return NotImplemented
84-
return self.obj >= other.obj
118+
return self._obj >= other._obj
85119

86120
def __hash__(self) -> int:
87121
return hash(self.to_string())
88122

89123
def add_delta(self, hours: int = 0, minutes: int = 0, seconds: int = 0, microseconds: int = 0) -> Timestamp:
90-
time = self.obj.add(hours=hours, minutes=minutes, seconds=seconds, microseconds=microseconds)
124+
time = self._obj.add(hours=hours, minutes=minutes, seconds=seconds, microseconds=microseconds)
91125
return Timestamp(time)

infrahub_sdk/utils.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@
1818
from infrahub_sdk.repository import GitRepoManager
1919

2020
from .exceptions import FileNotValidError, JsonDecodeError
21+
from .timestamp import Timestamp, TimestampFormatError
2122

2223
if TYPE_CHECKING:
2324
from graphql import GraphQLResolveInfo
25+
from whenever import TimeDelta
2426

2527

2628
def base36encode(number: int) -> str:
@@ -367,3 +369,26 @@ def get_user_permissions(data: list[dict]) -> dict:
367369
groups[group_name] = permissions
368370

369371
return groups
372+
373+
374+
def calculate_time_diff(value: str) -> str | None:
375+
"""Calculate the time in human format between a timedate in string format and now."""
376+
try:
377+
time_value = Timestamp(value)
378+
except TimestampFormatError:
379+
return None
380+
381+
delta: TimeDelta = Timestamp().obj.difference(time_value.obj)
382+
(hrs, mins, secs, _) = delta.in_hrs_mins_secs_nanos()
383+
384+
if hrs and hrs < 24 and mins:
385+
return f"{hrs}h {mins}m and {secs}s ago"
386+
if hrs and hrs > 24:
387+
remaining_hrs = hrs % 24
388+
days = int((hrs - remaining_hrs) / 24)
389+
return f"{days}d and {remaining_hrs}h ago"
390+
if hrs == 0 and mins and secs:
391+
return f"{mins}m and {secs}s ago"
392+
if hrs == 0 and mins == 0 and secs:
393+
return f"{secs}s ago"
394+
return "now"

0 commit comments

Comments
 (0)