diff --git a/.sampo/changesets/ardent-king-tursas.md b/.sampo/changesets/ardent-king-tursas.md new file mode 100644 index 00000000..07afcc41 --- /dev/null +++ b/.sampo/changesets/ardent-king-tursas.md @@ -0,0 +1,5 @@ +--- +pypi/posthog: patch +--- + +Remove python-dateutil as a runtime dependency diff --git a/mypy-baseline.txt b/mypy-baseline.txt index 000341d1..0189ac4d 100644 --- a/mypy-baseline.txt +++ b/mypy-baseline.txt @@ -1,7 +1,7 @@ -posthog/utils.py:0: error: Library stubs not installed for "dateutil.tz" [import-untyped] posthog/request.py:0: error: Library stubs not installed for "requests" [import-untyped] posthog/request.py:0: note: Hint: "python3 -m pip install types-requests" -posthog/request.py:0: error: Library stubs not installed for "dateutil.tz" [import-untyped] +posthog/request.py:0: note: (or run "mypy --install-types" to install all missing stub packages) +posthog/request.py:0: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports posthog/request.py:0: error: Incompatible types in assignment (expression has type "bytes", variable has type "str") [assignment] posthog/consumer.py:0: error: Name "Empty" already defined (possibly by an import) [no-redef] posthog/consumer.py:0: error: Need type annotation for "items" (hint: "items: list[] = ...") [var-annotated] @@ -9,13 +9,7 @@ posthog/consumer.py:0: error: Unsupported operand types for <= ("int" and "str") posthog/consumer.py:0: note: Right operand is of type "int | str" posthog/consumer.py:0: error: Unsupported operand types for < ("str" and "int") [operator] posthog/consumer.py:0: note: Left operand is of type "int | str" -posthog/feature_flags.py:0: error: Library stubs not installed for "dateutil" [import-untyped] -posthog/feature_flags.py:0: error: Library stubs not installed for "dateutil.relativedelta" [import-untyped] posthog/feature_flags.py:0: error: Unused "type: ignore" comment [unused-ignore] -posthog/client.py:0: error: Library stubs not installed for "dateutil.tz" [import-untyped] -posthog/client.py:0: note: Hint: "python3 -m pip install types-python-dateutil" -posthog/client.py:0: note: (or run "mypy --install-types" to install all missing stub packages) -posthog/client.py:0: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports posthog/client.py:0: error: Name "queue" already defined (by an import) [no-redef] posthog/client.py:0: error: Need type annotation for "queue" [var-annotated] posthog/client.py:0: error: Incompatible types in assignment (expression has type "Any | list[Any]", variable has type "None") [assignment] diff --git a/posthog/client.py b/posthog/client.py index 1b7f0a34..a4ffa1ed 100644 --- a/posthog/client.py +++ b/posthog/client.py @@ -3,11 +3,10 @@ import os import sys import warnings -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from typing import Any, Dict, Optional, Union from uuid import uuid4 -from dateutil.tz import tzutc from typing_extensions import Unpack from posthog.args import ID_TYPES, ExceptionArg, OptionalCaptureArgs, OptionalSetArgs @@ -1100,7 +1099,7 @@ def _enqueue(self, msg, disable_geoip): timestamp = msg["timestamp"] if timestamp is None: - timestamp = datetime.now(tz=tzutc()) + timestamp = datetime.now(tz=timezone.utc) # add common timestamp = guess_timezone(timestamp) @@ -1277,7 +1276,7 @@ def _load_feature_flags(self): self._update_flag_state( cached_data, old_flags_by_key=self.feature_flags_by_key or {} ) - self._last_feature_flag_poll = datetime.now(tz=tzutc()) + self._last_feature_flag_poll = datetime.now(tz=timezone.utc) return else: # Emergency fallback: if cache is empty and we have no flags, fetch anyway. @@ -1324,7 +1323,7 @@ def _fetch_feature_flags_from_api(self): self.log.debug( "[FEATURE FLAGS] Flags not modified (304), using cached data" ) - self._last_feature_flag_poll = datetime.now(tz=tzutc()) + self._last_feature_flag_poll = datetime.now(tz=timezone.utc) return if response.data is None: @@ -1395,7 +1394,7 @@ def _fetch_feature_flags_from_api(self): ) self.log.warning(e) - self._last_feature_flag_poll = datetime.now(tz=tzutc()) + self._last_feature_flag_poll = datetime.now(tz=timezone.utc) def load_feature_flags(self): """ diff --git a/posthog/feature_flags.py b/posthog/feature_flags.py index edfdbef9..dda34053 100644 --- a/posthog/feature_flags.py +++ b/posthog/feature_flags.py @@ -1,3 +1,4 @@ +import calendar import datetime import hashlib import logging @@ -5,9 +6,6 @@ import warnings from typing import Optional -from dateutil import parser -from dateutil.relativedelta import relativedelta - from posthog import utils from posthog.types import FlagValue from posthog.utils import convert_to_datetime_aware, is_valid_regex @@ -530,7 +528,7 @@ def compare(lhs, rhs, operator): parsed_date = relative_date_parse_for_feature_flag_matching(str(value)) if not parsed_date: - parsed_date = parser.parse(str(value)) + parsed_date = parse_datetime(str(value)) parsed_date = convert_to_datetime_aware(parsed_date) except Exception as e: raise InconclusiveMatchError( @@ -555,7 +553,7 @@ def compare(lhs, rhs, operator): return override_value > parsed_date.date() elif isinstance(override_value, str): try: - override_date = parser.parse(override_value) + override_date = parse_datetime(override_value) override_date = convert_to_datetime_aware(override_date) if operator == "is_date_before": return override_date < parsed_date @@ -776,6 +774,40 @@ def match_property_group( return property_group_type == "AND" +def parse_datetime(value: str) -> datetime.datetime: + text = value.strip() + if text.endswith("Z"): + text = text[:-1] + "+00:00" + elif text.upper().endswith(" UTC"): + text = text[:-4] + "+00:00" + elif re.fullmatch(r"\d{4}", text): + now = datetime.datetime.now() + return datetime.datetime(int(text), now.month, now.day) + + text = re.sub(r" ([+-]\d{2}:\d{2})$", r"\1", text) + return datetime.datetime.fromisoformat(text) + + +# Python's stdlib doesn't provide a calendar-aware month/year delta. +# Clamp the day to the target month's end to match dateutil.relativedelta behavior. +def _subtract_months(dt: datetime.datetime, months: int) -> Optional[datetime.datetime]: + month_index = dt.year * 12 + dt.month - 1 - months + year = month_index // 12 + month = month_index % 12 + 1 + if not 1 <= year <= 9999: + return None + day = min(dt.day, calendar.monthrange(year, month)[1]) + return dt.replace(year=year, month=month, day=day) + + +def _subtract_years(dt: datetime.datetime, years: int) -> Optional[datetime.datetime]: + year = dt.year - years + if not 1 <= year <= 9999: + return None + day = min(dt.day, calendar.monthrange(year, dt.month)[1]) + return dt.replace(year=year, day=day) + + def relative_date_parse_for_feature_flag_matching( value: str, ) -> Optional[datetime.datetime]: @@ -791,15 +823,15 @@ def relative_date_parse_for_feature_flag_matching( interval = match.group("interval") if interval == "h": - parsed_dt = parsed_dt - relativedelta(hours=number) + parsed_dt = parsed_dt - datetime.timedelta(hours=number) elif interval == "d": - parsed_dt = parsed_dt - relativedelta(days=number) + parsed_dt = parsed_dt - datetime.timedelta(days=number) elif interval == "w": - parsed_dt = parsed_dt - relativedelta(weeks=number) + parsed_dt = parsed_dt - datetime.timedelta(weeks=number) elif interval == "m": - parsed_dt = parsed_dt - relativedelta(months=number) + return _subtract_months(parsed_dt, number) elif interval == "y": - parsed_dt = parsed_dt - relativedelta(years=number) + return _subtract_years(parsed_dt, number) else: return None diff --git a/posthog/request.py b/posthog/request.py index fa54fac7..449f0bc4 100644 --- a/posthog/request.py +++ b/posthog/request.py @@ -9,7 +9,6 @@ from typing import Any, List, Optional, Tuple, Union import requests -from dateutil.tz import tzutc from requests.adapters import HTTPAdapter # type: ignore[import-untyped] from urllib3.connection import HTTPConnection from urllib3.util.retry import Retry @@ -197,7 +196,7 @@ def post( """Post the `kwargs` to the API""" log = logging.getLogger("posthog") body = kwargs - body["sentAt"] = datetime.now(tz=tzutc()).isoformat() + body["sentAt"] = datetime.now(tz=timezone.utc).isoformat() trimmed_host = remove_trailing_slash(normalize_host(host)) url = trimmed_host + path body["api_key"] = api_key diff --git a/posthog/test/test_feature_flags.py b/posthog/test/test_feature_flags.py index 0a8269a5..0ce5e72c 100644 --- a/posthog/test/test_feature_flags.py +++ b/posthog/test/test_feature_flags.py @@ -2,7 +2,9 @@ import unittest from unittest import mock -from dateutil import parser, tz +from zoneinfo import ZoneInfo +from dateutil import parser +from dateutil.relativedelta import relativedelta from freezegun import freeze_time from parameterized import parameterized @@ -10,6 +12,7 @@ from posthog.feature_flags import ( InconclusiveMatchError, match_property, + parse_datetime, relative_date_parse_for_feature_flag_matching, ) from posthog.request import APIError, GetResponse @@ -4107,12 +4110,22 @@ def test_match_property_date_operators(self): property_a, { "key": datetime.datetime( - 2022, 4, 30, 1, 2, 3, tzinfo=tz.gettz("Europe/Madrid") + 2022, + 4, + 30, + 1, + 2, + 3, + tzinfo=ZoneInfo("Europe/Madrid"), ) }, ) ) - self.assertTrue(match_property(property_a, {"key": parser.parse("2022-04-30")})) + self.assertTrue( + match_property( + property_a, {"key": datetime.datetime.fromisoformat("2022-04-30")} + ) + ) self.assertFalse(match_property(property_a, {"key": "2022-05-30"})) # Can't be a number @@ -4131,7 +4144,11 @@ def test_match_property_date_operators(self): self.assertTrue( match_property(property_b, {"key": datetime.datetime(2022, 5, 30)}) ) - self.assertTrue(match_property(property_b, {"key": parser.parse("2022-05-30")})) + self.assertTrue( + match_property( + property_b, {"key": datetime.datetime.fromisoformat("2022-05-30")} + ) + ) self.assertFalse(match_property(property_b, {"key": "2022-04-30"})) # can't be invalid string @@ -4192,12 +4209,22 @@ def test_match_property_relative_date_operators(self): property_a, { "key": datetime.datetime( - 2022, 4, 30, 1, 2, 3, tzinfo=tz.gettz("Europe/Madrid") + 2022, + 4, + 30, + 1, + 2, + 3, + tzinfo=ZoneInfo("Europe/Madrid"), ) }, ) ) - self.assertTrue(match_property(property_a, {"key": parser.parse("2022-04-30")})) + self.assertTrue( + match_property( + property_a, {"key": datetime.datetime.fromisoformat("2022-04-30")} + ) + ) self.assertFalse(match_property(property_a, {"key": "2022-05-30"})) # Can't be a number @@ -4214,7 +4241,11 @@ def test_match_property_relative_date_operators(self): self.assertTrue( match_property(property_b, {"key": datetime.datetime(2022, 5, 30)}) ) - self.assertTrue(match_property(property_b, {"key": parser.parse("2022-05-30")})) + self.assertTrue( + match_property( + property_b, {"key": datetime.datetime.fromisoformat("2022-05-30")} + ) + ) self.assertFalse(match_property(property_b, {"key": "2022-04-30"})) # can't be invalid string @@ -4548,6 +4579,23 @@ def test_unknown_operator(self): ) +class TestDateParsing(unittest.TestCase): + @parameterized.expand( + [ + ("iso_date", "2022-05-01"), + ("iso_datetime_Z", "2022-04-05T12:34:12Z"), + ("iso_datetime_utc", "2022-04-05 12:34:12 UTC"), + ("iso_datetime_offset", "2022-04-05 12:34:12 +01:00"), + ] + ) + def test_parse_datetime_matches_dateutil_for_supported_formats(self, _name, value): + assert parse_datetime(value) == parser.parse(value) + + def test_parse_datetime_year_only_matches_previous_dateutil_behavior(self): + with freeze_time("2020-04-03T00:00:00"): + assert parse_datetime("1234") == parser.parse("1234") + + class TestRelativeDateParsing(unittest.TestCase): def test_invalid_input(self): with freeze_time("2020-01-01T12:01:20.1340Z"): @@ -4568,6 +4616,7 @@ def test_invalid_input(self): def test_overflow(self): assert relative_date_parse_for_feature_flag_matching("1000000h") is None + assert relative_date_parse_for_feature_flag_matching("9999y") is None assert ( relative_date_parse_for_feature_flag_matching("100000000000000000y") is None ) @@ -4577,27 +4626,27 @@ def test_hour_parsing(self): assert relative_date_parse_for_feature_flag_matching( "1h" ) == datetime.datetime( - 2020, 1, 1, 11, 1, 20, 134000, tzinfo=tz.gettz("UTC") + 2020, 1, 1, 11, 1, 20, 134000, tzinfo=datetime.timezone.utc ) assert relative_date_parse_for_feature_flag_matching( "2h" ) == datetime.datetime( - 2020, 1, 1, 10, 1, 20, 134000, tzinfo=tz.gettz("UTC") + 2020, 1, 1, 10, 1, 20, 134000, tzinfo=datetime.timezone.utc ) assert relative_date_parse_for_feature_flag_matching( "24h" ) == datetime.datetime( - 2019, 12, 31, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC") + 2019, 12, 31, 12, 1, 20, 134000, tzinfo=datetime.timezone.utc ) assert relative_date_parse_for_feature_flag_matching( "30h" ) == datetime.datetime( - 2019, 12, 31, 6, 1, 20, 134000, tzinfo=tz.gettz("UTC") + 2019, 12, 31, 6, 1, 20, 134000, tzinfo=datetime.timezone.utc ) assert relative_date_parse_for_feature_flag_matching( "48h" ) == datetime.datetime( - 2019, 12, 30, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC") + 2019, 12, 30, 12, 1, 20, 134000, tzinfo=datetime.timezone.utc ) assert relative_date_parse_for_feature_flag_matching( @@ -4612,27 +4661,27 @@ def test_day_parsing(self): assert relative_date_parse_for_feature_flag_matching( "1d" ) == datetime.datetime( - 2019, 12, 31, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC") + 2019, 12, 31, 12, 1, 20, 134000, tzinfo=datetime.timezone.utc ) assert relative_date_parse_for_feature_flag_matching( "2d" ) == datetime.datetime( - 2019, 12, 30, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC") + 2019, 12, 30, 12, 1, 20, 134000, tzinfo=datetime.timezone.utc ) assert relative_date_parse_for_feature_flag_matching( "7d" ) == datetime.datetime( - 2019, 12, 25, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC") + 2019, 12, 25, 12, 1, 20, 134000, tzinfo=datetime.timezone.utc ) assert relative_date_parse_for_feature_flag_matching( "14d" ) == datetime.datetime( - 2019, 12, 18, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC") + 2019, 12, 18, 12, 1, 20, 134000, tzinfo=datetime.timezone.utc ) assert relative_date_parse_for_feature_flag_matching( "30d" ) == datetime.datetime( - 2019, 12, 2, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC") + 2019, 12, 2, 12, 1, 20, 134000, tzinfo=datetime.timezone.utc ) assert relative_date_parse_for_feature_flag_matching( @@ -4644,28 +4693,28 @@ def test_week_parsing(self): assert relative_date_parse_for_feature_flag_matching( "1w" ) == datetime.datetime( - 2019, 12, 25, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC") + 2019, 12, 25, 12, 1, 20, 134000, tzinfo=datetime.timezone.utc ) assert relative_date_parse_for_feature_flag_matching( "2w" ) == datetime.datetime( - 2019, 12, 18, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC") + 2019, 12, 18, 12, 1, 20, 134000, tzinfo=datetime.timezone.utc ) assert relative_date_parse_for_feature_flag_matching( "4w" ) == datetime.datetime( - 2019, 12, 4, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC") + 2019, 12, 4, 12, 1, 20, 134000, tzinfo=datetime.timezone.utc ) assert relative_date_parse_for_feature_flag_matching( "8w" ) == datetime.datetime( - 2019, 11, 6, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC") + 2019, 11, 6, 12, 1, 20, 134000, tzinfo=datetime.timezone.utc ) assert relative_date_parse_for_feature_flag_matching( "1m" ) == datetime.datetime( - 2019, 12, 1, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC") + 2019, 12, 1, 12, 1, 20, 134000, tzinfo=datetime.timezone.utc ) assert relative_date_parse_for_feature_flag_matching( "4w" @@ -4676,28 +4725,28 @@ def test_month_parsing(self): assert relative_date_parse_for_feature_flag_matching( "1m" ) == datetime.datetime( - 2019, 12, 1, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC") + 2019, 12, 1, 12, 1, 20, 134000, tzinfo=datetime.timezone.utc ) assert relative_date_parse_for_feature_flag_matching( "2m" ) == datetime.datetime( - 2019, 11, 1, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC") + 2019, 11, 1, 12, 1, 20, 134000, tzinfo=datetime.timezone.utc ) assert relative_date_parse_for_feature_flag_matching( "4m" ) == datetime.datetime( - 2019, 9, 1, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC") + 2019, 9, 1, 12, 1, 20, 134000, tzinfo=datetime.timezone.utc ) assert relative_date_parse_for_feature_flag_matching( "8m" ) == datetime.datetime( - 2019, 5, 1, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC") + 2019, 5, 1, 12, 1, 20, 134000, tzinfo=datetime.timezone.utc ) assert relative_date_parse_for_feature_flag_matching( "1y" ) == datetime.datetime( - 2019, 1, 1, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC") + 2019, 1, 1, 12, 1, 20, 134000, tzinfo=datetime.timezone.utc ) assert relative_date_parse_for_feature_flag_matching( "12m" @@ -4706,45 +4755,60 @@ def test_month_parsing(self): with freeze_time("2020-04-03T00:00:00"): assert relative_date_parse_for_feature_flag_matching( "1m" - ) == datetime.datetime(2020, 3, 3, 0, 0, 0, tzinfo=tz.gettz("UTC")) + ) == datetime.datetime(2020, 3, 3, 0, 0, 0, tzinfo=datetime.timezone.utc) assert relative_date_parse_for_feature_flag_matching( "2m" - ) == datetime.datetime(2020, 2, 3, 0, 0, 0, tzinfo=tz.gettz("UTC")) + ) == datetime.datetime(2020, 2, 3, 0, 0, 0, tzinfo=datetime.timezone.utc) assert relative_date_parse_for_feature_flag_matching( "4m" - ) == datetime.datetime(2019, 12, 3, 0, 0, 0, tzinfo=tz.gettz("UTC")) + ) == datetime.datetime(2019, 12, 3, 0, 0, 0, tzinfo=datetime.timezone.utc) assert relative_date_parse_for_feature_flag_matching( "8m" - ) == datetime.datetime(2019, 8, 3, 0, 0, 0, tzinfo=tz.gettz("UTC")) + ) == datetime.datetime(2019, 8, 3, 0, 0, 0, tzinfo=datetime.timezone.utc) assert relative_date_parse_for_feature_flag_matching( "1y" - ) == datetime.datetime(2019, 4, 3, 0, 0, 0, tzinfo=tz.gettz("UTC")) + ) == datetime.datetime(2019, 4, 3, 0, 0, 0, tzinfo=datetime.timezone.utc) assert relative_date_parse_for_feature_flag_matching( "12m" ) == relative_date_parse_for_feature_flag_matching("1y") + def test_month_parsing_clamps_to_target_month_end(self): + for value in ("2020-03-31T00:00:00Z", "2021-03-31T00:00:00Z"): + with freeze_time(value): + now = datetime.datetime.now(datetime.timezone.utc) + assert relative_date_parse_for_feature_flag_matching( + "1m" + ) == now - relativedelta(months=1) + + def test_year_parsing_clamps_to_target_month_end(self): + with freeze_time("2020-02-29T00:00:00Z"): + now = datetime.datetime.now(datetime.timezone.utc) + assert relative_date_parse_for_feature_flag_matching( + "1y" + ) == now - relativedelta(years=1) + def test_year_parsing(self): with freeze_time("2020-01-01T12:01:20.1340Z"): assert relative_date_parse_for_feature_flag_matching( "1y" ) == datetime.datetime( - 2019, 1, 1, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC") + 2019, 1, 1, 12, 1, 20, 134000, tzinfo=datetime.timezone.utc ) assert relative_date_parse_for_feature_flag_matching( "2y" ) == datetime.datetime( - 2018, 1, 1, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC") + 2018, 1, 1, 12, 1, 20, 134000, tzinfo=datetime.timezone.utc ) assert relative_date_parse_for_feature_flag_matching( "4y" ) == datetime.datetime( - 2016, 1, 1, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC") + 2016, 1, 1, 12, 1, 20, 134000, tzinfo=datetime.timezone.utc ) assert relative_date_parse_for_feature_flag_matching( "8y" ) == datetime.datetime( - 2012, 1, 1, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC") + 2012, 1, 1, 12, 1, 20, 134000, tzinfo=datetime.timezone.utc ) diff --git a/posthog/test/test_utils.py b/posthog/test/test_utils.py index b6907be1..ed19005a 100644 --- a/posthog/test/test_utils.py +++ b/posthog/test/test_utils.py @@ -2,12 +2,11 @@ import time import unittest from dataclasses import dataclass -from datetime import date, datetime, timedelta +from datetime import date, datetime, timedelta, timezone from decimal import Decimal from typing import Optional from uuid import UUID -from dateutil.tz import tzutc from parameterized import parameterized from pydantic import BaseModel from pydantic.v1 import BaseModel as BaseModelV1 @@ -30,13 +29,13 @@ def test_is_naive(self, _name: str, expected_naive: bool): if expected_naive: dt = datetime.now() # naive datetime else: - dt = datetime.now(tz=tzutc()) # timezone-aware datetime + dt = datetime.now(tz=timezone.utc) # timezone-aware datetime assert utils.is_naive(dt) is expected_naive def test_timezone_utils(self): now = datetime.now() - utcnow = datetime.now(tz=tzutc()) + utcnow = datetime.now(tz=timezone.utc) fixed = utils.guess_timezone(now) assert utils.is_naive(fixed) is False @@ -80,7 +79,7 @@ def test_clean(self): def test_clean_with_dates(self): dict_with_dates = { "birthdate": date(1980, 1, 1), - "registration": datetime.now(tz=tzutc()), + "registration": datetime.now(tz=timezone.utc), } assert dict_with_dates == utils.clean(dict_with_dates) diff --git a/posthog/utils.py b/posthog/utils.py index d1dbc7ea..8c28a091 100644 --- a/posthog/utils.py +++ b/posthog/utils.py @@ -13,8 +13,6 @@ import platform import distro # For Linux OS detection -from dateutil.tz import tzlocal, tzutc - log = logging.getLogger("posthog") @@ -36,12 +34,12 @@ def guess_timezone(dt): # case, and then defaults to utc delta = datetime.now() - dt if total_seconds(delta) < 5: - # this was created using datetime.datetime.now() - # so we are in the local timezone - return dt.replace(tzinfo=tzlocal()) + # this was created using datetime.datetime.now(), + # so use the current system local timezone + return dt.replace(tzinfo=datetime.now().astimezone().tzinfo) else: # at this point, the best we can do is guess UTC - return dt.replace(tzinfo=tzutc()) + return dt.replace(tzinfo=timezone.utc) return dt diff --git a/pyproject.toml b/pyproject.toml index 4904b87f..869d9e19 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,6 @@ classifiers = [ ] dependencies = [ "requests>=2.7,<3.0", - "python-dateutil>=2.2", "backoff>=1.10.0", "distro>=1.5.0", "typing-extensions>=4.2.0", @@ -46,7 +45,6 @@ dev = [ "lxml", "mypy", "mypy-baseline", - "types-python-dateutil", "types-requests", "types-setuptools", "pre-commit", @@ -61,6 +59,8 @@ dev = [ ] test = [ "freezegun==1.5.1", + "python-dateutil>=2.9.0.post0", + "tzdata", "coverage", "pytest", "pytest-timeout", diff --git a/uv.lock b/uv.lock index c1d0bb9e..42be7134 100644 --- a/uv.lock +++ b/uv.lock @@ -8,7 +8,7 @@ resolution-markers = [ ] [options] -exclude-newer = "2026-04-17T19:08:13.029980724Z" +exclude-newer = "2026-04-21T13:48:20.831231Z" exclude-newer-span = "P7D" [[package]] @@ -2042,7 +2042,6 @@ source = { editable = "." } dependencies = [ { name = "backoff" }, { name = "distro" }, - { name = "python-dateutil" }, { name = "requests" }, { name = "typing-extensions" }, ] @@ -2061,7 +2060,6 @@ dev = [ { name = "tomli" }, { name = "tomli-w" }, { name = "twine" }, - { name = "types-python-dateutil" }, { name = "types-requests" }, { name = "types-setuptools" }, { name = "wheel" }, @@ -2094,7 +2092,9 @@ test = [ { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-timeout" }, + { name = "python-dateutil" }, { name = "tiktoken" }, + { name = "tzdata" }, ] [package.dev-dependencies] @@ -2136,7 +2136,7 @@ requires-dist = [ { name = "pytest", marker = "extra == 'test'" }, { name = "pytest-asyncio", marker = "extra == 'test'" }, { name = "pytest-timeout", marker = "extra == 'test'" }, - { name = "python-dateutil", specifier = ">=2.2" }, + { name = "python-dateutil", marker = "extra == 'test'", specifier = ">=2.9.0.post0" }, { name = "requests", specifier = ">=2.7,<3.0" }, { name = "ruff", marker = "extra == 'dev'" }, { name = "setuptools", marker = "extra == 'dev'" }, @@ -2144,10 +2144,10 @@ requires-dist = [ { name = "tomli", marker = "extra == 'dev'" }, { name = "tomli-w", marker = "extra == 'dev'" }, { name = "twine", marker = "extra == 'dev'" }, - { name = "types-python-dateutil", marker = "extra == 'dev'" }, { name = "types-requests", marker = "extra == 'dev'" }, { name = "types-setuptools", marker = "extra == 'dev'" }, { name = "typing-extensions", specifier = ">=4.2.0" }, + { name = "tzdata", marker = "extra == 'test'" }, { name = "wheel", marker = "extra == 'dev'" }, ] provides-extras = ["langchain", "otel", "dev", "test"] @@ -3201,15 +3201,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7c/b6/74e927715a285743351233f33ea3c684528a0d374d2e43ff9ce9585b73fe/twine-6.1.0-py3-none-any.whl", hash = "sha256:a47f973caf122930bf0fbbf17f80b83bc1602c9ce393c7845f289a3001dc5384", size = 40791, upload-time = "2025-01-21T18:45:24.584Z" }, ] -[[package]] -name = "types-python-dateutil" -version = "2.9.0.20250516" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ef/88/d65ed807393285204ab6e2801e5d11fbbea811adcaa979a2ed3b67a5ef41/types_python_dateutil-2.9.0.20250516.tar.gz", hash = "sha256:13e80d6c9c47df23ad773d54b2826bd52dbbb41be87c3f339381c1700ad21ee5", size = 13943, upload-time = "2025-05-16T03:06:58.385Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/3f/b0e8db149896005adc938a1e7f371d6d7e9eca4053a29b108978ed15e0c2/types_python_dateutil-2.9.0.20250516-py3-none-any.whl", hash = "sha256:2b2b3f57f9c6a61fba26a9c0ffb9ea5681c9b83e69cd897c6b5f668d9c0cab93", size = 14356, upload-time = "2025-05-16T03:06:57.249Z" }, -] - [[package]] name = "types-pyyaml" version = "6.0.12.20250516"