Skip to content

Commit 37ec6e7

Browse files
authored
Implement ser_json_temporal config option (#1743)
1 parent d523cf5 commit 37ec6e7

File tree

12 files changed

+905
-114
lines changed

12 files changed

+905
-114
lines changed

python/pydantic_core/_pydantic_core.pyi

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,7 @@ def to_json(
402402
exclude_none: bool = False,
403403
round_trip: bool = False,
404404
timedelta_mode: Literal['iso8601', 'float'] = 'iso8601',
405+
temporal_mode: Literal['iso8601', 'seconds', 'milliseconds'] = 'iso8601',
405406
bytes_mode: Literal['utf8', 'base64', 'hex'] = 'utf8',
406407
inf_nan_mode: Literal['null', 'constants', 'strings'] = 'constants',
407408
serialize_unknown: bool = False,
@@ -425,6 +426,9 @@ def to_json(
425426
exclude_none: Whether to exclude fields that have a value of `None`.
426427
round_trip: Whether to enable serialization and validation round-trip support.
427428
timedelta_mode: How to serialize `timedelta` objects, either `'iso8601'` or `'float'`.
429+
temporal_mode: How to serialize datetime-like objects (`datetime`, `date`, `time`), either `'iso8601'`, `'seconds'`, or `'milliseconds'`.
430+
`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.
431+
428432
bytes_mode: How to serialize `bytes` objects, either `'utf8'`, `'base64'`, or `'hex'`.
429433
inf_nan_mode: How to serialize `Infinity`, `-Infinity` and `NaN` values, either `'null'`, `'constants'`, or `'strings'`.
430434
serialize_unknown: Attempt to serialize unknown types, `str(value)` will be used, if that fails
@@ -483,6 +487,7 @@ def to_jsonable_python(
483487
exclude_none: bool = False,
484488
round_trip: bool = False,
485489
timedelta_mode: Literal['iso8601', 'float'] = 'iso8601',
490+
temporal_mode: Literal['iso8601', 'seconds', 'milliseconds'] = 'iso8601',
486491
bytes_mode: Literal['utf8', 'base64', 'hex'] = 'utf8',
487492
inf_nan_mode: Literal['null', 'constants', 'strings'] = 'constants',
488493
serialize_unknown: bool = False,
@@ -504,6 +509,9 @@ def to_jsonable_python(
504509
exclude_none: Whether to exclude fields that have a value of `None`.
505510
round_trip: Whether to enable serialization and validation round-trip support.
506511
timedelta_mode: How to serialize `timedelta` objects, either `'iso8601'` or `'float'`.
512+
temporal_mode: How to serialize datetime-like objects (`datetime`, `date`, `time`), either `'iso8601'`, `'seconds'`, or `'milliseconds'`.
513+
`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.
514+
507515
bytes_mode: How to serialize `bytes` objects, either `'utf8'`, `'base64'`, or `'hex'`.
508516
inf_nan_mode: How to serialize `Infinity`, `-Infinity` and `NaN` values, either `'null'`, `'constants'`, or `'strings'`.
509517
serialize_unknown: Attempt to serialize unknown types, `str(value)` will be used, if that fails

python/pydantic_core/core_schema.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ class CoreConfig(TypedDict, total=False):
6161
str_to_upper: Whether to convert string fields to uppercase.
6262
allow_inf_nan: Whether to allow infinity and NaN values for float fields. Default is `True`.
6363
ser_json_timedelta: The serialization option for `timedelta` values. Default is 'iso8601'.
64+
Note that if ser_json_temporal is set, then this param will be ignored.
65+
ser_json_temporal: The serialization option for datetime like values. Default is 'iso8601'.
66+
The types this covers are datetime, date, time and timedelta.
67+
If this is set, it will take precedence over ser_json_timedelta
6468
ser_json_bytes: The serialization option for `bytes` values. Default is 'utf8'.
6569
ser_json_inf_nan: The serialization option for infinity and NaN values
6670
in float fields. Default is 'null'.
@@ -102,6 +106,7 @@ class CoreConfig(TypedDict, total=False):
102106
allow_inf_nan: bool # default: True
103107
# the config options are used to customise serialization to JSON
104108
ser_json_timedelta: Literal['iso8601', 'float'] # default: 'iso8601'
109+
ser_json_temporal: Literal['iso8601', 'seconds', 'milliseconds'] # default: 'iso8601'
105110
ser_json_bytes: Literal['utf8', 'base64', 'hex'] # default: 'utf8'
106111
ser_json_inf_nan: Literal['null', 'constants', 'strings'] # default: 'null'
107112
val_json_bytes: Literal['utf8', 'base64', 'hex'] # default: 'utf8'

src/errors/validation_exception.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,7 @@ impl ValidationError {
340340
include_context: bool,
341341
include_input: bool,
342342
) -> PyResult<Bound<'py, PyString>> {
343-
let state = SerializationState::new("iso8601", "utf8", "constants")?;
343+
let state = SerializationState::new("iso8601", "iso8601", "utf8", "constants")?;
344344
let extra = state.extra(py, &SerMode::Json, None, false, false, true, None, false, None);
345345
let serializer = ValidationErrorSerializer {
346346
py,

src/input/datetime.rs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,93 @@ impl EitherTimedelta<'_> {
131131
Self::PySubclass(py_timedelta) => pytimedelta_subclass_as_duration(py_timedelta),
132132
}
133133
}
134+
135+
pub fn total_seconds(&self) -> PyResult<f64> {
136+
match self {
137+
Self::Raw(timedelta) => {
138+
let mut days: i64 = i64::from(timedelta.day);
139+
let mut seconds: i64 = i64::from(timedelta.second);
140+
let mut microseconds = i64::from(timedelta.microsecond);
141+
if !timedelta.positive {
142+
days = -days;
143+
seconds = -seconds;
144+
microseconds = -microseconds;
145+
}
146+
147+
let days_seconds = (86_400 * days) + seconds;
148+
if let Some(days_seconds_as_micros) = days_seconds.checked_mul(1_000_000) {
149+
let total_microseconds = days_seconds_as_micros + microseconds;
150+
Ok(total_microseconds as f64 / 1_000_000.0)
151+
} else {
152+
// Fall back to floating-point operations if the multiplication overflows
153+
let total_seconds = days_seconds as f64 + microseconds as f64 / 1_000_000.0;
154+
Ok(total_seconds)
155+
}
156+
}
157+
Self::PyExact(py_timedelta) => {
158+
let days: i64 = py_timedelta.get_days().into(); // -999999999 to 999999999
159+
let seconds: i64 = py_timedelta.get_seconds().into(); // 0 through 86399
160+
let microseconds = py_timedelta.get_microseconds(); // 0 through 999999
161+
let days_seconds = (86_400 * days) + seconds;
162+
if let Some(days_seconds_as_micros) = days_seconds.checked_mul(1_000_000) {
163+
let total_microseconds = days_seconds_as_micros + i64::from(microseconds);
164+
Ok(total_microseconds as f64 / 1_000_000.0)
165+
} else {
166+
// Fall back to floating-point operations if the multiplication overflows
167+
let total_seconds = days_seconds as f64 + f64::from(microseconds) / 1_000_000.0;
168+
Ok(total_seconds)
169+
}
170+
}
171+
Self::PySubclass(py_timedelta) => py_timedelta
172+
.call_method0(intern!(py_timedelta.py(), "total_seconds"))?
173+
.extract(),
174+
}
175+
}
176+
177+
pub fn total_milliseconds(&self) -> PyResult<f64> {
178+
match self {
179+
Self::Raw(timedelta) => {
180+
let mut days: i64 = i64::from(timedelta.day);
181+
let mut seconds: i64 = i64::from(timedelta.second);
182+
let mut microseconds = i64::from(timedelta.microsecond);
183+
if !timedelta.positive {
184+
days = -days;
185+
seconds = -seconds;
186+
microseconds = -microseconds;
187+
}
188+
189+
let days_seconds = (86_400 * days) + seconds;
190+
if let Some(days_seconds_as_micros) = days_seconds.checked_mul(1_000_000) {
191+
let total_microseconds = days_seconds_as_micros + microseconds;
192+
Ok(total_microseconds as f64 / 1_000.0)
193+
} else {
194+
// Fall back to floating-point operations if the multiplication overflows
195+
let total_seconds = days_seconds as f64 + microseconds as f64 / 1_000.0;
196+
Ok(total_seconds)
197+
}
198+
}
199+
Self::PyExact(py_timedelta) => {
200+
let days: i64 = py_timedelta.get_days().into(); // -999999999 to 999999999
201+
let seconds: i64 = py_timedelta.get_seconds().into(); // 0 through 86399
202+
let microseconds = py_timedelta.get_microseconds(); // 0 through 999999
203+
let days_seconds = (86_400 * days) + seconds;
204+
if let Some(days_seconds_as_micros) = days_seconds.checked_mul(1_000_000) {
205+
let total_microseconds = days_seconds_as_micros + i64::from(microseconds);
206+
Ok(total_microseconds as f64 / 1_000.0)
207+
} else {
208+
// Fall back to floating-point operations if the multiplication overflows
209+
let total_milliseconds = days_seconds as f64 * 1_000.0 + f64::from(microseconds) / 1_000.0;
210+
Ok(total_milliseconds)
211+
}
212+
}
213+
Self::PySubclass(py_timedelta) => {
214+
let total_seconds: f64 = py_timedelta
215+
.call_method0(intern!(py_timedelta.py(), "total_seconds"))?
216+
.extract()?;
217+
Ok(total_seconds / 1000.0)
218+
}
219+
}
220+
}
134221
}
135222

136223
impl<'py> TryFrom<&'_ Bound<'py, PyAny>> for EitherTimedelta<'py> {

0 commit comments

Comments
 (0)