Skip to content

Commit f144bf0

Browse files
authored
Merge pull request #134 from Point72/tkp/ctx
Add datetime contexts, add datetime validator convenience, update wiki to show other context examples
2 parents 5d07635 + daa5f56 commit f144bf0

File tree

4 files changed

+183
-11
lines changed

4 files changed

+183
-11
lines changed

ccflow/context.py

Lines changed: 107 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,41 +7,60 @@
77

88
from .base import ContextBase
99
from .exttypes import Frequency
10-
from .validators import normalize_date
10+
from .validators import normalize_date, normalize_datetime
1111

12-
__all__ = [
12+
__all__ = (
1313
"NullContext",
1414
"GenericContext",
1515
"DateContext",
1616
"DatetimeContext",
1717
"EntryTimeContext",
18+
"SourceContext",
1819
"DateRangeContext",
20+
"DatetimeRangeContext",
21+
"SeededDateRangeContext",
22+
"SeededDatetimeRangeContext",
1923
"VersionedDateContext",
24+
"VersionedDatetimeContext",
2025
"VersionedDateRangeContext",
26+
"VersionedDatetimeRangeContext",
2127
"FreqContext",
2228
"FreqDateContext",
29+
"FreqDatetimeContext",
2330
"FreqDateRangeContext",
31+
"FreqDatetimeRangeContext",
2432
"HorizonContext",
2533
"FreqHorizonContext",
2634
"FreqHorizonDateContext",
35+
"FreqHorizonDatetimeContext",
2736
"FreqHorizonDateRangeContext",
28-
"SeededDateRangeContext",
29-
"SourceContext",
37+
"FreqHorizonDatetimeRangeContext",
3038
"UniverseContext",
3139
"UniverseDateContext",
40+
"UniverseDatetimeContext",
3241
"UniverseDateRangeContext",
42+
"UniverseDatetimeRangeContext",
3343
"UniverseFrequencyDateRangeContext",
44+
"UniverseFrequencyDatetimeRangeContext",
3445
"UniverseFrequencyHorizonDateRangeContext",
46+
"UniverseFrequencyHorizonDatetimeRangeContext",
3547
"VersionedUniverseDateContext",
48+
"VersionedUniverseDatetimeContext",
3649
"VersionedUniverseDateRangeContext",
50+
"VersionedUniverseDatetimeRangeContext",
3751
"ModelContext",
3852
"ModelDateContext",
53+
"ModelDatetimeContext",
3954
"ModelDateRangeContext",
55+
"ModelDatetimeRangeContext",
4056
"ModelDateRangeSourceContext",
4157
"ModelFreqDateRangeContext",
58+
"ModelFreqDatetimeRangeContext",
4259
"VersionedModelDateContext",
60+
"VersionedModelDatetimeContext",
4361
"VersionedModelDateRangeContext",
44-
]
62+
"VersionedModelDatetimeRangeContext",
63+
)
4564

4665
_SEPARATOR = ","
4766

@@ -100,6 +119,9 @@ def _date_context_validator(cls, v, handler, info):
100119
class DatetimeContext(ContextBase):
101120
dt: datetime
102121

122+
# validators
123+
_normalize_dt = field_validator("dt", mode="before")(normalize_datetime)
124+
103125
@model_validator(mode="wrap")
104126
def _datetime_context_validator(cls, v, handler, info):
105127
if cls is DatetimeContext and not isinstance(v, (DatetimeContext, dict)):
@@ -126,18 +148,38 @@ class DateRangeContext(ContextBase):
126148
_normalize_end = field_validator("end_date", mode="before")(normalize_date)
127149

128150

151+
class DatetimeRangeContext(ContextBase):
152+
start_datetime: datetime
153+
end_datetime: datetime
154+
155+
_normalize_start = field_validator("start_datetime", mode="before")(normalize_datetime)
156+
_normalize_end = field_validator("end_datetime", mode="before")(normalize_datetime)
157+
158+
129159
class SeededDateRangeContext(DateRangeContext):
130160
seed: int = 1234
131161

132162

163+
class SeededDatetimeRangeContext(DatetimeRangeContext):
164+
seed: int = 1234
165+
166+
133167
class VersionedDateContext(DateContext, EntryTimeContext):
134168
pass
135169

136170

