55API-specific formatting, and caching integration.
66"""
77
8- from datetime import datetime , timedelta , timezone
8+ from datetime import UTC , datetime , timedelta
99from unittest .mock import patch
1010
1111import pytest
@@ -24,45 +24,43 @@ class TestEnsureUTCDateTime:
2424 def test_timezone_naive_string_assumes_utc (self ):
2525 """Test that timezone-naive strings are assumed to be UTC."""
2626 result = ensure_utc_datetime ("2025-01-15" )
27- expected = datetime (2025 , 1 , 15 , tzinfo = timezone . utc )
27+ expected = datetime (2025 , 1 , 15 , tzinfo = UTC )
2828 assert result == expected
29- assert result .tzinfo == timezone . utc
29+ assert result .tzinfo == UTC
3030
3131 def test_timezone_aware_string_converts_to_utc (self ):
3232 """Test that timezone-aware strings are converted to UTC."""
3333 # Test with EST (UTC-5)
3434 result = ensure_utc_datetime ("2025-01-15T10:00:00-05:00" )
35- expected = datetime (
36- 2025 , 1 , 15 , 15 , 0 , 0 , tzinfo = timezone .utc
37- ) # 10 AM EST = 3 PM UTC
35+ expected = datetime (2025 , 1 , 15 , 15 , 0 , 0 , tzinfo = UTC ) # 10 AM EST = 3 PM UTC
3836 assert result == expected
39- assert result .tzinfo == timezone . utc
37+ assert result .tzinfo == UTC
4038
4139 def test_iso_formats (self ):
4240 """Test various ISO format inputs."""
4341 test_cases = [
44- ("2025-01-15" , datetime (2025 , 1 , 15 , tzinfo = timezone . utc )),
45- ("2025-01-15T00:00:00" , datetime (2025 , 1 , 15 , tzinfo = timezone . utc )),
42+ ("2025-01-15" , datetime (2025 , 1 , 15 , tzinfo = UTC )),
43+ ("2025-01-15T00:00:00" , datetime (2025 , 1 , 15 , tzinfo = UTC )),
4644 (
4745 "2025-01-15T12:30:45" ,
48- datetime (2025 , 1 , 15 , 12 , 30 , 45 , tzinfo = timezone . utc ),
46+ datetime (2025 , 1 , 15 , 12 , 30 , 45 , tzinfo = UTC ),
4947 ),
5048 # Z suffix indicates UTC timezone (Zulu time)
5149 (
5250 "2025-01-15T12:30:45Z" ,
53- datetime (2025 , 1 , 15 , 12 , 30 , 45 , tzinfo = timezone . utc ),
51+ datetime (2025 , 1 , 15 , 12 , 30 , 45 , tzinfo = UTC ),
5452 ),
5553 ]
5654
5755 for input_str , expected in test_cases :
5856 result = ensure_utc_datetime (input_str )
5957 assert result == expected
60- assert result .tzinfo == timezone . utc
58+ assert result .tzinfo == UTC
6159
6260 def test_utc_string_unchanged (self ):
6361 """Test that UTC strings remain unchanged."""
6462 result = ensure_utc_datetime ("2025-01-15T12:00:00+00:00" )
65- expected = datetime (2025 , 1 , 15 , 12 , 0 , 0 , tzinfo = timezone . utc )
63+ expected = datetime (2025 , 1 , 15 , 12 , 0 , 0 , tzinfo = UTC )
6664 assert result == expected
6765
6866 def test_different_timezones (self ):
@@ -71,17 +69,17 @@ def test_different_timezones(self):
7169 # PST (UTC-8) to UTC
7270 (
7371 "2025-01-15T08:00:00-08:00" ,
74- datetime (2025 , 1 , 15 , 16 , 0 , 0 , tzinfo = timezone . utc ),
72+ datetime (2025 , 1 , 15 , 16 , 0 , 0 , tzinfo = UTC ),
7573 ),
7674 # JST (UTC+9) to UTC
7775 (
7876 "2025-01-15T18:00:00+09:00" ,
79- datetime (2025 , 1 , 15 , 9 , 0 , 0 , tzinfo = timezone . utc ),
77+ datetime (2025 , 1 , 15 , 9 , 0 , 0 , tzinfo = UTC ),
8078 ),
8179 # CET (UTC+1) to UTC
8280 (
8381 "2025-01-15T13:00:00+01:00" ,
84- datetime (2025 , 1 , 15 , 12 , 0 , 0 , tzinfo = timezone . utc ),
82+ datetime (2025 , 1 , 15 , 12 , 0 , 0 , tzinfo = UTC ),
8583 ),
8684 ]
8785
@@ -95,30 +93,30 @@ class TestDateRange:
9593
9694 def test_daterange_creation (self ):
9795 """Test basic DateRange object creation."""
98- start = datetime (2025 , 1 , 1 , tzinfo = timezone . utc )
99- end = datetime (2025 , 1 , 31 , tzinfo = timezone . utc )
96+ start = datetime (2025 , 1 , 1 , tzinfo = UTC )
97+ end = datetime (2025 , 1 , 31 , tzinfo = UTC )
10098 dr = DateRange (start_date = start , end_date = end )
10199
102100 assert dr .start_date == start
103101 assert dr .end_date == end
104102
105103 def test_daterange_immutability (self ):
106104 """Test that DateRange is immutable (frozen dataclass)."""
107- start = datetime (2025 , 1 , 1 , tzinfo = timezone . utc )
108- end = datetime (2025 , 1 , 31 , tzinfo = timezone . utc )
105+ start = datetime (2025 , 1 , 1 , tzinfo = UTC )
106+ end = datetime (2025 , 1 , 31 , tzinfo = UTC )
109107 dr = DateRange (start_date = start , end_date = end )
110108
111109 # Attempting to modify should raise AttributeError
112110 with pytest .raises (AttributeError ):
113- dr .start_date = datetime (2025 , 2 , 1 , tzinfo = timezone . utc )
111+ dr .start_date = datetime (2025 , 2 , 1 , tzinfo = UTC )
114112
115113 with pytest .raises (AttributeError ):
116- dr .end_date = datetime (2025 , 2 , 28 , tzinfo = timezone . utc )
114+ dr .end_date = datetime (2025 , 2 , 28 , tzinfo = UTC )
117115
118116 def test_daterange_hashability (self ):
119117 """Test that DateRange objects are hashable and can be used as dict keys."""
120- start = datetime (2025 , 1 , 1 , tzinfo = timezone . utc )
121- end = datetime (2025 , 1 , 31 , tzinfo = timezone . utc )
118+ start = datetime (2025 , 1 , 1 , tzinfo = UTC )
119+ end = datetime (2025 , 1 , 31 , tzinfo = UTC )
122120
123121 dr1 = DateRange (start_date = start , end_date = end )
124122 dr2 = DateRange (start_date = start , end_date = end )
@@ -132,22 +130,20 @@ def test_daterange_hashability(self):
132130
133131 def test_daterange_equality (self ):
134132 """Test DateRange equality comparison."""
135- start = datetime (2025 , 1 , 1 , tzinfo = timezone . utc )
136- end = datetime (2025 , 1 , 31 , tzinfo = timezone . utc )
133+ start = datetime (2025 , 1 , 1 , tzinfo = UTC )
134+ end = datetime (2025 , 1 , 31 , tzinfo = UTC )
137135
138136 dr1 = DateRange (start_date = start , end_date = end )
139137 dr2 = DateRange (start_date = start , end_date = end )
140- dr3 = DateRange (
141- start_date = start , end_date = datetime (2025 , 2 , 1 , tzinfo = timezone .utc )
142- )
138+ dr3 = DateRange (start_date = start , end_date = datetime (2025 , 2 , 1 , tzinfo = UTC ))
143139
144140 assert dr1 == dr2
145141 assert dr1 != dr3
146142
147143 def test_aws_range_formatting (self ):
148144 """Test AWS date range formatting (exclusive end date, YYYY-MM-DD format)."""
149- start = datetime (2025 , 1 , 15 , 12 , 30 , 45 , tzinfo = timezone . utc )
150- end = datetime (2025 , 1 , 31 , 8 , 15 , 30 , tzinfo = timezone . utc )
145+ start = datetime (2025 , 1 , 15 , 12 , 30 , 45 , tzinfo = UTC )
146+ end = datetime (2025 , 1 , 31 , 8 , 15 , 30 , tzinfo = UTC )
151147 dr = DateRange (start_date = start , end_date = end )
152148
153149 aws_from , aws_to = dr .aws_range
@@ -159,8 +155,8 @@ def test_aws_range_formatting(self):
159155
160156 def test_prometheus_range_formatting (self ):
161157 """Test Prometheus date range formatting (inclusive dates, ISO format)."""
162- start = datetime (2025 , 1 , 15 , 12 , 30 , 45 , tzinfo = timezone . utc )
163- end = datetime (2025 , 1 , 31 , 8 , 15 , 30 , tzinfo = timezone . utc )
158+ start = datetime (2025 , 1 , 15 , 12 , 30 , 45 , tzinfo = UTC )
159+ end = datetime (2025 , 1 , 31 , 8 , 15 , 30 , tzinfo = UTC )
164160 dr = DateRange (start_date = start , end_date = end )
165161
166162 prom_from , prom_to = dr .prometheus_range
@@ -172,8 +168,8 @@ def test_prometheus_range_formatting(self):
172168 def test_same_logical_range_different_formats (self ):
173169 """Test that AWS and Prometheus formats represent the same logical date range."""
174170 # Create range for January 1-31, 2025
175- start = datetime (2025 , 1 , 1 , tzinfo = timezone . utc )
176- end = datetime (2025 , 1 , 31 , 23 , 59 , 59 , tzinfo = timezone . utc )
171+ start = datetime (2025 , 1 , 1 , tzinfo = UTC )
172+ end = datetime (2025 , 1 , 31 , 23 , 59 , 59 , tzinfo = UTC )
177173 dr = DateRange (start_date = start , end_date = end )
178174
179175 aws_from , aws_to = dr .aws_range
@@ -195,7 +191,7 @@ def test_default_date_range(self):
195191 """Test default 30-day range behavior."""
196192 with patch ("src.jupyterhub_cost_monitoring.date_utils.datetime" ) as mock_dt :
197193 # Mock current time as 2025-02-15 midnight UTC
198- mock_now = datetime (2025 , 2 , 15 , tzinfo = timezone . utc )
194+ mock_now = datetime (2025 , 2 , 15 , tzinfo = UTC )
199195 mock_dt .now .return_value = mock_now
200196 mock_dt .side_effect = lambda * args , ** kwargs : datetime (* args , ** kwargs )
201197
@@ -218,10 +214,10 @@ def test_timezone_handling_in_parsing(self):
218214
219215 # Should be converted to UTC
220216 expected_start = datetime (
221- 2025 , 1 , 1 , 15 , 0 , 0 , tzinfo = timezone . utc
217+ 2025 , 1 , 1 , 15 , 0 , 0 , tzinfo = UTC
222218 ) # 10 AM EST = 3 PM UTC
223219 expected_end = datetime (
224- 2025 , 1 , 31 , 13 , 0 , 0 , tzinfo = timezone . utc
220+ 2025 , 1 , 31 , 13 , 0 , 0 , tzinfo = UTC
225221 ) # 3 PM CET = 1 PM UTC
226222
227223 assert result .start_date == expected_start
@@ -242,8 +238,8 @@ def cached_function(date_range: DateRange) -> str:
242238 call_count += 1
243239 return f"Result for { date_range .start_date .date ()} "
244240
245- start = datetime (2025 , 1 , 1 , tzinfo = timezone . utc )
246- end = datetime (2025 , 1 , 31 , tzinfo = timezone . utc )
241+ start = datetime (2025 , 1 , 1 , tzinfo = UTC )
242+ end = datetime (2025 , 1 , 31 , tzinfo = UTC )
247243
248244 dr1 = DateRange (start_date = start , end_date = end )
249245 dr2 = DateRange (start_date = start , end_date = end ) # Identical range
@@ -267,12 +263,12 @@ def cached_function(date_range: DateRange) -> str:
267263 call_count += 1
268264 return f"Call #{ call_count } for { date_range .start_date .date ()} "
269265
270- start1 = datetime (2025 , 1 , 1 , tzinfo = timezone . utc )
271- end1 = datetime (2025 , 1 , 31 , tzinfo = timezone . utc )
266+ start1 = datetime (2025 , 1 , 1 , tzinfo = UTC )
267+ end1 = datetime (2025 , 1 , 31 , tzinfo = UTC )
272268 january_range = DateRange (start_date = start1 , end_date = end1 )
273269
274- start2 = datetime (2025 , 2 , 1 , tzinfo = timezone . utc )
275- end2 = datetime (2025 , 2 , 28 , tzinfo = timezone . utc )
270+ start2 = datetime (2025 , 2 , 1 , tzinfo = UTC )
271+ end2 = datetime (2025 , 2 , 28 , tzinfo = UTC )
276272 february_range = DateRange (start_date = start2 , end_date = end2 )
277273
278274 result1 = cached_function (january_range )
@@ -293,12 +289,12 @@ def cached_function(date_range: DateRange) -> str:
293289 return f"Call #{ call_count } for { date_range .start_date .date ()} "
294290
295291 # Same dates but different times
296- start1 = datetime (2025 , 1 , 1 , 8 , 30 , 15 , tzinfo = timezone . utc )
297- end1 = datetime (2025 , 1 , 31 , 14 , 45 , 22 , tzinfo = timezone . utc )
292+ start1 = datetime (2025 , 1 , 1 , 8 , 30 , 15 , tzinfo = UTC )
293+ end1 = datetime (2025 , 1 , 31 , 14 , 45 , 22 , tzinfo = UTC )
298294 morning_range = DateRange (start_date = start1 , end_date = end1 )
299295
300- start2 = datetime (2025 , 1 , 1 , 16 , 20 , 55 , tzinfo = timezone . utc )
301- end2 = datetime (2025 , 1 , 31 , 9 , 12 , 8 , tzinfo = timezone . utc )
296+ start2 = datetime (2025 , 1 , 1 , 16 , 20 , 55 , tzinfo = UTC )
297+ end2 = datetime (2025 , 1 , 31 , 9 , 12 , 8 , tzinfo = UTC )
302298 evening_range = DateRange (start_date = start2 , end_date = end2 )
303299
304300 result1 = cached_function (morning_range )
@@ -311,8 +307,8 @@ def cached_function(date_range: DateRange) -> str:
311307 def test_daterange_normalization (self ):
312308 """Test that DateRange normalizes times for consistent caching."""
313309 # Test start date normalization to 00:00:00
314- start_with_time = datetime (2025 , 1 , 1 , 15 , 30 , 45 , tzinfo = timezone . utc )
315- end_with_time = datetime (2025 , 1 , 31 , 8 , 15 , 30 , tzinfo = timezone . utc )
310+ start_with_time = datetime (2025 , 1 , 1 , 15 , 30 , 45 , tzinfo = UTC )
311+ end_with_time = datetime (2025 , 1 , 31 , 8 , 15 , 30 , tzinfo = UTC )
316312 time_specific_range = DateRange (
317313 start_date = start_with_time , end_date = end_with_time
318314 )
@@ -336,12 +332,12 @@ def test_daterange_normalization(self):
336332 def test_daterange_hash_consistency_across_times (self ):
337333 """Test that DateRange objects with same dates but different times have same hash."""
338334 # Same dates, different times
339- start1 = datetime (2025 , 1 , 1 , 2 , 30 , tzinfo = timezone . utc )
340- end1 = datetime (2025 , 1 , 31 , 18 , 45 , tzinfo = timezone . utc )
335+ start1 = datetime (2025 , 1 , 1 , 2 , 30 , tzinfo = UTC )
336+ end1 = datetime (2025 , 1 , 31 , 18 , 45 , tzinfo = UTC )
341337 early_range = DateRange (start_date = start1 , end_date = end1 )
342338
343- start2 = datetime (2025 , 1 , 1 , 22 , 15 , tzinfo = timezone . utc )
344- end2 = datetime (2025 , 1 , 31 , 6 , 20 , tzinfo = timezone . utc )
339+ start2 = datetime (2025 , 1 , 1 , 22 , 15 , tzinfo = UTC )
340+ end2 = datetime (2025 , 1 , 31 , 6 , 20 , tzinfo = UTC )
345341 late_range = DateRange (start_date = start2 , end_date = end2 )
346342
347343 # Should have same hash despite different input times
0 commit comments