Skip to content

Commit 3668b3a

Browse files
authored
✨ add new type: Epoch Unix Timestamp (#240)
1 parent f7875f0 commit 3668b3a

File tree

3 files changed

+169
-0
lines changed

3 files changed

+169
-0
lines changed

pydantic_extra_types/epoch.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
from __future__ import annotations
2+
3+
import datetime
4+
from typing import Any, Callable
5+
6+
import pydantic_core.core_schema
7+
from pydantic import GetJsonSchemaHandler
8+
from pydantic.json_schema import JsonSchemaValue
9+
from pydantic_core import CoreSchema, core_schema
10+
11+
EPOCH = datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc)
12+
13+
14+
class _Base(datetime.datetime):
15+
TYPE: str = ''
16+
SCHEMA: pydantic_core.core_schema.CoreSchema
17+
18+
@classmethod
19+
def __get_pydantic_json_schema__(
20+
cls, core_schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler
21+
) -> JsonSchemaValue:
22+
field_schema: dict[str, Any] = {}
23+
field_schema.update(type=cls.TYPE, format='date-time')
24+
return field_schema
25+
26+
@classmethod
27+
def __get_pydantic_core_schema__(
28+
cls, source: type[Any], handler: Callable[[Any], CoreSchema]
29+
) -> core_schema.CoreSchema:
30+
return core_schema.with_info_after_validator_function(
31+
cls._validate,
32+
cls.SCHEMA,
33+
serialization=core_schema.wrap_serializer_function_ser_schema(cls._f, return_schema=cls.SCHEMA),
34+
)
35+
36+
@classmethod
37+
def _validate(cls, __input_value: Any, _: Any) -> datetime.datetime:
38+
return EPOCH + datetime.timedelta(seconds=__input_value)
39+
40+
@classmethod
41+
def _f(cls, value: Any, serializer: Callable[[Any], Any]) -> Any: # pragma: no cover
42+
raise NotImplementedError(cls)
43+
44+
45+
class Number(_Base):
46+
"""epoch.Number parses unix timestamp as float and converts it to datetime.
47+
48+
```py
49+
from pydantic import BaseModel
50+
51+
from pydantic_extra_types import epoch
52+
53+
class LogEntry(BaseModel):
54+
timestamp: epoch.Number
55+
56+
logentry = LogEntry(timestamp=1.1)
57+
print(logentry)
58+
#> timestamp=datetime.datetime(1970, 1, 1, 0, 0, 1, 100000, tzinfo=datetime.timezone.utc)
59+
```
60+
"""
61+
62+
TYPE = 'number'
63+
SCHEMA = core_schema.float_schema()
64+
65+
@classmethod
66+
def _f(cls, value: Any, serializer: Callable[[float], float]) -> float:
67+
ts = value.timestamp()
68+
return serializer(ts)
69+
70+
71+
class Integer(_Base):
72+
"""epoch.Integer parses unix timestamp as integer and converts it to datetime.
73+
74+
```
75+
```py
76+
from pydantic import BaseModel
77+
78+
from pydantic_extra_types import epoch
79+
80+
class LogEntry(BaseModel):
81+
timestamp: epoch.Integer
82+
83+
logentry = LogEntry(timestamp=1)
84+
print(logentry)
85+
#> timestamp=datetime.datetime(1970, 1, 1, 0, 0, 1, tzinfo=datetime.timezone.utc)
86+
```
87+
"""
88+
89+
TYPE = 'integer'
90+
SCHEMA = core_schema.int_schema()
91+
92+
@classmethod
93+
def _f(cls, value: Any, serializer: Callable[[int], int]) -> int:
94+
ts = value.timestamp()
95+
return serializer(int(ts))

tests/test_epoch.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import datetime
2+
3+
import pytest
4+
5+
from pydantic_extra_types import epoch
6+
7+
8+
@pytest.mark.parametrize('type_,cls_', [(int, epoch.Integer), (float, epoch.Number)], ids=['integer', 'number'])
9+
def test_type(type_, cls_):
10+
from pydantic import BaseModel
11+
12+
class A(BaseModel):
13+
epoch: cls_
14+
15+
now = datetime.datetime.now(tz=datetime.timezone.utc)
16+
ts = type_(now.timestamp())
17+
a = A.model_validate({'epoch': ts})
18+
v = a.model_dump()
19+
assert v['epoch'] == ts
20+
21+
b = A.model_construct(epoch=now)
22+
23+
v = b.model_dump()
24+
assert v['epoch'] == ts
25+
26+
c = A.model_validate(dict(epoch=ts))
27+
v = c.model_dump()
28+
assert v['epoch'] == ts
29+
30+
31+
@pytest.mark.parametrize('cls_', [(epoch.Integer), (epoch.Number)], ids=['integer', 'number'])
32+
def test_schema(cls_):
33+
from pydantic import BaseModel
34+
35+
class A(BaseModel):
36+
dt: cls_
37+
38+
v = A.model_json_schema()
39+
assert (dt := v['properties']['dt'])['type'] == cls_.TYPE and dt['format'] == 'date-time'

tests/test_json_schema.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from typing_extensions import Annotated
1212

1313
import pydantic_extra_types
14+
from pydantic_extra_types import epoch
1415
from pydantic_extra_types.color import Color
1516
from pydantic_extra_types.coordinate import Coordinate, Latitude, Longitude
1617
from pydantic_extra_types.country import CountryAlpha2, CountryAlpha3, CountryNumericCode, CountryShortName
@@ -464,6 +465,40 @@
464465
],
465466
},
466467
),
468+
(
469+
epoch.Integer,
470+
{
471+
'title': 'Model',
472+
'type': 'object',
473+
'properties': {
474+
'x': {
475+
'title': 'X',
476+
'type': 'integer',
477+
'format': 'date-time',
478+
},
479+
},
480+
'required': [
481+
'x',
482+
],
483+
},
484+
),
485+
(
486+
epoch.Number,
487+
{
488+
'title': 'Model',
489+
'type': 'object',
490+
'properties': {
491+
'x': {
492+
'title': 'X',
493+
'type': 'number',
494+
'format': 'date-time',
495+
},
496+
},
497+
'required': [
498+
'x',
499+
],
500+
},
501+
),
467502
],
468503
)
469504
def test_json_schema(cls, expected):

0 commit comments

Comments
 (0)