Skip to content

Commit dac0d60

Browse files
Merge pull request #72 from Doist/brandon/312
Replace all uses of utcnow with tz-aware datetimes
2 parents e5d4b88 + 1acf98d commit dac0d60

File tree

8 files changed

+95
-64
lines changed

8 files changed

+95
-64
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,13 @@ Can be installed very easily via:
5454
Setting things up:
5555

5656
```python
57-
from datetime import datetime, timedelta
57+
from datetime import datetime, timedelta, timezone
5858
from bitmapist import setup_redis, delete_all_events, mark_event,\
5959
MonthEvents, WeekEvents, DayEvents, HourEvents,\
6060
BitOpAnd, BitOpOr
6161

62-
now = datetime.utcnow()
63-
last_month = datetime.utcnow() - timedelta(days=30)
62+
now = datetime.now(tz=timezone.utc)
63+
last_month = now - timedelta(days=30)
6464
```
6565

6666
Mark user 123 as active and has played a song:

bitmapist/__init__.py

Lines changed: 56 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@
3333
from datetime import datetime, timedelta
3434
from bitmapist import mark_event, MonthEvents
3535
36-
now = datetime.utcnow()
37-
last_month = datetime.utcnow() - timedelta(days=30)
36+
now = datetime.now(tz=timezone.utc)
37+
last_month = now - timedelta(days=30)
3838
3939
Mark user 123 as active::
4040
@@ -84,7 +84,7 @@
8484
import calendar
8585
import threading
8686
from collections import defaultdict
87-
from datetime import date, datetime, timedelta
87+
from datetime import date, datetime, timedelta, timezone
8888
from typing import TYPE_CHECKING, Any, Optional, Union
8989