171+
class VersionedDatetimeContext(DatetimeContext, EntryTimeContext):
172+
pass
173+
174+
137175
class VersionedDateRangeContext(DateRangeContext, EntryTimeContext):
138176
pass
139177

140178

179+
class VersionedDatetimeRangeContext(DatetimeRangeContext, EntryTimeContext):
180+
pass
181+
182+
141183
class FreqContext(ContextBase):
142184
freq: Frequency
143185

@@ -146,10 +188,18 @@ class FreqDateContext(DateContext, FreqContext):
146188
pass
147189

148190

191+
class FreqDatetimeContext(DatetimeContext, FreqContext):
192+
pass
193+
194+
149195
class FreqDateRangeContext(DateRangeContext, FreqContext):
150196
pass
151197

152198

199+
class FreqDatetimeRangeContext(DatetimeRangeContext, FreqContext):
200+
pass
201+
202+
153203
class HorizonContext(ContextBase):
154204
horizon: Frequency
155205

@@ -162,10 +212,18 @@ class FreqHorizonDateContext(DateContext, HorizonContext, FreqContext):
162212
pass
163213

164214

215+
class FreqHorizonDatetimeContext(DatetimeContext, HorizonContext, FreqContext):
216+
pass
217+
218+
165219
class FreqHorizonDateRangeContext(DateRangeContext, HorizonContext, FreqContext):
166220
pass
167221

168222

223+
class FreqHorizonDatetimeRangeContext(DatetimeRangeContext, HorizonContext, FreqContext):
224+
pass
225+
226+
169227
class UniverseContext(ContextBase):
170228
universe: str
171229

@@ -174,26 +232,50 @@ class UniverseDateContext(DateContext, UniverseContext):
174232
pass
175233

176234

235+
class UniverseDatetimeContext(DatetimeContext, UniverseContext):
236+
pass
237+
238+
177239
class UniverseDateRangeContext(DateRangeContext, UniverseContext):
178240
pass
179241

180242

243+
class UniverseDatetimeRangeContext(DatetimeRangeContext, UniverseContext):
244+
pass
245+
246+
181247
class UniverseFrequencyDateRangeContext(DateRangeContext, FreqContext, UniverseContext):
182248
pass
183249

184250

251+
class UniverseFrequencyDatetimeRangeContext(DatetimeRangeContext, FreqContext, UniverseContext):
252+
pass
253+
254+
185255
class UniverseFrequencyHorizonDateRangeContext(DateRangeContext, HorizonContext, FreqContext, UniverseContext):
186256
pass
187257

188258

259+
class UniverseFrequencyHorizonDatetimeRangeContext(DatetimeRangeContext, HorizonContext, FreqContext, UniverseContext):
260+
pass
261+
262+
189263
class VersionedUniverseDateContext(VersionedDateContext, UniverseContext):
190264
pass
191265

192266

267+
class VersionedUniverseDatetimeContext(VersionedDatetimeContext, UniverseContext):
268+
pass
269+
270+
193271
class VersionedUniverseDateRangeContext(VersionedDateRangeContext, UniverseContext):
194272
pass
195273

196274

275+
class VersionedUniverseDatetimeRangeContext(VersionedDatetimeRangeContext, UniverseContext):
276+
pass
277+
278+
197279
class ModelContext(ContextBase):
198280
model: str
199281

@@ -202,10 +284,18 @@ class ModelDateContext(DateContext, ModelContext):
202284
pass
203285

204286

287+
class ModelDatetimeContext(DatetimeContext, ModelContext):
288+
pass
289+
290+
205291
class ModelDateRangeContext(DateRangeContext, ModelContext):
206292
pass
207293

208294

295+
class ModelDatetimeRangeContext(DatetimeRangeContext, ModelContext):
296+
pass
297+
298+
209299
class ModelDateRangeSourceContext(SourceContext, ModelDateRangeContext):
210300
pass
211301

@@ -214,9 +304,21 @@ class ModelFreqDateRangeContext(FreqDateRangeContext, ModelContext):
214304
pass
215305

216306

307+
class ModelFreqDatetimeRangeContext(FreqDatetimeRangeContext, ModelContext):
308+
pass
309+
310+
217311
class VersionedModelDateContext(VersionedDateContext, ModelContext):
218312
pass
219313

220314

