Skip to content

Commit 4f89f75

Browse files
theunkn0wn1yezz123
andauthored
✨ Support Pendulum Datetime to pydantic-extra-types (#110)
* [#112] add pendulum dt support * [#112] Add pendulum to testing requirements * ♻️ update requirements * 📝 fix documentation * 🐛 add test case for JSON schema --------- Co-authored-by: Yasser Tahiri <[email protected]>
1 parent 2c16086 commit 4f89f75

File tree

7 files changed

+139
-6
lines changed

7 files changed

+139
-6
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""
2+
Native Pendulum DateTime object implementation. This is a copy of the Pendulum DateTime object, but with a Pydantic
3+
CoreSchema implementation. This allows Pydantic to validate the DateTime object.
4+
"""
5+
6+
try:
7+
from pendulum import DateTime as _DateTime
8+
from pendulum import parse
9+
except ModuleNotFoundError: # pragma: no cover
10+
raise RuntimeError(
11+
'The `pendulum_dt` module requires "pendulum" to be installed. You can install it with "pip install pendulum".'
12+
)
13+
from typing import Any, List, Type
14+
15+
from pydantic import GetCoreSchemaHandler
16+
from pydantic_core import PydanticCustomError, core_schema
17+
18+
19+
class DateTime(_DateTime):
20+
"""
21+
A `pendulum.DateTime` object. At runtime, this type decomposes into pendulum.DateTime automatically.
22+
This type exists because Pydantic throws a fit on unknown types.
23+
24+
```python
25+
from pydantic import BaseModel
26+
from pydantic_extra_types.pendulum_dt import DateTime
27+
28+
class test_model(BaseModel):
29+
dt: DateTime
30+
31+
print(test_model(dt='2021-01-01T00:00:00+00:00'))
32+
33+
#> test_model(dt=DateTime(2021, 1, 1, 0, 0, 0, tzinfo=FixedTimezone(0, name="+00:00")))
34+
```
35+
"""
36+
37+
__slots__: List[str] = []
38+
39+
@classmethod
40+
def __get_pydantic_core_schema__(cls, source: Type[Any], handler: GetCoreSchemaHandler) -> core_schema.CoreSchema:
41+
"""
42+
Return a Pydantic CoreSchema with the Datetime validation
43+
44+
Args:
45+
source: The source type to be converted.
46+
handler: The handler to get the CoreSchema.
47+
48+
Returns:
49+
A Pydantic CoreSchema with the Datetime validation.
50+
"""
51+
return core_schema.no_info_wrap_validator_function(cls._validate, core_schema.datetime_schema())
52+
53+
@classmethod
54+
def _validate(cls, value: Any, handler: core_schema.ValidatorFunctionWrapHandler) -> Any:
55+
"""
56+
Validate the datetime object and return it.
57+
58+
Args:
59+
value: The value to validate.
60+
handler: The handler to get the CoreSchema.
61+
62+
Returns:
63+
The validated value or raises a PydanticCustomError.
64+
"""
65+
# if we are passed an existing instance, pass it straight through.
66+
if isinstance(value, _DateTime):
67+
return handler(value)
68+
69+
# otherwise, parse it.
70+
try:
71+
data = parse(value)
72+
except Exception as exc:
73+
raise PydanticCustomError('value_error', 'value is not a valid timestamp') from exc
74+
return handler(data)

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ all = [
4848
'phonenumbers>=8,<9',
4949
'pycountry>=23,<24',
5050
'python-ulid>=1,<2',
51+
'pendulum>=3.0.0,<4.0.0'
5152
]
5253

5354
[project.urls]

requirements/linting.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ pyupgrade==3.15.0
4040
# via -r requirements/linting.in
4141
pyyaml==6.0.1
4242
# via pre-commit
43-
ruff==0.1.11
43+
ruff==0.1.14
4444
# via -r requirements/linting.in
4545
tokenize-rt==5.2.0
4646
# via pyupgrade

requirements/pyproject.txt

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,29 @@
66
#
77
annotated-types==0.6.0
88
# via pydantic
9-
phonenumbers==8.13.27
9+
pendulum==3.0.0
10+
# via pydantic-extra-types (pyproject.toml)
11+
phonenumbers==8.13.28
1012
# via pydantic-extra-types (pyproject.toml)
1113
pycountry==23.12.11
1214
# via pydantic-extra-types (pyproject.toml)
1315
pydantic==2.5.3
1416
# via pydantic-extra-types (pyproject.toml)
1517
pydantic-core==2.14.6
1618
# via pydantic
19+
python-dateutil==2.8.2
20+
# via
21+
# pendulum
22+
# time-machine
1723
python-ulid==1.1.0
1824
# via pydantic-extra-types (pyproject.toml)
25+
six==1.16.0
26+
# via python-dateutil
27+
time-machine==2.13.0
28+
# via pendulum
1929
typing-extensions==4.9.0
2030
# via
2131
# pydantic
2232
# pydantic-core
23-
24-
# The following packages are considered to be unsafe in a requirements file:
25-
# setuptools
33+
tzdata==2023.4
34+
# via pendulum

requirements/testing.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ mdurl==0.1.2
2727
# via markdown-it-py
2828
packaging==23.2
2929
# via pytest
30-
pluggy==1.3.0
30+
pluggy==1.4.0
3131
# via pytest
3232
pygments==2.17.2
3333
# via rich

tests/test_json_schema.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from pydantic_extra_types.isbn import ISBN
1313
from pydantic_extra_types.mac_address import MacAddress
1414
from pydantic_extra_types.payment import PaymentCardNumber
15+
from pydantic_extra_types.pendulum_dt import DateTime
1516
from pydantic_extra_types.ulid import ULID
1617

1718

@@ -190,6 +191,15 @@
190191
'type': 'object',
191192
},
192193
),
194+
(
195+
DateTime,
196+
{
197+
'properties': {'x': {'format': 'date-time', 'title': 'X', 'type': 'string'}},
198+
'required': ['x'],
199+
'title': 'Model',
200+
'type': 'object',
201+
},
202+
),
193203
],
194204
)
195205
def test_json_schema(cls, expected):

tests/test_pendulum_dt.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import pendulum
2+
import pytest
3+
from pydantic import BaseModel, ValidationError
4+
5+
from pydantic_extra_types.pendulum_dt import DateTime
6+
7+
8+
class Model(BaseModel):
9+
dt: DateTime
10+
11+
12+
def test_pendulum_dt_existing_instance():
13+
"""
14+
Verifies that constructing a model with an existing pendulum dt doesn't throw.
15+
"""
16+
now = pendulum.now()
17+
model = Model(dt=now)
18+
assert model.dt == now
19+
20+
21+
@pytest.mark.parametrize(
22+
'dt', [pendulum.now().to_iso8601_string(), pendulum.now().to_w3c_string(), pendulum.now().to_iso8601_string()]
23+
)
24+
def test_pendulum_dt_from_serialized(dt):
25+
"""
26+
Verifies that building an instance from serialized, well-formed strings decode properly.
27+
"""
28+
dt_actual = pendulum.parse(dt)
29+
model = Model(dt=dt)
30+
assert model.dt == dt_actual
31+
32+
33+
@pytest.mark.parametrize('dt', [None, 'malformed', pendulum.now().to_iso8601_string()[:5], 42])
34+
def test_pendulum_dt_malformed(dt):
35+
"""
36+
Verifies that the instance fails to validate if malformed dt are passed.
37+
"""
38+
with pytest.raises(ValidationError):
39+
Model(dt=dt)

0 commit comments

Comments
 (0)