Skip to content

Implement ser_json_temporal config option #1743

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions python/pydantic_core/_pydantic_core.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,7 @@ def to_json(
exclude_none: bool = False,
round_trip: bool = False,
timedelta_mode: Literal['iso8601', 'float'] = 'iso8601',
temporal_mode: Literal['iso8601', 'seconds', 'milliseconds'] = 'iso8601',
bytes_mode: Literal['utf8', 'base64', 'hex'] = 'utf8',
inf_nan_mode: Literal['null', 'constants', 'strings'] = 'constants',
serialize_unknown: bool = False,
Expand All @@ -425,6 +426,9 @@ def to_json(
exclude_none: Whether to exclude fields that have a value of `None`.
round_trip: Whether to enable serialization and validation round-trip support.
timedelta_mode: How to serialize `timedelta` objects, either `'iso8601'` or `'float'`.
temporal_mode: How to serialize datetime-like objects (`datetime`, `date`, `time`), either `'iso8601'`, `'seconds'`, or `'milliseconds'`.
`iso8601` returns an ISO 8601 string; `seconds` returns the Unix timestamp in seconds as a float; `milliseconds` returns the Unix timestamp in milliseconds as a float.

bytes_mode: How to serialize `bytes` objects, either `'utf8'`, `'base64'`, or `'hex'`.
inf_nan_mode: How to serialize `Infinity`, `-Infinity` and `NaN` values, either `'null'`, `'constants'`, or `'strings'`.
serialize_unknown: Attempt to serialize unknown types, `str(value)` will be used, if that fails
Expand Down Expand Up @@ -483,6 +487,7 @@ def to_jsonable_python(
exclude_none: bool = False,
round_trip: bool = False,
timedelta_mode: Literal['iso8601', 'float'] = 'iso8601',
temporal_mode: Literal['iso8601', 'seconds', 'milliseconds'] = 'iso8601',
bytes_mode: Literal['utf8', 'base64', 'hex'] = 'utf8',
inf_nan_mode: Literal['null', 'constants', 'strings'] = 'constants',
serialize_unknown: bool = False,
Expand All @@ -504,6 +509,9 @@ def to_jsonable_python(
exclude_none: Whether to exclude fields that have a value of `None`.
round_trip: Whether to enable serialization and validation round-trip support.
timedelta_mode: How to serialize `timedelta` objects, either `'iso8601'` or `'float'`.
temporal_mode: How to serialize datetime-like objects (`datetime`, `date`, `time`), either `'iso8601'`, `'seconds'`, or `'milliseconds'`.
`iso8601` returns an ISO 8601 string; `seconds` returns the Unix timestamp in seconds as a float; `milliseconds` returns the Unix timestamp in milliseconds as a float.

bytes_mode: How to serialize `bytes` objects, either `'utf8'`, `'base64'`, or `'hex'`.
inf_nan_mode: How to serialize `Infinity`, `-Infinity` and `NaN` values, either `'null'`, `'constants'`, or `'strings'`.
serialize_unknown: Attempt to serialize unknown types, `str(value)` will be used, if that fails
Expand Down
5 changes: 5 additions & 0 deletions python/pydantic_core/core_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ class CoreConfig(TypedDict, total=False):
str_to_upper: Whether to convert string fields to uppercase.
allow_inf_nan: Whether to allow infinity and NaN values for float fields. Default is `True`.
ser_json_timedelta: The serialization option for `timedelta` values. Default is 'iso8601'.
Note that if ser_json_temporal is set, then this param will be ignored.
ser_json_temporal: The serialization option for datetime like values. Default is 'iso8601'.
The types this covers are datetime, date, time and timedelta.
If this is set, it will take precedence over ser_json_timedelta
ser_json_bytes: The serialization option for `bytes` values. Default is 'utf8'.
ser_json_inf_nan: The serialization option for infinity and NaN values
in float fields. Default is 'null'.
Expand Down Expand Up @@ -102,6 +106,7 @@ class CoreConfig(TypedDict, total=False):
allow_inf_nan: bool # default: True
# the config options are used to customise serialization to JSON
ser_json_timedelta: Literal['iso8601', 'float'] # default: 'iso8601'
ser_json_temporal: Literal['iso8601', 'seconds', 'milliseconds'] # default: 'iso8601'
ser_json_bytes: Literal['utf8', 'base64', 'hex'] # default: 'utf8'
ser_json_inf_nan: Literal['null', 'constants', 'strings'] # default: 'null'
val_json_bytes: Literal['utf8', 'base64', 'hex'] # default: 'utf8'
Expand Down
2 changes: 1 addition & 1 deletion src/errors/validation_exception.rs
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ impl ValidationError {
include_context: bool,
include_input: bool,
) -> PyResult<Bound<'py, PyString>> {
let state = SerializationState::new("iso8601", "utf8", "constants")?;
let state = SerializationState::new("iso8601", "iso8601", "utf8", "constants")?;
let extra = state.extra(py, &SerMode::Json, None, false, false, true, None, false, None);
let serializer = ValidationErrorSerializer {
py,
Expand Down
87 changes: 87 additions & 0 deletions src/input/datetime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,93 @@ impl EitherTimedelta<'_> {
Self::PySubclass(py_timedelta) => pytimedelta_subclass_as_duration(py_timedelta),
}
}

pub fn total_seconds(&self) -> PyResult<f64> {
match self {
Self::Raw(timedelta) => {
let mut days: i64 = i64::from(timedelta.day);
let mut seconds: i64 = i64::from(timedelta.second);
let mut microseconds = i64::from(timedelta.microsecond);
if !timedelta.positive {
days = -days;
seconds = -seconds;
microseconds = -microseconds;
}

let days_seconds = (86_400 * days) + seconds;
if let Some(days_seconds_as_micros) = days_seconds.checked_mul(1_000_000) {
let total_microseconds = days_seconds_as_micros + microseconds;
Ok(total_microseconds as f64 / 1_000_000.0)
} else {
// Fall back to floating-point operations if the multiplication overflows
let total_seconds = days_seconds as f64 + microseconds as f64 / 1_000_000.0;
Ok(total_seconds)
}
}
Self::PyExact(py_timedelta) => {
let days: i64 = py_timedelta.get_days().into(); // -999999999 to 999999999
let seconds: i64 = py_timedelta.get_seconds().into(); // 0 through 86399
let microseconds = py_timedelta.get_microseconds(); // 0 through 999999
let days_seconds = (86_400 * days) + seconds;
if let Some(days_seconds_as_micros) = days_seconds.checked_mul(1_000_000) {
let total_microseconds = days_seconds_as_micros + i64::from(microseconds);
Ok(total_microseconds as f64 / 1_000_000.0)
} else {
// Fall back to floating-point operations if the multiplication overflows
let total_seconds = days_seconds as f64 + f64::from(microseconds) / 1_000_000.0;
Ok(total_seconds)
}
}
Self::PySubclass(py_timedelta) => py_timedelta
.call_method0(intern!(py_timedelta.py(), "total_seconds"))?
.extract(),
}
}

pub fn total_milliseconds(&self) -> PyResult<f64> {
match self {
Self::Raw(timedelta) => {
let mut days: i64 = i64::from(timedelta.day);
let mut seconds: i64 = i64::from(timedelta.second);
let mut microseconds = i64::from(timedelta.microsecond);
if !timedelta.positive {
days = -days;
seconds = -seconds;
microseconds = -microseconds;
}

let days_seconds = (86_400 * days) + seconds;
if let Some(days_seconds_as_micros) = days_seconds.checked_mul(1_000_000) {
let total_microseconds = days_seconds_as_micros + microseconds;
Ok(total_microseconds as f64 / 1_000.0)
} else {
// Fall back to floating-point operations if the multiplication overflows
let total_seconds = days_seconds as f64 + microseconds as f64 / 1_000.0;
Ok(total_seconds)
}
}
Self::PyExact(py_timedelta) => {
let days: i64 = py_timedelta.get_days().into(); // -999999999 to 999999999
let seconds: i64 = py_timedelta.get_seconds().into(); // 0 through 86399
let microseconds = py_timedelta.get_microseconds(); // 0 through 999999
let days_seconds = (86_400 * days) + seconds;
if let Some(days_seconds_as_micros) = days_seconds.checked_mul(1_000_000) {
let total_microseconds = days_seconds_as_micros + i64::from(microseconds);
Ok(total_microseconds as f64 / 1_000.0)
} else {
// Fall back to floating-point operations if the multiplication overflows
let total_milliseconds = days_seconds as f64 * 1_000.0 + f64::from(microseconds) / 1_000.0;
Ok(total_milliseconds)
}
}
Self::PySubclass(py_timedelta) => {
let total_seconds: f64 = py_timedelta
.call_method0(intern!(py_timedelta.py(), "total_seconds"))?
.extract()?;
Ok(total_seconds / 1000.0)
}
}
}
}

impl<'py> TryFrom<&'_ Bound<'py, PyAny>> for EitherTimedelta<'py> {
Expand Down
Loading
Loading