Skip to content

Commit

Permalink
Merge pull request #325 from lsst-sqre/tickets/DM-47262
Browse files Browse the repository at this point in the history
DM-47262: Add serializers for *Timedelta types
  • Loading branch information
rra authored Nov 13, 2024
2 parents 470e512 + d2d5b7c commit 4f13093
Showing 3 changed files with 34 additions and 3 deletions.
7 changes: 7 additions & 0 deletions changelog.d/20241112_163814_rra_DM_47262.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
### 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.

### Bug fixes

- `SecondsTimedelta` now correctly validates an input stringified floating-point number of seconds instead of truncating it to an integer.
18 changes: 15 additions & 3 deletions safir/src/safir/pydantic/_types.py
Original file line number Diff line number Diff line change
@@ -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
@@ -110,7 +115,11 @@ 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, when_used="json"
),
]
"""Parse a human-readable string into a `datetime.timedelta`.
@@ -133,7 +142,10 @@ 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)),
BeforeValidator(lambda v: v if not isinstance(v, str) else float(v)),
PlainSerializer(
lambda t: t.total_seconds(), return_type=float, when_used="json"
),
]
"""Parse a float number of seconds into a `datetime.timedelta`.
12 changes: 12 additions & 0 deletions safir/tests/pydantic_test.py
Original file line number Diff line number Diff line change
@@ -145,14 +145,20 @@ class TestModel(BaseModel):

model = TestModel.model_validate({"delta": timedelta(seconds=5)})
assert model.delta == timedelta(seconds=5)
assert model.model_dump(mode="python") == {"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)
assert model.model_dump(mode="python") == {"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)
model = TestModel.model_validate({"delta": "10.5"})
assert model.delta.total_seconds() == 10.5

with pytest.raises(ValidationError):
TestModel.model_validate({"delta": "P1DT12H"})
@@ -164,12 +170,18 @@ class TestModel(BaseModel):

model = TestModel.model_validate({"delta": timedelta(seconds=5)})
assert model.delta == timedelta(seconds=5)
assert model.model_dump(mode="python") == {"delta": timedelta(seconds=5)}
assert model.model_dump(mode="json") == {"delta": 5}
model = TestModel.model_validate({"delta": 600})
assert model.delta == timedelta(seconds=600)
assert model.model_dump(mode="python") == {"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)
model = TestModel.model_validate({"delta": "10.5"})
assert model.delta.total_seconds() == 10.5

with pytest.raises(ValidationError):
TestModel.model_validate({"delta": "P1DT12H"})

0 comments on commit 4f13093

Please sign in to comment.