From a2a496f20af496e1bd7c50ea73ac04a474eec85d Mon Sep 17 00:00:00 2001 From: Damien Garros Date: Wed, 26 Feb 2025 21:16:30 +0100 Subject: [PATCH] Replace pendulum with whenever --- .github/workflows/ci.yml | 1 + changelog/+python313.fixed.md | 1 + changelog/251.fixed.md | 2 +- changelog/255.deprecated.md | 1 + changelog/255.fixed.md | 1 + infrahub_sdk/ctl/branch.py | 5 +- infrahub_sdk/ctl/utils.py | 16 --- infrahub_sdk/exceptions.py | 6 + infrahub_sdk/timestamp.py | 167 ++++++++++++++++++++----- infrahub_sdk/utils.py | 30 ++++- poetry.lock | 205 ++++++++++++++----------------- pyproject.toml | 8 +- tests/unit/sdk/test_timestamp.py | 82 +++++++++++-- tests/unit/sdk/test_utils.py | 19 +++ 14 files changed, 363 insertions(+), 181 deletions(-) create mode 100644 changelog/+python313.fixed.md create mode 100644 changelog/255.deprecated.md create mode 100644 changelog/255.fixed.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cde9aa72..5529d3e3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -126,6 +126,7 @@ jobs: - "3.10" - "3.11" - "3.12" + - "3.13" if: | always() && !cancelled() && !contains(needs.*.result, 'failure') && diff --git a/changelog/+python313.fixed.md b/changelog/+python313.fixed.md new file mode 100644 index 00000000..80bcdb5d --- /dev/null +++ b/changelog/+python313.fixed.md @@ -0,0 +1 @@ +Fixed support for Python 3.13, it's no longer required to have Rust installed on the system \ No newline at end of file diff --git a/changelog/251.fixed.md b/changelog/251.fixed.md index 84e39163..a35bb72f 100644 --- a/changelog/251.fixed.md +++ b/changelog/251.fixed.md @@ -1 +1 @@ -Fix typing for Python 3.9 and remove support for Python 3.13 \ No newline at end of file +Fix typing for Python 3.9 \ No newline at end of file diff --git a/changelog/255.deprecated.md b/changelog/255.deprecated.md new file mode 100644 index 00000000..0955460c --- /dev/null +++ b/changelog/255.deprecated.md @@ -0,0 +1 @@ +Timestamp: Direct access to `obj` and `add_delta` have been deprecated and will be removed in a future version. \ No newline at end of file diff --git a/changelog/255.fixed.md b/changelog/255.fixed.md new file mode 100644 index 00000000..178e6b13 --- /dev/null +++ b/changelog/255.fixed.md @@ -0,0 +1 @@ +Refactor Timestamp to use `whenever` instead of `pendulum` and extend Timestamp with add(), subtract(), and to_datetime(). \ No newline at end of file diff --git a/infrahub_sdk/ctl/branch.py b/infrahub_sdk/ctl/branch.py index f77aa073..b44be462 100644 --- a/infrahub_sdk/ctl/branch.py +++ b/infrahub_sdk/ctl/branch.py @@ -5,9 +5,10 @@ from rich.table import Table from ..async_typer import AsyncTyper -from ..ctl.client import initialize_client -from ..ctl.utils import calculate_time_diff, catch_exception +from ..utils import calculate_time_diff +from .client import initialize_client from .parameters import CONFIG_PARAM +from .utils import catch_exception app = AsyncTyper() console = Console() diff --git a/infrahub_sdk/ctl/utils.py b/infrahub_sdk/ctl/utils.py index 4c627119..898095c7 100644 --- a/infrahub_sdk/ctl/utils.py +++ b/infrahub_sdk/ctl/utils.py @@ -8,11 +8,9 @@ from pathlib import Path from typing import TYPE_CHECKING, Any, Callable, NoReturn, Optional, TypeVar -import pendulum import typer from click.exceptions import Exit from httpx import HTTPError -from pendulum.datetime import DateTime from rich.console import Console from rich.logging import RichHandler from rich.markup import escape @@ -152,20 +150,6 @@ def parse_cli_vars(variables: Optional[list[str]]) -> dict[str, str]: return {var.split("=")[0]: var.split("=")[1] for var in variables if "=" in var} -def calculate_time_diff(value: str) -> str | None: - """Calculate the time in human format between a timedate in string format and now.""" - try: - time_value = pendulum.parse(value) - except pendulum.parsing.exceptions.ParserError: - return None - - if not isinstance(time_value, DateTime): - return None - - pendulum.set_locale("en") - return time_value.diff_for_humans(other=pendulum.now(), absolute=True) - - def find_graphql_query(name: str, directory: str | Path = ".") -> str: if isinstance(directory, str): directory = Path(directory) diff --git a/infrahub_sdk/exceptions.py b/infrahub_sdk/exceptions.py index b3120441..649b85fb 100644 --- a/infrahub_sdk/exceptions.py +++ b/infrahub_sdk/exceptions.py @@ -137,3 +137,9 @@ class FileNotValidError(Error): def __init__(self, name: str, message: str = ""): self.message = message or f"Cannot parse '{name}' content." super().__init__(self.message) + + +class TimestampFormatError(Error): + def __init__(self, message: str | None = None): + self.message = message or "Invalid timestamp format" + super().__init__(self.message) diff --git a/infrahub_sdk/timestamp.py b/infrahub_sdk/timestamp.py index 5a2f58d5..fb07f076 100644 --- a/infrahub_sdk/timestamp.py +++ b/infrahub_sdk/timestamp.py @@ -1,9 +1,15 @@ from __future__ import annotations import re +import warnings +from datetime import datetime, timezone +from typing import Literal -import pendulum -from pendulum.datetime import DateTime +from whenever import Date, Instant, LocalDateTime, Time, ZonedDateTime + +from .exceptions import TimestampFormatError + +UTC = timezone.utc # Required for older versions of Python REGEX_MAPPING = { "seconds": r"(\d+)(s|sec|second|seconds)", @@ -12,80 +18,175 @@ } -class TimestampFormatError(ValueError): ... - - class Timestamp: - def __init__(self, value: str | DateTime | Timestamp | None = None): - if value and isinstance(value, DateTime): - self.obj = value + _obj: ZonedDateTime + + def __init__(self, value: str | ZonedDateTime | Timestamp | None = None): + if value and isinstance(value, ZonedDateTime): + self._obj = value elif value and isinstance(value, self.__class__): - self.obj = value.obj + self._obj = value._obj elif isinstance(value, str): - self.obj = self._parse_string(value) + self._obj = self._parse_string(value) else: - self.obj = DateTime.now(tz="UTC") + self._obj = ZonedDateTime.now("UTC").round(unit="microsecond") + + @property + def obj(self) -> ZonedDateTime: + warnings.warn( + "Direct access to obj property is deprecated. Use to_string(), to_timestamp(), or to_datetime() instead.", + UserWarning, + stacklevel=2, + ) + return self._obj @classmethod - def _parse_string(cls, value: str) -> DateTime: + def _parse_string(cls, value: str) -> ZonedDateTime: + try: + zoned_date = ZonedDateTime.parse_common_iso(value) + return zoned_date + except ValueError: + pass + try: - parsed_date = pendulum.parse(value) - if isinstance(parsed_date, DateTime): - return parsed_date - except (pendulum.parsing.exceptions.ParserError, ValueError): + instant_date = Instant.parse_common_iso(value) + return instant_date.to_tz("UTC") + except ValueError: pass - params = {} + try: + local_date_time = LocalDateTime.parse_common_iso(value) + return local_date_time.assume_utc().to_tz("UTC") + except ValueError: + pass + + try: + date = Date.parse_common_iso(value) + local_date = date.at(Time(12, 00)) + return local_date.assume_tz("UTC", disambiguate="compatible") + except ValueError: + pass + + params: dict[str, float] = {} for key, regex in REGEX_MAPPING.items(): match = re.search(regex, value) if match: - params[key] = int(match.group(1)) + params[key] = float(match.group(1)) - if not params: - raise TimestampFormatError(f"Invalid time format for {value}") + if params: + return ZonedDateTime.now("UTC").subtract(**params) # type: ignore[call-overload] - return DateTime.now(tz="UTC").subtract(**params) + raise TimestampFormatError(f"Invalid time format for {value}") def __repr__(self) -> str: return f"Timestamp: {self.to_string()}" def to_string(self, with_z: bool = True) -> str: - iso8601_string = self.obj.to_iso8601_string() - if not with_z and iso8601_string[-1] == "Z": - iso8601_string = iso8601_string[:-1] + "+00:00" - return iso8601_string + if with_z: + return self._obj.instant().format_common_iso() + return self.to_datetime().isoformat() def to_timestamp(self) -> int: - return self.obj.int_timestamp + return self._obj.timestamp() + + def to_datetime(self) -> datetime: + return self._obj.py_datetime() + + def get_obj(self) -> ZonedDateTime: + return self._obj def __eq__(self, other: object) -> bool: if not isinstance(other, Timestamp): return NotImplemented - return self.obj == other.obj + return self._obj == other._obj def __lt__(self, other: object) -> bool: if not isinstance(other, Timestamp): return NotImplemented - return self.obj < other.obj + return self._obj < other._obj def __gt__(self, other: object) -> bool: if not isinstance(other, Timestamp): return NotImplemented - return self.obj > other.obj + return self._obj > other._obj def __le__(self, other: object) -> bool: if not isinstance(other, Timestamp): return NotImplemented - return self.obj <= other.obj + return self._obj <= other._obj def __ge__(self, other: object) -> bool: if not isinstance(other, Timestamp): return NotImplemented - return self.obj >= other.obj + return self._obj >= other._obj def __hash__(self) -> int: return hash(self.to_string()) def add_delta(self, hours: int = 0, minutes: int = 0, seconds: int = 0, microseconds: int = 0) -> Timestamp: - time = self.obj.add(hours=hours, minutes=minutes, seconds=seconds, microseconds=microseconds) - return Timestamp(time) + warnings.warn( + "add_delta() is deprecated. Use add() instead.", + UserWarning, + stacklevel=2, + ) + return self.add(hours=hours, minutes=minutes, seconds=seconds, microseconds=microseconds) + + def add( + self, + years: int = 0, + months: int = 0, + weeks: int = 0, + days: int = 0, + hours: float = 0, + minutes: float = 0, + seconds: float = 0, + milliseconds: float = 0, + microseconds: float = 0, + nanoseconds: int = 0, + disambiguate: Literal["compatible"] = "compatible", + ) -> Timestamp: + return Timestamp( + self._obj.add( + years=years, + months=months, + weeks=weeks, + days=days, + hours=hours, + minutes=minutes, + seconds=seconds, + milliseconds=milliseconds, + microseconds=microseconds, + nanoseconds=nanoseconds, + disambiguate=disambiguate, + ) + ) + + def subtract( + self, + years: int = 0, + months: int = 0, + weeks: int = 0, + days: int = 0, + hours: float = 0, + minutes: float = 0, + seconds: float = 0, + milliseconds: float = 0, + microseconds: float = 0, + nanoseconds: int = 0, + disambiguate: Literal["compatible"] = "compatible", + ) -> Timestamp: + return Timestamp( + self._obj.subtract( + years=years, + months=months, + weeks=weeks, + days=days, + hours=hours, + minutes=minutes, + seconds=seconds, + milliseconds=milliseconds, + microseconds=microseconds, + nanoseconds=nanoseconds, + disambiguate=disambiguate, + ) + ) diff --git a/infrahub_sdk/utils.py b/infrahub_sdk/utils.py index 2627a062..a45a65aa 100644 --- a/infrahub_sdk/utils.py +++ b/infrahub_sdk/utils.py @@ -17,10 +17,12 @@ from infrahub_sdk.repository import GitRepoManager -from .exceptions import FileNotValidError, JsonDecodeError +from .exceptions import FileNotValidError, JsonDecodeError, TimestampFormatError +from .timestamp import Timestamp if TYPE_CHECKING: from graphql import GraphQLResolveInfo + from whenever import TimeDelta def base36encode(number: int) -> str: @@ -367,3 +369,29 @@ def get_user_permissions(data: list[dict]) -> dict: groups[group_name] = permissions return groups + + +def calculate_time_diff(value: str) -> str | None: + """Calculate the time in human format between a timedate in string format and now.""" + try: + time_value = Timestamp(value) + except TimestampFormatError: + return None + + delta: TimeDelta = Timestamp().get_obj().difference(time_value.get_obj()) + (hrs, mins, secs, nanos) = delta.in_hrs_mins_secs_nanos() + + if nanos and nanos > 500_000_000: + secs += 1 + + if hrs and hrs < 24 and mins: + return f"{hrs}h {mins}m and {secs}s ago" + if hrs and hrs > 24: + remaining_hrs = hrs % 24 + days = int((hrs - remaining_hrs) / 24) + return f"{days}d and {remaining_hrs}h ago" + if hrs == 0 and mins and secs: + return f"{mins}m and {secs}s ago" + if hrs == 0 and mins == 0 and secs: + return f"{secs}s ago" + return "now" diff --git a/poetry.lock b/poetry.lock index 3b60de6a..a43ab5f0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1021,105 +1021,6 @@ files = [ {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, ] -[[package]] -name = "pendulum" -version = "3.0.0" -description = "Python datetimes made easy" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pendulum-3.0.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2cf9e53ef11668e07f73190c805dbdf07a1939c3298b78d5a9203a86775d1bfd"}, - {file = "pendulum-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fb551b9b5e6059377889d2d878d940fd0bbb80ae4810543db18e6f77b02c5ef6"}, - {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c58227ac260d5b01fc1025176d7b31858c9f62595737f350d22124a9a3ad82d"}, - {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60fb6f415fea93a11c52578eaa10594568a6716602be8430b167eb0d730f3332"}, - {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b69f6b4dbcb86f2c2fe696ba991e67347bcf87fe601362a1aba6431454b46bde"}, - {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:138afa9c373ee450ede206db5a5e9004fd3011b3c6bbe1e57015395cd076a09f"}, - {file = "pendulum-3.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:83d9031f39c6da9677164241fd0d37fbfc9dc8ade7043b5d6d62f56e81af8ad2"}, - {file = "pendulum-3.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0c2308af4033fa534f089595bcd40a95a39988ce4059ccd3dc6acb9ef14ca44a"}, - {file = "pendulum-3.0.0-cp310-none-win_amd64.whl", hash = "sha256:9a59637cdb8462bdf2dbcb9d389518c0263799189d773ad5c11db6b13064fa79"}, - {file = "pendulum-3.0.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3725245c0352c95d6ca297193192020d1b0c0f83d5ee6bb09964edc2b5a2d508"}, - {file = "pendulum-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6c035f03a3e565ed132927e2c1b691de0dbf4eb53b02a5a3c5a97e1a64e17bec"}, - {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:597e66e63cbd68dd6d58ac46cb7a92363d2088d37ccde2dae4332ef23e95cd00"}, - {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99a0f8172e19f3f0c0e4ace0ad1595134d5243cf75985dc2233e8f9e8de263ca"}, - {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:77d8839e20f54706aed425bec82a83b4aec74db07f26acd039905d1237a5e1d4"}, - {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afde30e8146292b059020fbc8b6f8fd4a60ae7c5e6f0afef937bbb24880bdf01"}, - {file = "pendulum-3.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:660434a6fcf6303c4efd36713ca9212c753140107ee169a3fc6c49c4711c2a05"}, - {file = "pendulum-3.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dee9e5a48c6999dc1106eb7eea3e3a50e98a50651b72c08a87ee2154e544b33e"}, - {file = "pendulum-3.0.0-cp311-none-win_amd64.whl", hash = "sha256:d4cdecde90aec2d67cebe4042fd2a87a4441cc02152ed7ed8fb3ebb110b94ec4"}, - {file = "pendulum-3.0.0-cp311-none-win_arm64.whl", hash = "sha256:773c3bc4ddda2dda9f1b9d51fe06762f9200f3293d75c4660c19b2614b991d83"}, - {file = "pendulum-3.0.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:409e64e41418c49f973d43a28afe5df1df4f1dd87c41c7c90f1a63f61ae0f1f7"}, - {file = "pendulum-3.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a38ad2121c5ec7c4c190c7334e789c3b4624798859156b138fcc4d92295835dc"}, - {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fde4d0b2024b9785f66b7f30ed59281bd60d63d9213cda0eb0910ead777f6d37"}, - {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b2c5675769fb6d4c11238132962939b960fcb365436b6d623c5864287faa319"}, - {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8af95e03e066826f0f4c65811cbee1b3123d4a45a1c3a2b4fc23c4b0dff893b5"}, - {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2165a8f33cb15e06c67070b8afc87a62b85c5a273e3aaa6bc9d15c93a4920d6f"}, - {file = "pendulum-3.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ad5e65b874b5e56bd942546ea7ba9dd1d6a25121db1c517700f1c9de91b28518"}, - {file = "pendulum-3.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:17fe4b2c844bbf5f0ece69cfd959fa02957c61317b2161763950d88fed8e13b9"}, - {file = "pendulum-3.0.0-cp312-none-win_amd64.whl", hash = "sha256:78f8f4e7efe5066aca24a7a57511b9c2119f5c2b5eb81c46ff9222ce11e0a7a5"}, - {file = "pendulum-3.0.0-cp312-none-win_arm64.whl", hash = "sha256:28f49d8d1e32aae9c284a90b6bb3873eee15ec6e1d9042edd611b22a94ac462f"}, - {file = "pendulum-3.0.0-cp37-cp37m-macosx_10_12_x86_64.whl", hash = "sha256:d4e2512f4e1a4670284a153b214db9719eb5d14ac55ada5b76cbdb8c5c00399d"}, - {file = "pendulum-3.0.0-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:3d897eb50883cc58d9b92f6405245f84b9286cd2de6e8694cb9ea5cb15195a32"}, - {file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e169cc2ca419517f397811bbe4589cf3cd13fca6dc38bb352ba15ea90739ebb"}, - {file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f17c3084a4524ebefd9255513692f7e7360e23c8853dc6f10c64cc184e1217ab"}, - {file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:826d6e258052715f64d05ae0fc9040c0151e6a87aae7c109ba9a0ed930ce4000"}, - {file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2aae97087872ef152a0c40e06100b3665d8cb86b59bc8471ca7c26132fccd0f"}, - {file = "pendulum-3.0.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ac65eeec2250d03106b5e81284ad47f0d417ca299a45e89ccc69e36130ca8bc7"}, - {file = "pendulum-3.0.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a5346d08f3f4a6e9e672187faa179c7bf9227897081d7121866358af369f44f9"}, - {file = "pendulum-3.0.0-cp37-none-win_amd64.whl", hash = "sha256:235d64e87946d8f95c796af34818c76e0f88c94d624c268693c85b723b698aa9"}, - {file = "pendulum-3.0.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:6a881d9c2a7f85bc9adafcfe671df5207f51f5715ae61f5d838b77a1356e8b7b"}, - {file = "pendulum-3.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d7762d2076b9b1cb718a6631ad6c16c23fc3fac76cbb8c454e81e80be98daa34"}, - {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e8e36a8130819d97a479a0e7bf379b66b3b1b520e5dc46bd7eb14634338df8c"}, - {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7dc843253ac373358ffc0711960e2dd5b94ab67530a3e204d85c6e8cb2c5fa10"}, - {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a78ad3635d609ceb1e97d6aedef6a6a6f93433ddb2312888e668365908c7120"}, - {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b30a137e9e0d1f751e60e67d11fc67781a572db76b2296f7b4d44554761049d6"}, - {file = "pendulum-3.0.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c95984037987f4a457bb760455d9ca80467be792236b69d0084f228a8ada0162"}, - {file = "pendulum-3.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d29c6e578fe0f893766c0d286adbf0b3c726a4e2341eba0917ec79c50274ec16"}, - {file = "pendulum-3.0.0-cp38-none-win_amd64.whl", hash = "sha256:deaba8e16dbfcb3d7a6b5fabdd5a38b7c982809567479987b9c89572df62e027"}, - {file = "pendulum-3.0.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b11aceea5b20b4b5382962b321dbc354af0defe35daa84e9ff3aae3c230df694"}, - {file = "pendulum-3.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a90d4d504e82ad236afac9adca4d6a19e4865f717034fc69bafb112c320dcc8f"}, - {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:825799c6b66e3734227756fa746cc34b3549c48693325b8b9f823cb7d21b19ac"}, - {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad769e98dc07972e24afe0cff8d365cb6f0ebc7e65620aa1976fcfbcadc4c6f3"}, - {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6fc26907eb5fb8cc6188cc620bc2075a6c534d981a2f045daa5f79dfe50d512"}, - {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c717eab1b6d898c00a3e0fa7781d615b5c5136bbd40abe82be100bb06df7a56"}, - {file = "pendulum-3.0.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3ddd1d66d1a714ce43acfe337190be055cdc221d911fc886d5a3aae28e14b76d"}, - {file = "pendulum-3.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:822172853d7a9cf6da95d7b66a16c7160cb99ae6df55d44373888181d7a06edc"}, - {file = "pendulum-3.0.0-cp39-none-win_amd64.whl", hash = "sha256:840de1b49cf1ec54c225a2a6f4f0784d50bd47f68e41dc005b7f67c7d5b5f3ae"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3b1f74d1e6ffe5d01d6023870e2ce5c2191486928823196f8575dcc786e107b1"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:729e9f93756a2cdfa77d0fc82068346e9731c7e884097160603872686e570f07"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e586acc0b450cd21cbf0db6bae386237011b75260a3adceddc4be15334689a9a"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22e7944ffc1f0099a79ff468ee9630c73f8c7835cd76fdb57ef7320e6a409df4"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:fa30af36bd8e50686846bdace37cf6707bdd044e5cb6e1109acbad3277232e04"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:440215347b11914ae707981b9a57ab9c7b6983ab0babde07063c6ee75c0dc6e7"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:314c4038dc5e6a52991570f50edb2f08c339debdf8cea68ac355b32c4174e820"}, - {file = "pendulum-3.0.0-pp37-pypy37_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5acb1d386337415f74f4d1955c4ce8d0201978c162927d07df8eb0692b2d8533"}, - {file = "pendulum-3.0.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a789e12fbdefaffb7b8ac67f9d8f22ba17a3050ceaaa635cd1cc4645773a4b1e"}, - {file = "pendulum-3.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:860aa9b8a888e5913bd70d819306749e5eb488e6b99cd6c47beb701b22bdecf5"}, - {file = "pendulum-3.0.0-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:5ebc65ea033ef0281368217fbf59f5cb05b338ac4dd23d60959c7afcd79a60a0"}, - {file = "pendulum-3.0.0-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d9fef18ab0386ef6a9ac7bad7e43ded42c83ff7ad412f950633854f90d59afa8"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1c134ba2f0571d0b68b83f6972e2307a55a5a849e7dac8505c715c531d2a8795"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:385680812e7e18af200bb9b4a49777418c32422d05ad5a8eb85144c4a285907b"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9eec91cd87c59fb32ec49eb722f375bd58f4be790cae11c1b70fac3ee4f00da0"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4386bffeca23c4b69ad50a36211f75b35a4deb6210bdca112ac3043deb7e494a"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:dfbcf1661d7146d7698da4b86e7f04814221081e9fe154183e34f4c5f5fa3bf8"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:04a1094a5aa1daa34a6b57c865b25f691848c61583fb22722a4df5699f6bf74c"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5b0ec85b9045bd49dd3a3493a5e7ddfd31c36a2a60da387c419fa04abcaecb23"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0a15b90129765b705eb2039062a6daf4d22c4e28d1a54fa260892e8c3ae6e157"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:bb8f6d7acd67a67d6fedd361ad2958ff0539445ef51cbe8cd288db4306503cd0"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd69b15374bef7e4b4440612915315cc42e8575fcda2a3d7586a0d88192d0c88"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc00f8110db6898360c53c812872662e077eaf9c75515d53ecc65d886eec209a"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:83a44e8b40655d0ba565a5c3d1365d27e3e6778ae2a05b69124db9e471255c4a"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:1a3604e9fbc06b788041b2a8b78f75c243021e0f512447806a6d37ee5214905d"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:92c307ae7accebd06cbae4729f0ba9fa724df5f7d91a0964b1b972a22baa482b"}, - {file = "pendulum-3.0.0.tar.gz", hash = "sha256:5d034998dea404ec31fae27af6b22cff1708f830a1ed7353be4d1019bb9f584e"}, -] - -[package.dependencies] -python-dateutil = ">=2.6" -tzdata = ">=2020.1" - -[package.extras] -test = ["time-machine (>=2.6.0)"] - [[package]] name = "pexpect" version = "4.9.0" @@ -1301,7 +1202,10 @@ files = [ [package.dependencies] annotated-types = ">=0.6.0" pydantic-core = "2.23.3" -typing-extensions = {version = ">=4.6.1", markers = "python_version < \"3.13\""} +typing-extensions = [ + {version = ">=4.6.1", markers = "python_version < \"3.13\""}, + {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, +] [package.extras] email = ["email-validator (>=2.0.0)"] @@ -1553,20 +1457,6 @@ psutil = ["psutil (>=3.0)"] setproctitle = ["setproctitle"] testing = ["filelock"] -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -description = "Extensions to the standard Python datetime module" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -files = [ - {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, - {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, -] - -[package.dependencies] -six = ">=1.5" - [[package]] name = "python-dotenv" version = "1.0.1" @@ -2116,6 +2006,89 @@ files = [ {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, ] +[[package]] +name = "whenever" +version = "0.7.2" +description = "Modern datetime library for Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "whenever-0.7.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a87864d3e7679dbedc55d3aa8c6cef5ffdc45520e16805f4c5a3cf71241fb986"}, + {file = "whenever-0.7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f37dc37d1bea611af16a3aaba5960038604ddfb4a592b1d72a3efccd5853b6da"}, + {file = "whenever-0.7.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3da602b9fb80f8c6495e0495638c54a8b9a43362769199fcfe4e4fc6df33697"}, + {file = "whenever-0.7.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:54c7afaeaada1d244016ce38252f0c0340bd7d199b4a240ba986efaab66b02f2"}, + {file = "whenever-0.7.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f71fd077601c27830e202ed652bd89b46ae6f1ba0f96d29897038dae9c80eead"}, + {file = "whenever-0.7.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40c74e8f0f3a9a540f580d44a22f2f9dc54b17b68d64abb1c0c961ab1343d43b"}, + {file = "whenever-0.7.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de72b15de311b28e6fcdb45bd436fbb0bde0d4596e0c446f9301bb523b6f2369"}, + {file = "whenever-0.7.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a84ff30d230e56250f89e99f5442d51a5215e10f6b7902d0d7ec51d8b06b6b2"}, + {file = "whenever-0.7.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2af28fa6c8446f513ed3c71275349831e79df021dadb0051fb5b6cbd353d16d6"}, + {file = "whenever-0.7.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:7f65c163f80b397f532d6dd9f56ead5b5d8b76bc24b1587dbb152bb466bd7de0"}, + {file = "whenever-0.7.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ef6c83a20b9ccfe10623596dda19d666cc95c0e83260a6568d767bc926da3781"}, + {file = "whenever-0.7.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6322b78dd97b295164f0d59115be71e9242f74c100899736876b1e8f19b2ff0f"}, + {file = "whenever-0.7.2-cp310-cp310-win32.whl", hash = "sha256:b9a2fc32a8914771d994d6349dcf25208c82d0eb6cf33f27b2309d9e8f58a51a"}, + {file = "whenever-0.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:1440b8e1ef507c318a741bede7a43263f84909c43cf48f110de509233b89d77c"}, + {file = "whenever-0.7.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0b5aaa62551213b3b099b460331fce75c7dbabc2f6696fe3be845cb4ecc8a856"}, + {file = "whenever-0.7.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a4720cc7bf704e92b89bf60329f21084256b4b4a9dcc47a782461f7918d7e1fb"}, + {file = "whenever-0.7.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91a18c81e517124463200b7fcde40ddcc18c959791b219dd681dc5fdec04f050"}, + {file = "whenever-0.7.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:85ef3850f6a9ce3d5349a4f5a1d7fda14c68d3f18c0d18a890bcb11955709a8c"}, + {file = "whenever-0.7.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9af47e5ff5d8528a7149f253276e1094bb944335074241d7e9f6c26ea12aa9ac"}, + {file = "whenever-0.7.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:389afeb92b6272f35132a428884ba03f52ca5a9e80c1b28e0f9699f6098abf34"}, + {file = "whenever-0.7.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:825a9567ba0b91f1e970cd59f0bbf7b6c2c12c41621fd3264e2d1a0f596c3efe"}, + {file = "whenever-0.7.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9d303cb0c691784219b7539e537167ea573cf58acc42696159585d27dacd10af"}, + {file = "whenever-0.7.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6194cf9bf76cb0e3c8593d757b73b41cb33c1137ce1a79795812d43be8a29a95"}, + {file = "whenever-0.7.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:59c0fb56aed72a0ec10a83b99f8eee2e96e4b32045e4ecfe85027129295cde6a"}, + {file = "whenever-0.7.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f50068f98c85706e384a84e6a6e5d0d38760bbcb770fbd140596d2228f101c2e"}, + {file = "whenever-0.7.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:993c98e9956597b61e21c3c65da8d5e9eb342fe6c6efc2135432be56aa64a116"}, + {file = "whenever-0.7.2-cp311-cp311-win32.whl", hash = "sha256:e1d0ea62becd437ae9c911303cbcc5ba66107a79c9e60a4e0f965537878a3c77"}, + {file = "whenever-0.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:c70a6ab84a4d7bb44e86fa9ebec2ea36a456457d211dcb48f16f54487774ec45"}, + {file = "whenever-0.7.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:acaedfa0327e8859c078c40c2e17a3d169ce9f784c3735c09fd701d4035b7432"}, + {file = "whenever-0.7.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:38de1c34ab4e42eda4006e8635cadc0c526094a546aa5ebf6a903c61d33053f3"}, + {file = "whenever-0.7.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87e62291c4a0f212a13053f021b9255e0b820e57303c96e94b48304b84a1849d"}, + {file = "whenever-0.7.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e0cbf68b2833e6766fb4898ebe432406ce6ead7ac846f7b15427bfbd560d5939"}, + {file = "whenever-0.7.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2220cf0d818d960d4a7ec1b05ffbed7b81e482807be0b4bb7a5466418a4c8f79"}, + {file = "whenever-0.7.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7395c69109113eb1666bac29b6207caf28e38e25d332c57649a7e710f0d863db"}, + {file = "whenever-0.7.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efcbffe9a510f310f019fe5bfe877e591ea8cdad90ac8fe6868a80659d411ac5"}, + {file = "whenever-0.7.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e2d836ad37f4333e938779eae6e64f532f27ce19529ee9c09bfb62f796e41db1"}, + {file = "whenever-0.7.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:59f1949d1efe4a85cfe81130159dc2c871ea5b56bae6e9782d5e344a747a758e"}, + {file = "whenever-0.7.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:030836f2cb15eb33631c3d3c2f904d481edc797df063814f9c77d060db5db17d"}, + {file = "whenever-0.7.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b25d8fd6ade64cf1492707c019cccb726aa07dfb20f79a4751eccb56555c2012"}, + {file = "whenever-0.7.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:247e0255c6c8ded48a6d3734aabf448f2bf07bb2abb65b2828104df1eaab82cf"}, + {file = "whenever-0.7.2-cp312-cp312-win32.whl", hash = "sha256:81fcef2c6917333d3aa8d24043e01323d8831c1354cabcb935e29b2a1f6a7a4f"}, + {file = "whenever-0.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:2cdb4ddd2b2e908a076232a60577e4616096d4cf166da9373c4a03bf9d81721e"}, + {file = "whenever-0.7.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:c35f47f613a7816d602fd39594400bfe7fff70a3bd7272cd9b8c736ffc13feed"}, + {file = "whenever-0.7.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0cb7515e180aa2fea6c2d1855607011dd08d14acaba750b0673d7d6f536b0f5e"}, + {file = "whenever-0.7.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fac40aed9be0bc1aeba2662e17f145987f84e8a0bafbfa5f938b40db82fc7aba"}, + {file = "whenever-0.7.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ccd0840d04648dad3c5ae81a53a56e08a971a316d4167921665a7aa5fa8f0085"}, + {file = "whenever-0.7.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bf5199ffa1da783207b0c75d478ab6d808309cc0cbb2631640393bd943b6167e"}, + {file = "whenever-0.7.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7be787eeb542f86cc2d0081c541e89e4417261976a50a7824f6e43248fadb294"}, + {file = "whenever-0.7.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d78e13c648ce246dbaa54f78faf1d2f3d8107619f3c598d3d127ca45fd5d792a"}, + {file = "whenever-0.7.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cc490b577f38bb55957e04d6a1b594c5365f01a6f3429c38b26243d3cf473d80"}, + {file = "whenever-0.7.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bf322daa4184e7d89a4549498c8408e6c4a0bd2309eacd4b21151020bf51870c"}, + {file = "whenever-0.7.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:00b60a9af13e4c6b618f52a55ae7c15c36eb3ff42bfc6cb050981e8a2402bc9f"}, + {file = "whenever-0.7.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a2f82fd85a6521090d3f44412f4c76687a0e141df215541f6f0f6691276257e7"}, + {file = "whenever-0.7.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a386df7b5e08f56a49f8a00991e54c3f5ebb218570d7a98c726d793859a2b0ea"}, + {file = "whenever-0.7.2-cp313-cp313-win32.whl", hash = "sha256:46e51abd495c91fd586828401884750d7eb96ca3658d3d9f228f62beb140c758"}, + {file = "whenever-0.7.2-cp313-cp313-win_amd64.whl", hash = "sha256:af67395516ed16a8423735a4dd5a8795353f39e758b7428178dbe8de06977f21"}, + {file = "whenever-0.7.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a8b25304ffc9563bf17914a9a9bf6642456923c727d330fcfa483d303f549805"}, + {file = "whenever-0.7.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2978fb80700e583e2f957cd47c51d6b161f38a50b85a1744fcf3b13e53acf113"}, + {file = "whenever-0.7.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:246ce04d18469169582cd492b6a4f74f6c166ed2caa869679522b02228c0bbf8"}, + {file = "whenever-0.7.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6d464feea543f36dd712eee0f47ea690cf1a4d474c39ddaafe30254434ac9b2e"}, + {file = "whenever-0.7.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:253460d1194a1dcb27a47a0c6cead61cbf0a29d5bb795e7f42caa0e7be32cae9"}, + {file = "whenever-0.7.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a6b35953ca90ef5f0f2a7f3e951d110239fcccde5eccf08c4a0872821d41066"}, + {file = "whenever-0.7.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6c9bb2528c345d552e0e25ab82276dd9765185718dfdf2654f0d84771eb3fa9"}, + {file = "whenever-0.7.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bc0a7e6e5bfa15531910ca4a062fdc20c071747f016599999eac3d8fef7ea4db"}, + {file = "whenever-0.7.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8c16c03a556819c8f1738dbcfa2793c8c0d2a9a496e0ec1524fea8a124d20037"}, + {file = "whenever-0.7.2-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:43c2f1be68f638c7f3f27c60e5851b5b94aa3ba0186e84bc2010c880e71f7f84"}, + {file = "whenever-0.7.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:715c4da7fbef766bfb5511017782873c98adac9f5f982806ead9b4a99f7bb086"}, + {file = "whenever-0.7.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d3daadd03d392048a4041969132ae2a6b57941b172870c526b14c8343721967d"}, + {file = "whenever-0.7.2-cp39-cp39-win32.whl", hash = "sha256:7b3c1d9ec5dc844686aad66bb0e14dda7d9667a113757c1f566a8e8036e4585f"}, + {file = "whenever-0.7.2-cp39-cp39-win_amd64.whl", hash = "sha256:166f4d31f0be9ee59d00670f52a724c4d1090688b46e3531d0ccb74ae3157032"}, + {file = "whenever-0.7.2.tar.gz", hash = "sha256:a292dddd4d635a5b597686117e455d41e6134716a7be66b3903554514df8729c"}, +] + +[package.dependencies] +tzdata = {version = ">=2020.1", markers = "sys_platform == \"win32\""} + [[package]] name = "wrapt" version = "1.17.0" @@ -2234,5 +2207,5 @@ tests = ["Jinja2", "pytest", "pyyaml", "rich"] [metadata] lock-version = "2.0" -python-versions = "^3.9, < 3.13" -content-hash = "7cf3b9fd5e6ad627c30cb1660ef9c45d5b6a264150d064bc47cc7ae7a2be4030" +python-versions = "^3.9, <=3.13" +content-hash = "bbd7280cdf6fab3001d339f4b12d01b0883008905d1a0c3f9010eafdaec5b15f" diff --git a/pyproject.toml b/pyproject.toml index 96fe6c51..c905a307 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,10 +16,11 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", ] [tool.poetry.dependencies] -python = "^3.9, < 3.13" +python = "^3.9, <=3.13" pydantic = ">=2.0.0,!=2.0.1,!=2.1.0,<3.0.0" pydantic-settings = ">=2.0" graphql-core = ">=3.1,<3.3" @@ -27,10 +28,6 @@ httpx = [ { version = ">=0.20", python = ">=3.9,<3.11" }, { version = ">=0.23", python = ">=3.11" }, ] -pendulum = [ - { version = ">=2", python = ">=3.9,<3.12" }, - { version = ">=3", python = ">=3.12" }, -] ujson = "^5" Jinja2 = { version = "^3", optional = true } numpy = [ @@ -45,6 +42,7 @@ pytest = { version = "*", optional = true } pyyaml = { version = "^6", optional = true } eval-type-backport = { version = "^0.2.2", python = "~3.9" } dulwich = "^0.21.4" +whenever = "0.7.2" [tool.poetry.group.dev.dependencies] pytest = "*" diff --git a/tests/unit/sdk/test_timestamp.py b/tests/unit/sdk/test_timestamp.py index bcdf18a7..3f1f3165 100644 --- a/tests/unit/sdk/test_timestamp.py +++ b/tests/unit/sdk/test_timestamp.py @@ -1,17 +1,22 @@ -import pendulum +from datetime import datetime, timezone + import pytest +from whenever import Instant + +from infrahub_sdk.exceptions import TimestampFormatError +from infrahub_sdk.timestamp import Timestamp -from infrahub_sdk.timestamp import Timestamp, TimestampFormatError +UTC = timezone.utc # Required for older versions of Python def test_init_empty(): t1 = Timestamp() assert isinstance(t1, Timestamp) - assert t1.to_string() == t1.obj.to_iso8601_string() + assert t1.to_string() == t1._obj.instant().format_common_iso() t2 = Timestamp(None) assert isinstance(t2, Timestamp) - assert t2.to_string() == t2.obj.to_iso8601_string() + assert t2.to_string() == t2._obj.instant().format_common_iso() def test_init_timestamp(): @@ -19,22 +24,85 @@ def test_init_timestamp(): t2 = Timestamp(t1) assert t1.to_string() == t2.to_string() assert isinstance(t2, Timestamp) - assert t2.to_string() == t2.obj.to_iso8601_string() + assert t2.to_string() == t2._obj.instant().format_common_iso() def test_parse_string(): REF = "2022-01-01T10:00:00.000000Z" - assert Timestamp._parse_string(REF) == pendulum.parse(REF) + assert Timestamp._parse_string(REF).instant() == Instant.parse_common_iso(REF) assert Timestamp._parse_string("5m") assert Timestamp._parse_string("10min") assert Timestamp._parse_string("2h") assert Timestamp._parse_string("10s") + assert Timestamp._parse_string("2025-01-02") + assert Timestamp._parse_string("2024-06-04T03:13:03.386270") - with pytest.raises(ValueError): + with pytest.raises(TimestampFormatError): Timestamp._parse_string("notvalid") +@pytest.mark.parametrize( + "input_str,expected_datetime", + [ + pytest.param( + "2022-01-01T10:01:01.123000Z", datetime(2022, 1, 1, 10, 1, 1, 123000, tzinfo=UTC), id="milliseconds" + ), + pytest.param( + "2023-12-31T23:59:59.999999Z", datetime(2023, 12, 31, 23, 59, 59, 999999, tzinfo=UTC), id="microseconds" + ), + pytest.param( + "2025-02-25T05:58:54.524191Z", + datetime(2025, 2, 25, 5, 58, 54, 524191, tzinfo=UTC), + id="milliseconds_with_offset", + ), + pytest.param( + "2025-02-25T06:38:37.753389419Z", + datetime(2025, 2, 25, 6, 38, 37, 753389, tzinfo=UTC), + id="nanoseconds", + ), + ], +) +def test_to_datetime(input_str, expected_datetime): + assert isinstance(Timestamp(input_str).to_datetime(), datetime) + assert str(Timestamp(input_str).to_datetime()) == str(expected_datetime) + + +@pytest.mark.parametrize( + "input_str,expected_str,expected_str_no_z", + [ + pytest.param( + "2022-01-01T10:01:01.123000Z", + "2022-01-01T10:01:01.123Z", + "2022-01-01T10:01:01.123000+00:00", + id="milliseconds", + ), + pytest.param( + "2023-12-31T23:59:59.999999Z", + "2023-12-31T23:59:59.999999Z", + "2023-12-31T23:59:59.999999+00:00", + id="microseconds", + ), + ], +) +def test_to_string_default(input_str, expected_str, expected_str_no_z): + assert isinstance(Timestamp(input_str).to_string(), str) + assert Timestamp(input_str).to_string() == expected_str + assert Timestamp(input_str).to_string(with_z=False) == expected_str_no_z + + +def test_add(): + t1 = Timestamp("2022-01-01T10:01:01.123Z") + t2 = t1.add(hours=1) + assert t2.to_string() == "2022-01-01T11:01:01.123Z" + + +def test_subtract(): + t1 = Timestamp("2022-01-01T10:05:01.123Z") + t2 = t1.subtract(hours=1) + assert t2.to_string() == "2022-01-01T09:05:01.123Z" + + def test_compare(): time1 = "2022-01-01T11:00:00.000000Z" time2 = "2022-02-01T11:00:00.000000Z" diff --git a/tests/unit/sdk/test_utils.py b/tests/unit/sdk/test_utils.py index df424d2a..7f628c62 100644 --- a/tests/unit/sdk/test_utils.py +++ b/tests/unit/sdk/test_utils.py @@ -4,6 +4,7 @@ import pytest from graphql import parse +from whenever import Instant from infrahub_sdk.node import InfrahubNode from infrahub_sdk.utils import ( @@ -11,6 +12,7 @@ base16encode, base36decode, base36encode, + calculate_time_diff, compare_lists, deep_merge_dict, dict_hash, @@ -207,3 +209,20 @@ def test_write_to_file(): assert write_to_file(directory / "file.txt", {"key": "value"}) is True tmp_dir.cleanup() + + +def test_calculate_time_diff(): + time1 = Instant.now().subtract(seconds=98).format_common_iso() + assert calculate_time_diff(time1) == "1m and 38s ago" + + time2 = Instant.now().subtract(hours=1, minutes=12, seconds=34).format_common_iso() + assert calculate_time_diff(time2) == "1h 12m and 34s ago" + + time3 = Instant.now().format_common_iso() + assert calculate_time_diff(time3) == "now" + + time4 = Instant.now().subtract(seconds=23).format_common_iso() + assert calculate_time_diff(time4) == "23s ago" + + time5 = Instant.now().subtract(hours=77, minutes=12, seconds=34).format_common_iso() + assert calculate_time_diff(time5) == "3d and 5h ago"