Skip to content

Commit ba2b0aa

Browse files
committed
Replace pendulum with whenever
1 parent 1a6f3cd commit ba2b0aa

File tree

8 files changed

+336
-171
lines changed

8 files changed

+336
-171
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: 129 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
from __future__ import annotations
22

33
import re
4+
import warnings
5+
from datetime import datetime, timezone
6+
from typing import Literal
47

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

812
REGEX_MAPPING = {
913
"seconds": r"(\d+)(s|sec|second|seconds)",
@@ -16,76 +20,171 @@ class TimestampFormatError(ValueError): ...
1620

1721

1822
class Timestamp:
19-
def __init__(self, value: str | DateTime | Timestamp | None = None):
20-
if value and isinstance(value, DateTime):
21-
self.obj = value
23+
_obj: ZonedDateTime
24+
25+
def __init__(self, value: str | ZonedDateTime | Timestamp | None = None):
26+
if value and isinstance(value, ZonedDateTime):
27+
self._obj = value
2228
elif value and isinstance(value, self.__class__):
23-
self.obj = value.obj
29+
self._obj = value._obj
2430
elif isinstance(value, str):
25-
self.obj = self._parse_string(value)
31+
self._obj = self._parse_string(value)
2632
else:
27-
self.obj = DateTime.now(tz="UTC")
33+
self._obj = ZonedDateTime.now("UTC").round(unit="microsecond")
34+
35+
@property
36+
def obj(self) -> ZonedDateTime:
37+
warnings.warn(
38+
"Direct access to obj property is deprecated. Use to_string(), to_timestamp(), or to_datetime() instead.",
39+
UserWarning,
40+
stacklevel=2,
41+
)
42+
return self._obj
2843

2944
@classmethod
30-
def _parse_string(cls, value: str) -> DateTime:
45+
def _parse_string(cls, value: str) -> ZonedDateTime:
3146
try:
32-
parsed_date = pendulum.parse(value)
33-
if isinstance(parsed_date, DateTime):
34-
return parsed_date
35-
except (pendulum.parsing.exceptions.ParserError, ValueError):
47+
zoned_date = ZonedDateTime.parse_common_iso(value)
48+
return zoned_date
49+
except ValueError:
3650
pass
3751

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

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

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

4982
def __repr__(self) -> str:
5083
return f"Timestamp: {self.to_string()}"
5184

5285
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
86+
if with_z:
87+
return self._obj.instant().format_common_iso()
88+
return self.to_datetime().isoformat()
5789

5890
def to_timestamp(self) -> int:
59-
return self.obj.int_timestamp
91+
return self._obj.timestamp()
92+
93+
def to_datetime(self) -> datetime:
94+
return self._obj.py_datetime()
6095

6196
def __eq__(self, other: object) -> bool:
6297
if not isinstance(other, Timestamp):
6398
return NotImplemented
64-
return self.obj == other.obj
99+
return self._obj == other._obj
65100

66101
def __lt__(self, other: object) -> bool:
67102
if not isinstance(other, Timestamp):
68103
return NotImplemented
69-
return self.obj < other.obj
104+
return self._obj < other._obj
70105

71106
def __gt__(self, other: object) -> bool:
72107
if not isinstance(other, Timestamp):
73108
return NotImplemented
74-
return self.obj > other.obj
109+
return self._obj > other._obj
75110

76111
def __le__(self, other: object) -> bool:
77112
if not isinstance(other, Timestamp):
78113
return NotImplemented
79-
return self.obj <= other.obj
114+
return self._obj <= other._obj
80115

81116
def __ge__(self, other: object) -> bool:
82117
if not isinstance(other, Timestamp):
83118
return NotImplemented
84-
return self.obj >= other.obj
119+
return self._obj >= other._obj
85120

86121
def __hash__(self) -> int:
87122
return hash(self.to_string())
88123

89124
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)
91-
return Timestamp(time)
125+
warnings.warn(
126+
"add_delta() is deprecated. Use add() instead.",
127+
UserWarning,
128+
stacklevel=2,
129+
)
130+
return self.add(hours=hours, minutes=minutes, seconds=seconds, microseconds=microseconds)
131+
132+
def add(
133+
self,
134+
years: int = 0,
135+
months: int = 0,
136+
weeks: int = 0,
137+
days: int = 0,
138+
hours: float = 0,
139+
minutes: float = 0,
140+
seconds: float = 0,
141+
milliseconds: float = 0,
142+
microseconds: float = 0,
143+
nanoseconds: int = 0,
144+
disambiguate: Literal["compatible"] = "compatible",
145+
) -> Timestamp:
146+
return Timestamp(
147+
self._obj.add(
148+
years=years,
149+
months=months,
150+
weeks=weeks,
151+
days=days,
152+
hours=hours,
153+
minutes=minutes,
154+
seconds=seconds,
155+
milliseconds=milliseconds,
156+
microseconds=microseconds,
157+
nanoseconds=nanoseconds,
158+
disambiguate=disambiguate,
159+
)
160+
)
161+
162+
def subtract(
163+
self,
164+
years: int = 0,
165+
months: int = 0,
166+
weeks: int = 0,
167+
days: int = 0,
168+
hours: float = 0,
169+
minutes: float = 0,
170+
seconds: float = 0,
171+
milliseconds: float = 0,
172+
microseconds: float = 0,
173+
nanoseconds: int = 0,
174+
disambiguate: Literal["compatible"] = "compatible",
175+
) -> Timestamp:
176+
return Timestamp(
177+
self._obj.subtract(
178+
years=years,
179+
months=months,
180+
weeks=weeks,
181+
days=days,
182+
hours=hours,
183+
minutes=minutes,
184+
seconds=seconds,
185+
milliseconds=milliseconds,
186+
microseconds=microseconds,
187+
nanoseconds=nanoseconds,
188+
disambiguate=disambiguate,
189+
)
190+
)

infrahub_sdk/utils.py

Lines changed: 28 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,29 @@ 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, nanos) = delta.in_hrs_mins_secs_nanos()
383+
384+
if nanos and nanos > 500_000_000:
385+
secs += 1
386+
387+
if hrs and hrs < 24 and mins:
388+
return f"{hrs}h {mins}m and {secs}s ago"
389+
if hrs and hrs > 24:
390+
remaining_hrs = hrs % 24
391+
days = int((hrs - remaining_hrs) / 24)
392+
return f"{days}d and {remaining_hrs}h ago"
393+
if hrs == 0 and mins and secs:
394+
return f"{mins}m and {secs}s ago"
395+
if hrs == 0 and mins == 0 and secs:
396+
return f"{secs}s ago"
397+
return "now"

0 commit comments

Comments
 (0)