From f6cb0ecad74b678f9fc381584ea23713d71c81e1 Mon Sep 17 00:00:00 2001 From: Brandon Willett Date: Thu, 27 Feb 2025 10:58:52 -0500 Subject: [PATCH 1/3] Replace all uses of utcnow with aware datetimes --- README.md | 6 ++-- bitmapist/__init__.py | 65 +++++++++++++++++------------------ bitmapist/cohort/__init__.py | 6 ++-- test/conftest.py | 2 +- test/test_bitmapist.py | 24 ++++++------- test/test_cohort.py | 4 +-- test/test_from_date.py | 12 +++---- test/test_period_start_end.py | 4 +-- 8 files changed, 60 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index ae3dbd1..57af03c 100644 --- a/README.md +++ b/README.md @@ -54,13 +54,13 @@ Can be installed very easily via: Setting things up: ```python -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from bitmapist import setup_redis, delete_all_events, mark_event,\ MonthEvents, WeekEvents, DayEvents, HourEvents,\ BitOpAnd, BitOpOr -now = datetime.utcnow() -last_month = datetime.utcnow() - timedelta(days=30) +now = datetime.now(tz=timezone.utc) +last_month = now - timedelta(days=30) ``` Mark user 123 as active and has played a song: diff --git a/bitmapist/__init__.py b/bitmapist/__init__.py index e276fea..fd525f6 100644 --- a/bitmapist/__init__.py +++ b/bitmapist/__init__.py @@ -33,8 +33,8 @@ from datetime import datetime, timedelta from bitmapist import mark_event, MonthEvents - now = datetime.utcnow() - last_month = datetime.utcnow() - timedelta(days=30) + now = datetime.now(tz=timezone.utc) + last_month = now - timedelta(days=30) Mark user 123 as active:: @@ -84,7 +84,7 @@ import calendar import threading from collections import defaultdict -from datetime import date, datetime, timedelta +from datetime import date, datetime, timedelta, timezone from typing import TYPE_CHECKING, Any, Optional, Union import redis @@ -159,7 +159,7 @@ def mark_event( :param :system The Redis system to use (string, Redis instance, or Pipeline instance). :param :now Which date should be used as a reference point, default is - `datetime.utcnow()` + `datetime.now(tz=timezone.utc)` :param :track_hourly Should hourly stats be tracked, defaults to bitmapist.TRACK_HOURLY :param :track_unique Should unique stats be tracked, defaults to @@ -200,7 +200,7 @@ def _mark( event_name, uuid: int, system="default", - now=None, + now: Optional[datetime] = None, track_hourly=None, track_unique=None, use_pipeline: bool = True, @@ -211,8 +211,7 @@ def _mark( if track_unique is None: track_unique = TRACK_UNIQUE - if not now: - now = datetime.utcnow() + now = now or datetime.now(tz=timezone.utc) obj_classes: list[ type[MonthEvents] @@ -474,12 +473,12 @@ class YearEvents(GenericPeriodEvents): """ @classmethod - def from_date(cls, event_name, dt=None, system="default"): - dt = dt or datetime.utcnow() + def from_date(cls, event_name, dt: Optional[date | datetime] = None, system="default"): + dt = dt or datetime.now(tz=timezone.utc) return cls(event_name, dt.year, system=system) def __init__(self, event_name, year=None, system="default"): - now = datetime.utcnow() + now = datetime.now(tz=timezone.utc) self.event_name = event_name self.year = not_none(year, now.year) self.system = system @@ -492,10 +491,10 @@ def delta(self, value): return self.__class__(self.event_name, self.year + value, self.system) def period_start(self): - return datetime(self.year, 1, 1) + return datetime(self.year, 1, 1, tzinfo=timezone.utc) def period_end(self): - return datetime(self.year, 12, 31, 23, 59, 59, 999999) + return datetime(self.year, 12, 31, 23, 59, 59, 999999, tzinfo=timezone.utc) class MonthEvents(GenericPeriodEvents): @@ -508,12 +507,12 @@ class MonthEvents(GenericPeriodEvents): """ @classmethod - def from_date(cls, event_name, dt=None, system="default"): - dt = dt or datetime.utcnow() + def from_date(cls, event_name, dt: Optional[date | datetime] = None, system="default"): + dt = dt or datetime.now(tz=timezone.utc) return cls(event_name, dt.year, dt.month, system=system) def __init__(self, event_name, year=None, month=None, system="default"): - now = datetime.utcnow() + now = datetime.now(tz=timezone.utc) self.event_name = event_name self.year = not_none(year, now.year) self.month = not_none(month, now.month) @@ -525,11 +524,11 @@ def delta(self, value): return self.__class__(self.event_name, year, month, self.system) def period_start(self): - return datetime(self.year, self.month, 1) + return datetime(self.year, self.month, 1, tzinfo=timezone.utc) def period_end(self): _, day = calendar.monthrange(self.year, self.month) - return datetime(self.year, self.month, day, 23, 59, 59, 999999) + return datetime(self.year, self.month, day, 23, 59, 59, 999999, tzinfo=timezone.utc) class WeekEvents(GenericPeriodEvents): @@ -543,14 +542,14 @@ class WeekEvents(GenericPeriodEvents): @classmethod def from_date( - cls, event_name: str, dt: Optional[date] = None, system: str = "default" + cls, event_name: str, dt: Optional[date | datetime] = None, system: str = "default" ): - dt = dt or datetime.utcnow() + dt = dt or datetime.now(tz=timezone.utc) dt_year, dt_week, _ = dt.isocalendar() return cls(event_name, dt_year, dt_week, system=system) def __init__(self, event_name: str, year=None, week=None, system="default"): - now = datetime.utcnow() + now = datetime.now(tz=timezone.utc) now_year, now_week, _ = now.isocalendar() self.event_name = event_name self.year = not_none(year, now_year) @@ -565,11 +564,11 @@ def delta(self, value): def period_start(self): s = iso_to_gregorian(self.year, self.week, 1) # mon - return datetime(s.year, s.month, s.day) + return datetime(s.year, s.month, s.day, tzinfo=timezone.utc) def period_end(self): e = iso_to_gregorian(self.year, self.week, 7) # mon - return datetime(e.year, e.month, e.day, 23, 59, 59, 999999) + return datetime(e.year, e.month, e.day, 23, 59, 59, 999999, tzinfo=timezone.utc) class DayEvents(GenericPeriodEvents): @@ -582,12 +581,12 @@ class DayEvents(GenericPeriodEvents): """ @classmethod - def from_date(cls, event_name: str, dt: Optional[date] = None, system="default"): - dt = dt or datetime.utcnow() + def from_date(cls, event_name: str, dt: Optional[date | datetime] = None, system="default"): + dt = dt or datetime.now(tz=timezone.utc) return cls(event_name, dt.year, dt.month, dt.day, system=system) def __init__(self, event_name, year=None, month=None, day=None, system="default"): - now = datetime.utcnow() + now = datetime.now(tz=timezone.utc) self.event_name = event_name self.year = not_none(year, now.year) self.month = not_none(month, now.month) @@ -600,10 +599,10 @@ def delta(self, value): return self.__class__(self.event_name, dt.year, dt.month, dt.day, self.system) def period_start(self) -> datetime: - return datetime(self.year, self.month, self.day) + return datetime(self.year, self.month, self.day, tzinfo=timezone.utc) def period_end(self) -> datetime: - return datetime(self.year, self.month, self.day, 23, 59, 59, 999999) + return datetime(self.year, self.month, self.day, 23, 59, 59, 999999, tzinfo=timezone.utc) class HourEvents(GenericPeriodEvents): @@ -619,7 +618,7 @@ class HourEvents(GenericPeriodEvents): def from_date( cls, event_name: str, dt: Optional[datetime] = None, system="default" ): - dt = dt or datetime.utcnow() + dt = dt or datetime.now(tz=timezone.utc) return cls(event_name, dt.year, dt.month, dt.day, dt.hour, system=system) def __init__( @@ -631,7 +630,7 @@ def __init__( hour: Optional[int] = None, system: str = "default", ): - now = datetime.utcnow() + now = datetime.now(tz=timezone.utc) self.event_name = event_name self.year = not_none(year, now.year) self.month = not_none(month, now.month) @@ -643,18 +642,16 @@ def __init__( ) def delta(self, value): - dt = datetime(self.year, self.month, self.day, self.hour) + timedelta( - hours=value - ) + dt = datetime(self.year, self.month, self.day, self.hour, tzinfo=timezone.utc) + timedelta(hours=value) return self.__class__( self.event_name, dt.year, dt.month, dt.day, dt.hour, self.system ) def period_start(self) -> datetime: - return datetime(self.year, self.month, self.day, self.hour) + return datetime(self.year, self.month, self.day, self.hour, tzinfo=timezone.utc) def period_end(self) -> datetime: - return datetime(self.year, self.month, self.day, self.hour, 59, 59, 999999) + return datetime(self.year, self.month, self.day, self.hour, 59, 59, 999999, tzinfo=timezone.utc) # --- Bit operations diff --git a/bitmapist/cohort/__init__.py b/bitmapist/cohort/__init__.py index 7b8439b..7189595 100644 --- a/bitmapist/cohort/__init__.py +++ b/bitmapist/cohort/__init__.py @@ -62,7 +62,7 @@ :license: BSD """ -from datetime import date, datetime, timedelta +from datetime import date, datetime, timedelta, timezone from os import path from typing import Any, Callable, Literal, Optional, Union @@ -228,10 +228,10 @@ def get_dates_data( num_of_rows = int(num_of_rows) if start_date: - now = datetime.strptime(start_date, "%Y-%m-%d") + now = datetime.strptime(start_date, "%Y-%m-%d").replace(tzinfo=timezone.utc) now = now + timedelta(days=num_results - 1) else: - now = datetime.utcnow() + now = datetime.now(tz=timezone.utc) # Days if time_group == "days": diff --git a/test/conftest.py b/test/conftest.py index 9614948..e44462d 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -9,7 +9,7 @@ from bitmapist import delete_all_events, setup_redis -@pytest.yield_fixture(scope="session", autouse=True) +@pytest.fixture(scope="session", autouse=True) def redis_server(): """Fixture starting the Redis server""" redis_host = "127.0.0.1" diff --git a/test/test_bitmapist.py b/test/test_bitmapist.py index be69709..f462e52 100644 --- a/test/test_bitmapist.py +++ b/test/test_bitmapist.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from bitmapist import ( BitOpAnd, @@ -17,7 +17,7 @@ def test_mark_with_diff_days(): mark_event("active", 123, track_hourly=True) - now = datetime.utcnow() + now = datetime.now(tz=timezone.utc) # Month assert 123 in MonthEvents("active", now.year, now.month) @@ -38,7 +38,7 @@ def test_mark_with_diff_days(): def test_mark_unmark(): - now = datetime.utcnow() + now = datetime.now(tz=timezone.utc) mark_event("active", 125) assert 125 in MonthEvents("active", now.year, now.month) @@ -48,7 +48,7 @@ def test_mark_unmark(): def test_mark_counts(): - now = datetime.utcnow() + now = datetime.now(tz=timezone.utc) assert MonthEvents("active", now.year, now.month).get_count() == 0 @@ -59,7 +59,7 @@ def test_mark_counts(): def test_mark_iter(): - now = datetime.utcnow() + now = datetime.now(tz=timezone.utc) ev = MonthEvents("active", now.year, now.month) assert list(ev) == [] @@ -73,7 +73,7 @@ def test_mark_iter(): def test_different_dates(): - now = datetime.utcnow() + now = datetime.now(tz=timezone.utc) yesterday = now - timedelta(days=1) mark_event("active", 123, now=now) @@ -88,7 +88,7 @@ def test_different_dates(): def test_different_buckets(): - now = datetime.utcnow() + now = datetime.now(tz=timezone.utc) mark_event("active", 123) mark_event("tasks:completed", 23232) @@ -98,8 +98,8 @@ def test_different_buckets(): def test_bit_operations(): - now = datetime.utcnow() - last_month = datetime.utcnow() - timedelta(days=30) + now = datetime.now(tz=timezone.utc) + last_month = now - timedelta(days=30) # 123 has been active for two months mark_event("active", 123, now=now) @@ -156,7 +156,7 @@ def test_bit_operations(): def test_bit_operations_complex(): - now = datetime.utcnow() + now = datetime.now(tz=timezone.utc) tom = now + timedelta(days=1) mark_event("task1", 111, now=now) @@ -185,7 +185,7 @@ def test_bit_operations_complex(): def test_bitop_key_sharing(): - today = datetime.utcnow() + today = datetime.now(tz=timezone.utc) mark_event("task1", 111, now=today) mark_event("task2", 111, now=today) @@ -207,7 +207,7 @@ def test_bitop_key_sharing(): def test_events_marked(): - now = datetime.utcnow() + now = datetime.now(tz=timezone.utc) assert MonthEvents("active", now.year, now.month).get_count() == 0 assert MonthEvents("active", now.year, now.month).has_events_marked() is False diff --git a/test/test_cohort.py b/test/test_cohort.py index 8d48051..6a005d6 100644 --- a/test/test_cohort.py +++ b/test/test_cohort.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone import pytest @@ -8,7 +8,7 @@ @pytest.fixture def events(): - today = datetime.utcnow() + today = datetime.now(tz=timezone.utc) tomorrow = today + timedelta(days=1) mark_event("signup", 111, now=today) diff --git a/test/test_from_date.py b/test/test_from_date.py index f181a5a..af51a08 100644 --- a/test/test_from_date.py +++ b/test/test_from_date.py @@ -1,33 +1,33 @@ -import datetime +from datetime import datetime, timezone import bitmapist def test_from_date_year(): - ev1 = bitmapist.YearEvents.from_date("foo", datetime.datetime(2014, 1, 1)) + ev1 = bitmapist.YearEvents.from_date("foo", datetime(2014, 1, 1, tzinfo=timezone.utc)) ev2 = bitmapist.YearEvents("foo", 2014) assert ev1 == ev2 def test_from_date_month(): - ev1 = bitmapist.MonthEvents.from_date("foo", datetime.datetime(2014, 1, 1)) + ev1 = bitmapist.MonthEvents.from_date("foo", datetime(2014, 1, 1, tzinfo=timezone.utc)) ev2 = bitmapist.MonthEvents("foo", 2014, 1) assert ev1 == ev2 def test_from_date_week(): - ev1 = bitmapist.MonthEvents.from_date("foo", datetime.datetime(2014, 1, 1)) + ev1 = bitmapist.MonthEvents.from_date("foo", datetime(2014, 1, 1, tzinfo=timezone.utc)) ev2 = bitmapist.MonthEvents("foo", 2014, 1) assert ev1 == ev2 def test_from_date_day(): - ev1 = bitmapist.DayEvents.from_date("foo", datetime.datetime(2014, 1, 1)) + ev1 = bitmapist.DayEvents.from_date("foo", datetime(2014, 1, 1, tzinfo=timezone.utc)) ev2 = bitmapist.DayEvents("foo", 2014, 1, 1) assert ev1 == ev2 def test_from_date_hour(): - ev1 = bitmapist.HourEvents.from_date("foo", datetime.datetime(2014, 1, 1, 1)) + ev1 = bitmapist.HourEvents.from_date("foo", datetime(2014, 1, 1, 1, tzinfo=timezone.utc)) ev2 = bitmapist.HourEvents("foo", 2014, 1, 1, 1) assert ev1 == ev2 diff --git a/test/test_period_start_end.py b/test/test_period_start_end.py index 2c86396..cced5b3 100644 --- a/test/test_period_start_end.py +++ b/test/test_period_start_end.py @@ -1,4 +1,4 @@ -import datetime +from datetime import datetime, timezone import pytest @@ -16,6 +16,6 @@ ], ) def test_period_start_end(cls): - dt = datetime.datetime(2014, 1, 1, 8, 30) + dt = datetime(2014, 1, 1, 8, 30, tzinfo=timezone.utc) ev = cls.from_date("foo", dt) assert ev.period_start() <= dt <= ev.period_end() From 57c93d030a3f8235ef08a973f98cf3042384ec61 Mon Sep 17 00:00:00 2001 From: Brandon Willett Date: Thu, 27 Feb 2025 11:04:06 -0500 Subject: [PATCH 2/3] ruff --- bitmapist/__init__.py | 40 ++++++++++++++++++++++++++++++++-------- test/test_from_date.py | 20 +++++++++++++++----- 2 files changed, 47 insertions(+), 13 deletions(-) diff --git a/bitmapist/__init__.py b/bitmapist/__init__.py index fd525f6..27df150 100644 --- a/bitmapist/__init__.py +++ b/bitmapist/__init__.py @@ -473,7 +473,9 @@ class YearEvents(GenericPeriodEvents): """ @classmethod - def from_date(cls, event_name, dt: Optional[date | datetime] = None, system="default"): + def from_date( + cls, event_name, dt: Optional[date | datetime] = None, system="default" + ): dt = dt or datetime.now(tz=timezone.utc) return cls(event_name, dt.year, system=system) @@ -507,7 +509,9 @@ class MonthEvents(GenericPeriodEvents): """ @classmethod - def from_date(cls, event_name, dt: Optional[date | datetime] = None, system="default"): + def from_date( + cls, event_name, dt: Optional[date | datetime] = None, system="default" + ): dt = dt or datetime.now(tz=timezone.utc) return cls(event_name, dt.year, dt.month, system=system) @@ -528,7 +532,9 @@ def period_start(self): def period_end(self): _, day = calendar.monthrange(self.year, self.month) - return datetime(self.year, self.month, day, 23, 59, 59, 999999, tzinfo=timezone.utc) + return datetime( + self.year, self.month, day, 23, 59, 59, 999999, tzinfo=timezone.utc + ) class WeekEvents(GenericPeriodEvents): @@ -542,7 +548,10 @@ class WeekEvents(GenericPeriodEvents): @classmethod def from_date( - cls, event_name: str, dt: Optional[date | datetime] = None, system: str = "default" + cls, + event_name: str, + dt: Optional[date | datetime] = None, + system: str = "default", ): dt = dt or datetime.now(tz=timezone.utc) dt_year, dt_week, _ = dt.isocalendar() @@ -581,7 +590,9 @@ class DayEvents(GenericPeriodEvents): """ @classmethod - def from_date(cls, event_name: str, dt: Optional[date | datetime] = None, system="default"): + def from_date( + cls, event_name: str, dt: Optional[date | datetime] = None, system="default" + ): dt = dt or datetime.now(tz=timezone.utc) return cls(event_name, dt.year, dt.month, dt.day, system=system) @@ -602,7 +613,9 @@ def period_start(self) -> datetime: return datetime(self.year, self.month, self.day, tzinfo=timezone.utc) def period_end(self) -> datetime: - return datetime(self.year, self.month, self.day, 23, 59, 59, 999999, tzinfo=timezone.utc) + return datetime( + self.year, self.month, self.day, 23, 59, 59, 999999, tzinfo=timezone.utc + ) class HourEvents(GenericPeriodEvents): @@ -642,7 +655,9 @@ def __init__( ) def delta(self, value): - dt = datetime(self.year, self.month, self.day, self.hour, tzinfo=timezone.utc) + timedelta(hours=value) + dt = datetime( + self.year, self.month, self.day, self.hour, tzinfo=timezone.utc + ) + timedelta(hours=value) return self.__class__( self.event_name, dt.year, dt.month, dt.day, dt.hour, self.system ) @@ -651,7 +666,16 @@ def period_start(self) -> datetime: return datetime(self.year, self.month, self.day, self.hour, tzinfo=timezone.utc) def period_end(self) -> datetime: - return datetime(self.year, self.month, self.day, self.hour, 59, 59, 999999, tzinfo=timezone.utc) + return datetime( + self.year, + self.month, + self.day, + self.hour, + 59, + 59, + 999999, + tzinfo=timezone.utc, + ) # --- Bit operations diff --git a/test/test_from_date.py b/test/test_from_date.py index af51a08..af03d3f 100644 --- a/test/test_from_date.py +++ b/test/test_from_date.py @@ -4,30 +4,40 @@ def test_from_date_year(): - ev1 = bitmapist.YearEvents.from_date("foo", datetime(2014, 1, 1, tzinfo=timezone.utc)) + ev1 = bitmapist.YearEvents.from_date( + "foo", datetime(2014, 1, 1, tzinfo=timezone.utc) + ) ev2 = bitmapist.YearEvents("foo", 2014) assert ev1 == ev2 def test_from_date_month(): - ev1 = bitmapist.MonthEvents.from_date("foo", datetime(2014, 1, 1, tzinfo=timezone.utc)) + ev1 = bitmapist.MonthEvents.from_date( + "foo", datetime(2014, 1, 1, tzinfo=timezone.utc) + ) ev2 = bitmapist.MonthEvents("foo", 2014, 1) assert ev1 == ev2 def test_from_date_week(): - ev1 = bitmapist.MonthEvents.from_date("foo", datetime(2014, 1, 1, tzinfo=timezone.utc)) + ev1 = bitmapist.MonthEvents.from_date( + "foo", datetime(2014, 1, 1, tzinfo=timezone.utc) + ) ev2 = bitmapist.MonthEvents("foo", 2014, 1) assert ev1 == ev2 def test_from_date_day(): - ev1 = bitmapist.DayEvents.from_date("foo", datetime(2014, 1, 1, tzinfo=timezone.utc)) + ev1 = bitmapist.DayEvents.from_date( + "foo", datetime(2014, 1, 1, tzinfo=timezone.utc) + ) ev2 = bitmapist.DayEvents("foo", 2014, 1, 1) assert ev1 == ev2 def test_from_date_hour(): - ev1 = bitmapist.HourEvents.from_date("foo", datetime(2014, 1, 1, 1, tzinfo=timezone.utc)) + ev1 = bitmapist.HourEvents.from_date( + "foo", datetime(2014, 1, 1, 1, tzinfo=timezone.utc) + ) ev2 = bitmapist.HourEvents("foo", 2014, 1, 1, 1) assert ev1 == ev2 From 1acf98d32a33bfb5ccf4c57059959c743a189688 Mon Sep 17 00:00:00 2001 From: Brandon Willett Date: Fri, 28 Feb 2025 11:46:32 -0500 Subject: [PATCH 3/3] style change --- bitmapist/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bitmapist/__init__.py b/bitmapist/__init__.py index 27df150..09f9951 100644 --- a/bitmapist/__init__.py +++ b/bitmapist/__init__.py @@ -210,8 +210,8 @@ def _mark( track_hourly = TRACK_HOURLY if track_unique is None: track_unique = TRACK_UNIQUE - - now = now or datetime.now(tz=timezone.utc) + if now is None: + now = datetime.now(tz=timezone.utc) obj_classes: list[ type[MonthEvents]