9090
import redis
@@ -159,7 +159,7 @@ def mark_event(
159159
:param :system The Redis system to use (string, Redis instance, or Pipeline
160160
instance).
161161
:param :now Which date should be used as a reference point, default is
162-
`datetime.utcnow()`
162+
`datetime.now(tz=timezone.utc)`
163163
:param :track_hourly Should hourly stats be tracked, defaults to
164164
bitmapist.TRACK_HOURLY
165165
:param :track_unique Should unique stats be tracked, defaults to
@@ -200,7 +200,7 @@ def _mark(
200200
event_name,
201201
uuid: int,
202202
system="default",
203-
now=None,
203+
now: Optional[datetime] = None,
204204
track_hourly=None,
205205
track_unique=None,
206206
use_pipeline: bool = True,
@@ -210,9 +210,8 @@ def _mark(
210210
track_hourly = TRACK_HOURLY
211211
if track_unique is None:
212212
track_unique = TRACK_UNIQUE
213-
214-
if not now:
215-
now = datetime.utcnow()
213+
if now is None:
214+
now = datetime.now(tz=timezone.utc)
216215

217216
obj_classes: list[
218217
type[MonthEvents]
@@ -474,12 +473,14 @@ class YearEvents(GenericPeriodEvents):
474473
"""
475474

476475
@classmethod
477-
def from_date(cls, event_name, dt=None, system="default"):
478-
dt = dt or datetime.utcnow()
476+
def from_date(
477+
cls, event_name, dt: Optional[date | datetime] = None, system="default"
478+
):
479+
dt = dt or datetime.now(tz=timezone.utc)
479480
return cls(event_name, dt.year, system=system)
480481

481482
def __init__(self, event_name, year=None, system="default"):
482-
now = datetime.utcnow()
483+
now = datetime.now(tz=timezone.utc)
483484
self.event_name = event_name
484485
self.year = not_none(year, now.year)
485486
self.system = system
@@ -492,10 +493,10 @@ def delta(self, value):
492493
return self.__class__(self.event_name, self.year + value, self.system)
493494

494495
def period_start(self):
495-
return datetime(self.year, 1, 1)
496+
return datetime(self.year, 1, 1, tzinfo=timezone.utc)
496497

497498
def period_end(self):
498-
return datetime(self.year, 12, 31, 23, 59, 59, 999999)
499+
return datetime(self.year, 12, 31, 23, 59, 59, 999999, tzinfo=timezone.utc)
499500

500501

501502
class MonthEvents(GenericPeriodEvents):
@@ -508,12 +509,14 @@ class MonthEvents(GenericPeriodEvents):
508509
"""
509510

510511
@classmethod
511-
def from_date(cls, event_name, dt=None, system="default"):
512-
dt = dt or datetime.utcnow()
512+
def from_date(
513+
cls, event_name, dt: Optional[date | datetime] = None, system="default"
514+
):
515+
dt = dt or datetime.now(tz=timezone.utc)
513516
return cls(event_name, dt.year, dt.month, system=system)
514517

515518
def __init__(self, event_name, year=None, month=None, system="default"):
516-
now = datetime.utcnow()
519+
now = datetime.now(tz=timezone.utc)
517520
self.event_name = event_name
518521
self.year = not_none(year, now.year)
519522
self.month = not_none(month, now.month)
@@ -525,11 +528,13 @@ def delta(self, value):
525528
return self.__class__(self.event_name, year, month, self.system)
526529

527530
def period_start(self):
528-
return datetime(self.year, self.month, 1)
531+
return datetime(self.year, self.month, 1, tzinfo=timezone.utc)
529532

530533
def period_end(self):
531534
_, day = calendar.monthrange(self.year, self.month)
532-
return datetime(self.year, self.month, day, 23, 59, 59, 999999)
535+
return datetime(
536+
self.year, self.month, day, 23, 59, 59, 999999, tzinfo=timezone.utc
537+
)
533538

534539

535540
class WeekEvents(GenericPeriodEvents):
@@ -543,14 +548,17 @@ class WeekEvents(GenericPeriodEvents):
543548

544549
@classmethod
545550
def from_date(
546-
cls, event_name: str, dt: Optional[date] = None, system: str = "default"
551+
cls,
552+
event_name: str,
553+
dt: Optional[date | datetime] = None,
554+
system: str = "default",
547555
):
548-
dt = dt or datetime.utcnow()
556+
dt = dt or datetime.now(tz=timezone.utc)
549557
dt_year, dt_week, _ = dt.isocalendar()
550558
return cls(event_name, dt_year, dt_week, system=system)
551559

552560
def __init__(self, event_name: str, year=None, week=None, system="default"):
553-
now = datetime.utcnow()
561+
now = datetime.now(tz=timezone.utc)
554562
now_year, now_week, _ = now.isocalendar()
555563
self.event_name = event_name
556564
self.year = not_none(year, now_year)
@@ -565,11 +573,11 @@ def delta(self, value):
565573

566574
def period_start(self):
567575
s = iso_to_gregorian(self.year, self.week, 1) # mon
568-
return datetime(s.year, s.month, s.day)
576+
return datetime(s.year, s.month, s.day, tzinfo=timezone.utc)
569577

570578
def period_end(self):
571579
e = iso_to_gregorian(self.year, self.week, 7) # mon
572-
return datetime(e.year, e.month, e.day, 23, 59, 59, 999999)
580+
return datetime(e.year, e.month, e.day, 23, 59, 59, 999999, tzinfo=timezone.utc)
573581

574582

575583
class DayEvents(GenericPeriodEvents):
@@ -582,12 +590,14 @@ class DayEvents(GenericPeriodEvents):
582590
"""
583591

584592
@classmethod
585-
def from_date(cls, event_name: str, dt: Optional[date] = None, system="default"):
586-
dt = dt or datetime.utcnow()
593+
def from_date(
594+
cls, event_name: str, dt: Optional[date | datetime] = None, system="default"
595+
):
596+
dt = dt or datetime.now(tz=timezone.utc)
587597
return cls(event_name, dt.year, dt.month, dt.day, system=system)
588598

589599
def __init__(self, event_name, year=None, month=None, day=None, system="default"):
590-
now = datetime.utcnow()
600+
now = datetime.now(tz=timezone.utc)
591601
self.event_name = event_name
592602
self.year = not_none(year, now.year)
593603
self.month = not_none(month, now.month)
@@ -600,10 +610,12 @@ def delta(self, value):
600610
return self.__class__(self.event_name, dt.year, dt.month, dt.day, self.system)
601611

602612
def period_start(self) -> datetime:
603-
return datetime(self.year, self.month, self.day)
613+
return datetime(self.year, self.month, self.day, tzinfo=timezone.utc)
604614

605615
def period_end(self) -> datetime:
606-
return datetime(self.year, self.month, self.day, 23, 59, 59, 999999)
616+
return datetime(
617+
self.year, self.month, self.day, 23, 59, 59, 999999, tzinfo=timezone.utc
618+
)
607619

608620

609621
class HourEvents(GenericPeriodEvents):
@@ -619,7 +631,7 @@ class HourEvents(GenericPeriodEvents):
619631
def from_date(
620632
cls, event_name: str, dt: Optional[datetime] = None, system="default"
621633
):
622-
dt = dt or datetime.utcnow()
634+
dt = dt or datetime.now(tz=timezone.utc)
623635
return cls(event_name, dt.year, dt.month, dt.day, dt.hour, system=system)
624636

625637
def __init__(
@@ -631,7 +643,7 @@ def __init__(
631643
hour: Optional[int] = None,
632644
system: str = "default",
633645
):
634-
now = datetime.utcnow()
646+
now = datetime.now(tz=timezone.utc)
635647
self.event_name = event_name
636648
self.year = not_none(year, now.year)
637649
self.month = not_none(month, now.month)
@@ -643,18 +655,27 @@ def __init__(
643655
)
644656

645657
def delta(self, value):
646-
dt = datetime(self.year, self.month, self.day, self.hour) + timedelta(
647-
hours=value
648-
)
658+
dt = datetime(
659+
self.year, self.month, self.day, self.hour, tzinfo=timezone.utc
660+
) + timedelta(hours=value)
649661
return self.__class__(
650662
self.event_name, dt.year, dt.month, dt.day, dt.hour, self.system
651663
)
652664

653665
def period_start(self) -> datetime:
654-
return datetime(self.year, self.month, self.day, self.hour)
666+
return datetime(self.year, self.month, self.day, self.hour, tzinfo=timezone.utc)
655667

656668
def period_end(self) -> datetime:
657-
return datetime(self.year, self.month, self.day, self.hour, 59, 59, 999999)
669+
return datetime(
670+
self.year,
671+
self.month,
672+
self.day,
673+
self.hour,
674+
59,
675+
59,
676+
999999,
677+
tzinfo=timezone.utc,
678+
)
658679

659680

660681
# --- Bit operations

bitmapist/cohort/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
:license: BSD
6363
"""
6464

65-
from datetime import date, datetime, timedelta
65+
from datetime import date, datetime, timedelta, timezone
6666
from os import path
6767
from typing import Any, Callable, Literal, Optional, Union
6868

@@ -228,10 +228,10 @@ def get_dates_data(
228228
num_of_rows = int(num_of_rows)
229229

230230
if start_date:
231-
now = datetime.strptime(start_date, "%Y-%m-%d")
231+
now = datetime.strptime(start_date, "%Y-%m-%d").replace(tzinfo=timezone.utc)
232232
now = now + timedelta(days=num_results - 1)
233233
else:
234-
now = datetime.utcnow()
234+
now = datetime.now(tz=timezone.utc)
235235

236236
# Days
237237
if time_group == "days":

test/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from bitmapist import delete_all_events, setup_redis
1010

1111

12-
@pytest.yield_fixture(scope="session", autouse=True)
12+
@pytest.fixture(scope="session", autouse=True)
1313
def redis_server():
1414
"""Fixture starting the Redis server"""
1515
redis_host = "127.0.0.1"

test/test_bitmapist.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from datetime import datetime, timedelta
1+
from datetime import datetime, timedelta, timezone
22

33
from bitmapist import (
44
BitOpAnd,
@@ -17,7 +17,7 @@
1717
def test_mark_with_diff_days():
1818
mark_event("active", 123, track_hourly=True)
1919

20-
now = datetime.utcnow()
20+
now = datetime.now(tz=timezone.utc)
2121

2222
# Month
2323
assert 123 in MonthEvents("active", now.year, now.month)
@@ -38,7 +38,7 @@ def test_mark_with_diff_days():
3838

3939

4040
def test_mark_unmark():
41-
now = datetime.utcnow()
41+
now = datetime.now(tz=timezone.utc)
4242

4343
mark_event("active", 125)
4444
assert 125 in MonthEvents("active", now.year, now.month)
@@ -48,7 +48,7 @@ def test_mark_unmark():
4848

4949

5050
def test_mark_counts():
51-
now = datetime.utcnow()
51+
now = datetime.now(tz=timezone.utc)
5252

5353
assert MonthEvents("active", now.year, now.month).get_count() == 0
5454

@@ -59,7 +59,7 @@ def test_mark_counts():
5959

6060

6161
def test_mark_iter():
62-
now = datetime.utcnow()
62+
now = datetime.now(tz=timezone.utc)
6363
ev = MonthEvents("active", now.year, now.month)
6464

6565
assert list(ev) == []
@@ -73,7 +73,7 @@ def test_mark_iter():
7373

7474

7575
def test_different_dates():
76-
now = datetime.utcnow()
76+
now = datetime.now(tz=timezone.utc)
7777
yesterday = now - timedelta(days=1)
7878

7979
mark_event("active", 123, now=now)
@@ -88,7 +88,7 @@ def test_different_dates():
8888

8989

9090
def test_different_buckets():
91-
now = datetime.utcnow()
91+
now = datetime.now(tz=timezone.utc)
9292

9393
mark_event("active", 123)
9494
mark_event("tasks:completed", 23232)
@@ -98,8 +98,8 @@ def test_different_buckets():
9898

9999

100100
def test_bit_operations():
101-
now = datetime.utcnow()
102-
last_month = datetime.utcnow() - timedelta(days=30)
101+
now = datetime.now(tz=timezone.utc)
102+
last_month = now - timedelta(days=30)
103103

104104
# 123 has been active for two months
105105
mark_event("active", 123, now=now)
@@ -156,7 +156,7 @@ def test_bit_operations():
156156

157157

158158
def test_bit_operations_complex():
159-
now = datetime.utcnow()
159+
now = datetime.now(tz=timezone.utc)
160160
tom = now + timedelta(days=1)
161161

162162
mark_event("task1", 111, now=now)
@@ -185,7 +185,7 @@ def test_bit_operations_complex():
185185

186186

187187
def test_bitop_key_sharing():
188-
today = datetime.utcnow()
188+
today = datetime.now(tz=timezone.utc)
189189

190190
mark_event("task1", 111, now=today)
191191
mark_event("task2", 111, now=today)
@@ -207,7 +207,7 @@ def test_bitop_key_sharing():
207207

208208

209209
def test_events_marked():
210-
now = datetime.utcnow()
210+
now = datetime.now(tz=timezone.utc)
211211

212212
assert MonthEvents("active", now.year, now.month).get_count() == 0
213213
assert MonthEvents("active", now.year, now.month).has_events_marked() is False

test/test_cohort.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from datetime import datetime, timedelta
1+
from datetime import datetime, timedelta, timezone
22

33
import pytest
44

@@ -8,7 +8,7 @@
88

99
@pytest.fixture
1010
def events():
11-
today = datetime.utcnow()
11+
today = datetime.now(tz=timezone.utc)
1212
tomorrow = today + timedelta(days=1)
1313

1414
mark_event("signup", 111, now=today)

0 commit comments

Comments
 (0)