Skip to content

Commit c9887d8

Browse files
authored
Add support for pendulum Duration (#162)
1 parent c046fb9 commit c9887d8

File tree

2 files changed

+111
-2
lines changed

2 files changed

+111
-2
lines changed

pydantic_extra_types/pendulum_dt.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
try:
77
from pendulum import Date as _Date
88
from pendulum import DateTime as _DateTime
9+
from pendulum import Duration as _Duration
910
from pendulum import parse
1011
except ModuleNotFoundError: # pragma: no cover
1112
raise RuntimeError(
@@ -131,3 +132,61 @@ def _validate(cls, value: Any, handler: core_schema.ValidatorFunctionWrapHandler
131132
except Exception as exc:
132133
raise PydanticCustomError('value_error', 'value is not a valid date') from exc
133134
return handler(data)
135+
136+
137+
class Duration(_Duration):
138+
"""
139+
A `pendulum.Duration` object. At runtime, this type decomposes into pendulum.Duration automatically.
140+
This type exists because Pydantic throws a fit on unknown types.
141+
142+
```python
143+
from pydantic import BaseModel
144+
from pydantic_extra_types.pendulum_dt import Duration
145+
146+
class test_model(BaseModel):
147+
delta_t: Duration
148+
149+
print(test_model(delta_t='P1DT25H'))
150+
151+
#> test_model(delta_t=Duration(days=2, hours=1))
152+
```
153+
"""
154+
155+
__slots__: List[str] = []
156+
157+
@classmethod
158+
def __get_pydantic_core_schema__(cls, source: Type[Any], handler: GetCoreSchemaHandler) -> core_schema.CoreSchema:
159+
"""
160+
Return a Pydantic CoreSchema with the Duration validation
161+
162+
Args:
163+
source: The source type to be converted.
164+
handler: The handler to get the CoreSchema.
165+
166+
Returns:
167+
A Pydantic CoreSchema with the Duration validation.
168+
"""
169+
return core_schema.no_info_wrap_validator_function(cls._validate, core_schema.timedelta_schema())
170+
171+
@classmethod
172+
def _validate(cls, value: Any, handler: core_schema.ValidatorFunctionWrapHandler) -> Any:
173+
"""
174+
Validate the Duration object and return it.
175+
176+
Args:
177+
value: The value to validate.
178+
handler: The handler to get the CoreSchema.
179+
180+
Returns:
181+
The validated value or raises a PydanticCustomError.
182+
"""
183+
# if we are passed an existing instance, pass it straight through.
184+
if isinstance(value, _Duration):
185+
return handler(value)
186+
187+
# otherwise, parse it.
188+
try:
189+
data = parse(value)
190+
except Exception as exc:
191+
raise PydanticCustomError('value_error', 'value is not a valid duration') from exc
192+
return handler(data)

tests/test_pendulum_dt.py

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import pytest
33
from pydantic import BaseModel, ValidationError
44

5-
from pydantic_extra_types.pendulum_dt import Date, DateTime
5+
from pydantic_extra_types.pendulum_dt import Date, DateTime, Duration
66

77

88
class DtModel(BaseModel):
@@ -13,6 +13,10 @@ class DateModel(BaseModel):
1313
d: Date
1414

1515

16+
class DurationModel(BaseModel):
17+
delta_t: Duration
18+
19+
1620
def test_pendulum_dt_existing_instance():
1721
"""
1822
Verifies that constructing a model with an existing pendulum dt doesn't throw.
@@ -31,8 +35,23 @@ def test_pendulum_date_existing_instance():
3135
assert model.d == today
3236

3337

38+
def test_pendulum_duration_existing_instance():
39+
"""
40+
Verifies that constructing a model with an existing pendulum duration doesn't throw.
41+
"""
42+
delta_t = pendulum.duration(days=42, hours=13, minutes=37)
43+
model = DurationModel(delta_t=delta_t)
44+
45+
assert model.delta_t.total_seconds() == delta_t.total_seconds()
46+
47+
3448
@pytest.mark.parametrize(
35-
'dt', [pendulum.now().to_iso8601_string(), pendulum.now().to_w3c_string(), pendulum.now().to_iso8601_string()]
49+
'dt',
50+
[
51+
pendulum.now().to_iso8601_string(),
52+
pendulum.now().to_w3c_string(),
53+
pendulum.now().to_iso8601_string(),
54+
],
3655
)
3756
def test_pendulum_dt_from_serialized(dt):
3857
"""
@@ -52,6 +71,25 @@ def test_pendulum_date_from_serialized():
5271
assert model.d == date_actual
5372

5473

74+
@pytest.mark.parametrize(
75+
'delta_t_str',
76+
[
77+
'P3.14D',
78+
'PT404H',
79+
'P1DT25H',
80+
'P2W',
81+
'P10Y10M10D',
82+
],
83+
)
84+
def test_pendulum_duration_from_serialized(delta_t_str):
85+
"""
86+
Verifies that building an instance from serialized, well-formed strings decode properly.
87+
"""
88+
true_delta_t = pendulum.parse(delta_t_str)
89+
model = DurationModel(delta_t=delta_t_str)
90+
assert model.delta_t == true_delta_t
91+
92+
5593
@pytest.mark.parametrize('dt', [None, 'malformed', pendulum.now().to_iso8601_string()[:5], 42])
5694
def test_pendulum_dt_malformed(dt):
5795
"""
@@ -68,3 +106,15 @@ def test_pendulum_date_malformed(date):
68106
"""
69107
with pytest.raises(ValidationError):
70108
DateModel(d=date)
109+
110+
111+
@pytest.mark.parametrize(
112+
'delta_t',
113+
[None, 'malformed', pendulum.today().to_iso8601_string()[:5], 42, '12m'],
114+
)
115+
def test_pendulum_duration_malformed(delta_t):
116+
"""
117+
Verifies that the instance fails to validate if malformed durations are passed.
118+
"""
119+
with pytest.raises(ValidationError):
120+
DurationModel(delta_t=delta_t)

0 commit comments

Comments
 (0)