315+
class VersionedModelDatetimeContext(VersionedDatetimeContext, ModelContext):
316+
pass
317+
318+
221319
class VersionedModelDateRangeContext(VersionedDateRangeContext, ModelContext):
222320
pass
321+
322+
323+
class VersionedModelDatetimeRangeContext(VersionedDatetimeRangeContext, ModelContext):
324+
pass

ccflow/tests/test_validators.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import logging
22
from datetime import date, datetime, timedelta
33
from unittest import TestCase
4+
from zoneinfo import ZoneInfo
45

5-
from ccflow.validators import eval_or_load_object, load_object, normalize_date, str_to_log_level
6+
from ccflow.validators import eval_or_load_object, load_object, normalize_date, normalize_datetime, str_to_log_level
67

78

89
class A:
@@ -23,6 +24,34 @@ def test_normalize_date(self):
2324
self.assertEqual(normalize_date("foo"), "foo")
2425
self.assertEqual(normalize_date(None), None)
2526

27+
def test_normalize_datetime(self):
28+
today = datetime.today()
29+
now = datetime.now()
30+
c = datetime(today.year, today.month, today.day)
31+
32+
self.assertEqual(normalize_datetime(c), c)
33+
self.assertEqual(normalize_datetime("0d"), c)
34+
35+
c1 = c - timedelta(1)
36+
self.assertEqual(normalize_datetime("-1d"), c1)
37+
38+
self.assertEqual(normalize_datetime(now), now)
39+
40+
# check passthrough validation error
41+
self.assertEqual(normalize_datetime("foo"), "foo")
42+
self.assertEqual(normalize_datetime(None), None)
43+
44+
# check dict
45+
self.assertEqual(
46+
normalize_datetime({"dt": now.isoformat(), "tz": "US/Hawaii"}),
47+
now.astimezone(tz=ZoneInfo("US/Hawaii")),
48+
)
49+
# check list
50+
self.assertEqual(
51+
normalize_datetime([now.isoformat(), "US/Hawaii"]),
52+
now.astimezone(tz=ZoneInfo("US/Hawaii")),
53+
)
54+
2655
def test_load_object(self):
2756
self.assertEqual(load_object("ccflow.tests.test_validators.A"), A)
2857
self.assertIsNone(load_object(None))

ccflow/validators.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import logging
44
from datetime import date, datetime
55
from typing import Any, Dict, Optional
6+
from zoneinfo import ZoneInfo
67

78
import pandas as pd
89
from pydantic import TypeAdapter, ValidationError
@@ -11,6 +12,14 @@
1112

1213
_DatetimeAdapter = TypeAdapter(datetime)
1314

15+
__all__ = (
16+
"normalize_date",
17+
"normalize_datetime",
18+
"load_object",
19+
"eval_or_load_object",
20+
"str_to_log_level",
21+
)
22+
1423

1524
def normalize_date(v: Any) -> Any:
1625
"""Validator that will convert string offsets to date based on today, and convert datetime to date."""
@@ -31,6 +40,34 @@ def normalize_date(v: Any) -> Any:
3140
return v
3241

3342

43+
def normalize_datetime(v: Any) -> Any:
44+
"""Validator that will convert string offsets to datetime based on today, and convert datetime to date."""
45+
if isinstance(v, str): # Check case where it's an offset
46+
try:
47+
return (pd.tseries.frequencies.to_offset(v) + date.today()).to_pydatetime()
48+
except ValueError:
49+
pass
50+
if isinstance(v, dict):
51+
# e.g. DatetimeContext object, {"dt": datetime(...)}
52+
dt = list(v.values())[0]
53+
tz = list(v.values())[1] if len(v) > 1 else None
54+
elif isinstance(v, list):
55+
dt = v[0]
56+
tz = v[1] if len(v) > 1 else None
57+
else:
58+
dt = v
59+
tz = None
60+
try:
61+
dt = TypeAdapter(datetime).validate_python(dt)
62+
if tz and isinstance(tz, str):
63+
tz = ZoneInfo(tz)
64+
if tz:
65+
dt = dt.astimezone(tz)
66+
return dt
67+
except ValidationError:
68+
return v
69+
70+
3471
def load_object(v: Any) -> Any:
3572
"""Validator that loads an object from path if a string is provided"""
3673
if isinstance(v, str):

0 commit comments

Comments
 (0)