Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .sampo/changesets/ardent-king-tursas.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
pypi/posthog: patch
---

Remove python-dateutil as a runtime dependency
10 changes: 2 additions & 8 deletions mypy-baseline.txt
Original file line number Diff line number Diff line change
@@ -1,21 +1,15 @@
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[<type>] = ...") [var-annotated]
posthog/consumer.py:0: error: Unsupported operand types for <= ("int" and "str") [operator]
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]
Expand Down
11 changes: 5 additions & 6 deletions posthog/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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):
"""
Expand Down
52 changes: 42 additions & 10 deletions posthog/feature_flags.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import calendar
import datetime
import hashlib
import logging
import re
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
Expand Down Expand Up @@ -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(
Expand All @@ -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
Expand Down Expand Up @@ -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)
Comment thread
marandaneto marked this conversation as resolved.


# 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
Comment thread
marandaneto marked this conversation as resolved.
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]:
Expand All @@ -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

Expand Down
3 changes: 1 addition & 2 deletions posthog/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading