Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace all uses of utcnow with tz-aware datetimes #72

Merged
merged 3 commits into from
Mar 3, 2025
Merged
Show file tree
Hide file tree
Changes from 2 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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
89 changes: 55 additions & 34 deletions bitmapist/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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::

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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]
Expand Down Expand Up @@ -474,12 +473,14 @@ 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
Expand All @@ -492,10 +493,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):
Expand All @@ -508,12 +509,14 @@ 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)
Expand All @@ -525,11 +528,13 @@ 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):
Expand All @@ -543,14 +548,17 @@ 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)
Expand All @@ -565,11 +573,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):
Expand All @@ -582,12 +590,14 @@ 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)
Expand All @@ -600,10 +610,12 @@ 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):
Expand All @@ -619,7 +631,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__(
Expand All @@ -631,7 +643,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)
Expand All @@ -643,18 +655,27 @@ 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
Expand Down
6 changes: 3 additions & 3 deletions bitmapist/cohort/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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":
Expand Down
2 changes: 1 addition & 1 deletion test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
24 changes: 12 additions & 12 deletions test/test_bitmapist.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone

from bitmapist import (
BitOpAnd,
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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

Expand All @@ -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) == []
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions test/test_cohort.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone

import pytest

Expand All @@ -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)
Expand Down
Loading