Skip to content

Commit

Permalink
Add serializers for *Timedelta types
Browse files Browse the repository at this point in the history
Add serializers to `HumanTimedelta` and `SecondsTimedelta` that
serialize those Pydantic fields to a float number of seconds instead
of ISO 8601 durations. This means those data types now can be
round-tripped (serialized and then deserialized to the original
value), whereas before they could not be. This also avoids ISO 8601
durations in service replies that use models including those types.
  • Loading branch information
rra committed Nov 13, 2024
1 parent 470e512 commit 736c932
Show file tree
Hide file tree
Showing 3 changed files with 17 additions and 2 deletions.
3 changes: 3 additions & 0 deletions changelog.d/20241112_163814_rra_DM_47262.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Backwards-incompatible changes

- Add serializers to `HumanTimedelta` and `SecondsTimedelta` that serialize those Pydantic fields to a float number of seconds instead of ISO 8601 durations. This means those data types now can be round-tripped (serialized and then deserialized to the original value), whereas before they could not be.
12 changes: 10 additions & 2 deletions safir/src/safir/pydantic/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@
from datetime import timedelta
from typing import Annotated, TypeAlias

from pydantic import AfterValidator, BeforeValidator, UrlConstraints
from pydantic import (
AfterValidator,
BeforeValidator,
PlainSerializer,
UrlConstraints,
)
from pydantic_core import Url

from safir.datetime import parse_timedelta
Expand Down Expand Up @@ -110,7 +115,9 @@ def _validate_human_timedelta(v: str | float | timedelta) -> float | timedelta:


HumanTimedelta: TypeAlias = Annotated[
timedelta, BeforeValidator(_validate_human_timedelta)
timedelta,
BeforeValidator(_validate_human_timedelta),
PlainSerializer(lambda t: t.total_seconds(), return_type=float),
]
"""Parse a human-readable string into a `datetime.timedelta`.
Expand All @@ -134,6 +141,7 @@ def _validate_human_timedelta(v: str | float | timedelta) -> float | timedelta:
SecondsTimedelta: TypeAlias = Annotated[
timedelta,
BeforeValidator(lambda v: v if not isinstance(v, str) else int(v)),
PlainSerializer(lambda t: t.total_seconds(), return_type=float),
]
"""Parse a float number of seconds into a `datetime.timedelta`.
Expand Down
4 changes: 4 additions & 0 deletions safir/tests/pydantic_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,12 +145,14 @@ class TestModel(BaseModel):

model = TestModel.model_validate({"delta": timedelta(seconds=5)})
assert model.delta == timedelta(seconds=5)
assert model.model_dump(mode="json") == {"delta": 5}
model = TestModel.model_validate({"delta": "4h5m18s"})
assert model.delta == timedelta(hours=4, minutes=5, seconds=18)
model = TestModel.model_validate({"delta": 600})
assert model.delta == timedelta(seconds=600)
model = TestModel.model_validate({"delta": 4.5})
assert model.delta.total_seconds() == 4.5
assert model.model_dump(mode="json") == {"delta": 4.5}
model = TestModel.model_validate({"delta": "300"})
assert model.delta == timedelta(seconds=300)

Expand All @@ -164,10 +166,12 @@ class TestModel(BaseModel):

model = TestModel.model_validate({"delta": timedelta(seconds=5)})
assert model.delta == timedelta(seconds=5)
assert model.model_dump(mode="json") == {"delta": 5}
model = TestModel.model_validate({"delta": 600})
assert model.delta == timedelta(seconds=600)
model = TestModel.model_validate({"delta": 4.5})
assert model.delta.total_seconds() == 4.5
assert model.model_dump(mode="json") == {"delta": 4.5}
model = TestModel.model_validate({"delta": "300"})
assert model.delta == timedelta(seconds=300)

Expand Down

0 comments on commit 736c932

Please sign in to comment.