diff --git a/src/input/datetime.rs b/src/input/datetime.rs index 279e79159..a69da9227 100644 --- a/src/input/datetime.rs +++ b/src/input/datetime.rs @@ -6,6 +6,7 @@ use pyo3::pyclass::CompareOp; use pyo3::types::PyTuple; use pyo3::types::{PyDate, PyDateTime, PyDelta, PyDeltaAccess, PyDict, PyTime, PyTzInfo}; use pyo3::IntoPyObjectExt; +use speedate::DateConfig; use speedate::{ Date, DateTime, DateTimeConfig, Duration, MicrosecondsPrecisionOverflowBehavior, ParseError, Time, TimeConfig, }; @@ -21,6 +22,7 @@ use super::Input; use crate::errors::ToErrorValue; use crate::errors::{ErrorType, ValError, ValResult}; use crate::tools::py_err; +use crate::validators::TemporalUnitMode; #[cfg_attr(debug_assertions, derive(Debug))] pub enum EitherDate<'py> { @@ -324,8 +326,12 @@ impl<'py> EitherDateTime<'py> { } } -pub fn bytes_as_date<'py>(input: &(impl Input<'py> + ?Sized), bytes: &[u8]) -> ValResult> { - match Date::parse_bytes(bytes) { +pub fn bytes_as_date<'py>( + input: &(impl Input<'py> + ?Sized), + bytes: &[u8], + mode: TemporalUnitMode, +) -> ValResult> { + match Date::parse_bytes_with_config(bytes, &DateConfig::builder().timestamp_unit(mode.into()).build()) { Ok(date) => Ok(date.into()), Err(err) => Err(ValError::new( ErrorType::DateParsing { @@ -364,6 +370,7 @@ pub fn bytes_as_datetime<'py>( input: &(impl Input<'py> + ?Sized), bytes: &[u8], microseconds_overflow_behavior: MicrosecondsPrecisionOverflowBehavior, + mode: TemporalUnitMode, ) -> ValResult> { match DateTime::parse_bytes_with_config( bytes, @@ -372,7 +379,7 @@ pub fn bytes_as_datetime<'py>( microseconds_precision_overflow_behavior: microseconds_overflow_behavior, unix_timestamp_offset: Some(0), }, - ..Default::default() + timestamp_unit: mode.into(), }, ) { Ok(dt) => Ok(dt.into()), @@ -390,6 +397,7 @@ pub fn int_as_datetime<'py>( input: &(impl Input<'py> + ?Sized), timestamp: i64, timestamp_microseconds: u32, + mode: TemporalUnitMode, ) -> ValResult> { match DateTime::from_timestamp_with_config( timestamp, @@ -399,7 +407,7 @@ pub fn int_as_datetime<'py>( unix_timestamp_offset: Some(0), ..Default::default() }, - ..Default::default() + timestamp_unit: mode.into(), }, ) { Ok(dt) => Ok(dt.into()), @@ -427,12 +435,30 @@ macro_rules! nan_check { }; } -pub fn float_as_datetime<'py>(input: &(impl Input<'py> + ?Sized), timestamp: f64) -> ValResult> { +pub fn float_as_datetime<'py>( + input: &(impl Input<'py> + ?Sized), + timestamp: f64, + mode: TemporalUnitMode, +) -> ValResult> { nan_check!(input, timestamp, DatetimeParsing); - let microseconds = timestamp.fract().abs() * 1_000_000.0; + let microseconds = match mode { + TemporalUnitMode::Seconds => timestamp.fract().abs() * 1_000_000.0, + TemporalUnitMode::Milliseconds => timestamp.fract().abs() * 1_000.0, + TemporalUnitMode::Infer => { + // Use the same watershed from speedate to determine if we treat the float as seconds or milliseconds. + // TODO: should we expose this from speedate? + if timestamp.abs() <= 20_000_000_000.0 { + // treat as seconds + timestamp.fract().abs() * 1_000_000.0 + } else { + // treat as milliseconds + timestamp.fract().abs() * 1_000.0 + } + } + }; // checking for extra digits in microseconds is unreliable with large floats, // so we just round to the nearest microsecond - int_as_datetime(input, timestamp.floor() as i64, microseconds.round() as u32) + int_as_datetime(input, timestamp.floor() as i64, microseconds.round() as u32, mode) } pub fn date_as_datetime<'py>(date: &Bound<'py, PyDate>) -> PyResult> { diff --git a/src/input/input_abstract.rs b/src/input/input_abstract.rs index 119c862ac..e84c132df 100644 --- a/src/input/input_abstract.rs +++ b/src/input/input_abstract.rs @@ -8,7 +8,7 @@ use pyo3::{intern, prelude::*, IntoPyObjectExt}; use crate::errors::{ErrorTypeDefaults, InputValue, LocItem, ValError, ValResult}; use crate::lookup_key::{LookupKey, LookupPath}; use crate::tools::py_err; -use crate::validators::ValBytesMode; +use crate::validators::{TemporalUnitMode, ValBytesMode}; use super::datetime::{EitherDate, EitherDateTime, EitherTime, EitherTimedelta}; use super::return_enums::{EitherBytes, EitherComplex, EitherInt, EitherString}; @@ -158,7 +158,7 @@ pub trait Input<'py>: fmt::Debug { fn validate_iter(&self) -> ValResult>; - fn validate_date(&self, strict: bool) -> ValMatch>; + fn validate_date(&self, strict: bool, mode: TemporalUnitMode) -> ValMatch>; fn validate_time( &self, @@ -170,6 +170,7 @@ pub trait Input<'py>: fmt::Debug { &self, strict: bool, microseconds_overflow_behavior: speedate::MicrosecondsPrecisionOverflowBehavior, + mode: TemporalUnitMode, ) -> ValMatch>; fn validate_timedelta( diff --git a/src/input/input_json.rs b/src/input/input_json.rs index 6828f5927..27a710058 100644 --- a/src/input/input_json.rs +++ b/src/input/input_json.rs @@ -12,7 +12,7 @@ use crate::input::return_enums::EitherComplex; use crate::lookup_key::{LookupKey, LookupPath}; use crate::validators::complex::string_to_complex; use crate::validators::decimal::create_decimal; -use crate::validators::ValBytesMode; +use crate::validators::{TemporalUnitMode, ValBytesMode}; use super::datetime::{ bytes_as_date, bytes_as_datetime, bytes_as_time, bytes_as_timedelta, float_as_datetime, float_as_duration, @@ -277,9 +277,9 @@ impl<'py, 'data> Input<'py> for JsonValue<'data> { } } - fn validate_date(&self, _strict: bool) -> ValResult>> { + fn validate_date(&self, _strict: bool, mode: TemporalUnitMode) -> ValResult>> { match self { - JsonValue::Str(v) => bytes_as_date(self, v.as_bytes()).map(ValidationMatch::strict), + JsonValue::Str(v) => bytes_as_date(self, v.as_bytes(), mode).map(ValidationMatch::strict), _ => Err(ValError::new(ErrorTypeDefaults::DateType, self)), } } @@ -313,13 +313,14 @@ impl<'py, 'data> Input<'py> for JsonValue<'data> { &self, strict: bool, microseconds_overflow_behavior: speedate::MicrosecondsPrecisionOverflowBehavior, + mode: TemporalUnitMode, ) -> ValResult>> { match self { JsonValue::Str(v) => { - bytes_as_datetime(self, v.as_bytes(), microseconds_overflow_behavior).map(ValidationMatch::strict) + bytes_as_datetime(self, v.as_bytes(), microseconds_overflow_behavior, mode).map(ValidationMatch::strict) } - JsonValue::Int(v) if !strict => int_as_datetime(self, *v, 0).map(ValidationMatch::lax), - JsonValue::Float(v) if !strict => float_as_datetime(self, *v).map(ValidationMatch::lax), + JsonValue::Int(v) if !strict => int_as_datetime(self, *v, 0, mode).map(ValidationMatch::lax), + JsonValue::Float(v) if !strict => float_as_datetime(self, *v, mode).map(ValidationMatch::lax), _ => Err(ValError::new(ErrorTypeDefaults::DatetimeType, self)), } } @@ -485,8 +486,8 @@ impl<'py> Input<'py> for str { Ok(string_to_vec(self).into()) } - fn validate_date(&self, _strict: bool) -> ValResult>> { - bytes_as_date(self, self.as_bytes()).map(ValidationMatch::lax) + fn validate_date(&self, _strict: bool, mode: TemporalUnitMode) -> ValResult>> { + bytes_as_date(self, self.as_bytes(), mode).map(ValidationMatch::lax) } fn validate_time( @@ -501,8 +502,9 @@ impl<'py> Input<'py> for str { &self, _strict: bool, microseconds_overflow_behavior: MicrosecondsPrecisionOverflowBehavior, + mode: TemporalUnitMode, ) -> ValResult>> { - bytes_as_datetime(self, self.as_bytes(), microseconds_overflow_behavior).map(ValidationMatch::lax) + bytes_as_datetime(self, self.as_bytes(), microseconds_overflow_behavior, mode).map(ValidationMatch::lax) } fn validate_timedelta( diff --git a/src/input/input_python.rs b/src/input/input_python.rs index e82cbaed7..71f2d37dc 100644 --- a/src/input/input_python.rs +++ b/src/input/input_python.rs @@ -18,6 +18,7 @@ use crate::tools::{extract_i64, safe_repr}; use crate::validators::complex::string_to_complex; use crate::validators::decimal::{create_decimal, get_decimal_type}; use crate::validators::Exactness; +use crate::validators::TemporalUnitMode; use crate::validators::ValBytesMode; use crate::ArgsKwargs; @@ -494,7 +495,7 @@ impl<'py> Input<'py> for Bound<'py, PyAny> { } } - fn validate_date(&self, strict: bool) -> ValResult>> { + fn validate_date(&self, strict: bool, mode: TemporalUnitMode) -> ValResult>> { if let Ok(date) = self.downcast_exact::() { Ok(ValidationMatch::exact(date.clone().into())) } else if self.is_instance_of::() { @@ -515,7 +516,7 @@ impl<'py> Input<'py> for Bound<'py, PyAny> { None } } { - bytes_as_date(self, bytes).map(ValidationMatch::lax) + bytes_as_date(self, bytes, mode).map(ValidationMatch::lax) } else { Err(ValError::new(ErrorTypeDefaults::DateType, self)) } @@ -559,6 +560,7 @@ impl<'py> Input<'py> for Bound<'py, PyAny> { &self, strict: bool, microseconds_overflow_behavior: MicrosecondsPrecisionOverflowBehavior, + mode: TemporalUnitMode, ) -> ValResult>> { if let Ok(dt) = self.downcast_exact::() { return Ok(ValidationMatch::exact(dt.clone().into())); @@ -570,15 +572,15 @@ impl<'py> Input<'py> for Bound<'py, PyAny> { if !strict { return if let Ok(py_str) = self.downcast::() { let str = py_string_str(py_str)?; - bytes_as_datetime(self, str.as_bytes(), microseconds_overflow_behavior) + bytes_as_datetime(self, str.as_bytes(), microseconds_overflow_behavior, mode) } else if let Ok(py_bytes) = self.downcast::() { - bytes_as_datetime(self, py_bytes.as_bytes(), microseconds_overflow_behavior) + bytes_as_datetime(self, py_bytes.as_bytes(), microseconds_overflow_behavior, mode) } else if self.is_exact_instance_of::() { Err(ValError::new(ErrorTypeDefaults::DatetimeType, self)) } else if let Some(int) = extract_i64(self) { - int_as_datetime(self, int, 0) + int_as_datetime(self, int, 0, mode) } else if let Ok(float) = self.extract::() { - float_as_datetime(self, float) + float_as_datetime(self, float, mode) } else if let Ok(date) = self.downcast::() { Ok(date_as_datetime(date)?) } else { diff --git a/src/input/input_string.rs b/src/input/input_string.rs index 0ab4ad014..97fb17a3d 100644 --- a/src/input/input_string.rs +++ b/src/input/input_string.rs @@ -9,7 +9,7 @@ use crate::lookup_key::{LookupKey, LookupPath}; use crate::tools::safe_repr; use crate::validators::complex::string_to_complex; use crate::validators::decimal::create_decimal; -use crate::validators::ValBytesMode; +use crate::validators::{TemporalUnitMode, ValBytesMode}; use super::datetime::{ bytes_as_date, bytes_as_datetime, bytes_as_time, bytes_as_timedelta, EitherDate, EitherDateTime, EitherTime, @@ -201,9 +201,9 @@ impl<'py> Input<'py> for StringMapping<'py> { Err(ValError::new(ErrorTypeDefaults::IterableType, self)) } - fn validate_date(&self, _strict: bool) -> ValResult>> { + fn validate_date(&self, _strict: bool, mode: TemporalUnitMode) -> ValResult>> { match self { - Self::String(s) => bytes_as_date(self, py_string_str(s)?.as_bytes()).map(ValidationMatch::strict), + Self::String(s) => bytes_as_date(self, py_string_str(s)?.as_bytes(), mode).map(ValidationMatch::strict), Self::Mapping(_) => Err(ValError::new(ErrorTypeDefaults::DateType, self)), } } @@ -224,10 +224,13 @@ impl<'py> Input<'py> for StringMapping<'py> { &self, _strict: bool, microseconds_overflow_behavior: MicrosecondsPrecisionOverflowBehavior, + mode: TemporalUnitMode, ) -> ValResult>> { match self { - Self::String(s) => bytes_as_datetime(self, py_string_str(s)?.as_bytes(), microseconds_overflow_behavior) - .map(ValidationMatch::strict), + Self::String(s) => { + bytes_as_datetime(self, py_string_str(s)?.as_bytes(), microseconds_overflow_behavior, mode) + .map(ValidationMatch::strict) + } Self::Mapping(_) => Err(ValError::new(ErrorTypeDefaults::DatetimeType, self)), } } diff --git a/src/validators/config.rs b/src/validators/config.rs index a14104628..9d7c24770 100644 --- a/src/validators/config.rs +++ b/src/validators/config.rs @@ -1,16 +1,17 @@ use std::borrow::Cow; use std::str::FromStr; +use crate::build_tools::py_schema_err; +use crate::errors::ErrorType; +use crate::input::EitherBytes; +use crate::serializers::BytesMode; +use crate::tools::SchemaDict; use base64::engine::general_purpose::GeneralPurpose; use base64::engine::{DecodePaddingMode, GeneralPurposeConfig}; use base64::{alphabet, DecodeError, Engine}; use pyo3::types::{PyDict, PyString}; use pyo3::{intern, prelude::*}; - -use crate::errors::ErrorType; -use crate::input::EitherBytes; -use crate::serializers::BytesMode; -use crate::tools::SchemaDict; +use speedate::TimestampUnit; const URL_SAFE_OPTIONAL_PADDING: GeneralPurpose = GeneralPurpose::new( &alphabet::URL_SAFE, @@ -21,6 +22,55 @@ const STANDARD_OPTIONAL_PADDING: GeneralPurpose = GeneralPurpose::new( GeneralPurposeConfig::new().with_decode_padding_mode(DecodePaddingMode::Indifferent), ); +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub enum TemporalUnitMode { + Seconds, + Milliseconds, + #[default] + Infer, +} + +impl FromStr for TemporalUnitMode { + type Err = PyErr; + + fn from_str(s: &str) -> Result { + match s { + "seconds" => Ok(Self::Seconds), + "milliseconds" => Ok(Self::Milliseconds), + "infer" => Ok(Self::Infer), + + s => py_schema_err!( + "Invalid temporal_unit_mode serialization mode: `{}`, expected seconds, milliseconds or infer", + s + ), + } + } +} + +impl TemporalUnitMode { + pub fn from_config(config: Option<&Bound<'_, PyDict>>) -> PyResult { + let Some(config_dict) = config else { + return Ok(Self::default()); + }; + let raw_mode = config_dict.get_as::>(intern!(config_dict.py(), "val_temporal_unit"))?; + let temporal_unit = raw_mode.map_or_else( + || Ok(TemporalUnitMode::default()), + |raw| TemporalUnitMode::from_str(&raw.to_cow()?), + )?; + Ok(temporal_unit) + } +} + +impl From for TimestampUnit { + fn from(value: TemporalUnitMode) -> Self { + match value { + TemporalUnitMode::Seconds => TimestampUnit::Second, + TemporalUnitMode::Milliseconds => TimestampUnit::Millisecond, + TemporalUnitMode::Infer => TimestampUnit::Infer, + } + } +} + #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] pub struct ValBytesMode { pub ser: BytesMode, diff --git a/src/validators/date.rs b/src/validators/date.rs index 6fec1f89e..6a5c1faa9 100644 --- a/src/validators/date.rs +++ b/src/validators/date.rs @@ -11,13 +11,14 @@ use crate::input::{EitherDate, Input}; use crate::validators::datetime::{NowConstraint, NowOp}; -use super::Exactness; use super::{BuildValidator, CombinedValidator, DefinitionsBuilder, ValidationState, Validator}; +use super::{Exactness, TemporalUnitMode}; #[derive(Debug, Clone)] pub struct DateValidator { strict: bool, constraints: Option, + val_temporal_unit: TemporalUnitMode, } impl BuildValidator for DateValidator { @@ -31,6 +32,7 @@ impl BuildValidator for DateValidator { Ok(Self { strict: is_strict(schema, config)?, constraints: DateConstraints::from_py(schema)?, + val_temporal_unit: TemporalUnitMode::from_config(config)?, } .into()) } @@ -46,12 +48,12 @@ impl Validator for DateValidator { state: &mut ValidationState<'_, 'py>, ) -> ValResult { let strict = state.strict_or(self.strict); - let date = match input.validate_date(strict) { + let date = match input.validate_date(strict, self.val_temporal_unit) { Ok(val_match) => val_match.unpack(state), // if the error was a parsing error, in lax mode we allow datetimes at midnight Err(line_errors @ ValError::LineErrors(..)) if !strict => { state.floor_exactness(Exactness::Lax); - date_from_datetime(input)?.ok_or(line_errors)? + date_from_datetime(input, self.val_temporal_unit)?.ok_or(line_errors)? } Err(otherwise) => return Err(otherwise), }; @@ -109,30 +111,34 @@ impl Validator for DateValidator { /// "exact date", e.g. has a zero time component. /// /// Ok(None) means that this is not relevant to dates (the input was not a datetime nor a string) -fn date_from_datetime<'py>(input: &(impl Input<'py> + ?Sized)) -> Result>, ValError> { - let either_dt = match input.validate_datetime(false, speedate::MicrosecondsPrecisionOverflowBehavior::Truncate) { - Ok(val_match) => val_match.into_inner(), - // if the error was a parsing error, update the error type from DatetimeParsing to DateFromDatetimeParsing - // and return it - Err(ValError::LineErrors(mut line_errors)) => { - if line_errors.iter_mut().fold(false, |has_parsing_error, line_error| { - if let ErrorType::DatetimeParsing { error, .. } = &mut line_error.error_type { - line_error.error_type = ErrorType::DateFromDatetimeParsing { - error: std::mem::take(error), - context: None, - }; - true - } else { - has_parsing_error +fn date_from_datetime<'py>( + input: &(impl Input<'py> + ?Sized), + mode: TemporalUnitMode, +) -> Result>, ValError> { + let either_dt = + match input.validate_datetime(false, speedate::MicrosecondsPrecisionOverflowBehavior::Truncate, mode) { + Ok(val_match) => val_match.into_inner(), + // if the error was a parsing error, update the error type from DatetimeParsing to DateFromDatetimeParsing + // and return it + Err(ValError::LineErrors(mut line_errors)) => { + if line_errors.iter_mut().fold(false, |has_parsing_error, line_error| { + if let ErrorType::DatetimeParsing { error, .. } = &mut line_error.error_type { + line_error.error_type = ErrorType::DateFromDatetimeParsing { + error: std::mem::take(error), + context: None, + }; + true + } else { + has_parsing_error + } + }) { + return Err(ValError::LineErrors(line_errors)); } - }) { - return Err(ValError::LineErrors(line_errors)); + return Ok(None); } - return Ok(None); - } - // for any other error, don't return it - Err(_) => return Ok(None), - }; + // for any other error, don't return it + Err(_) => return Ok(None), + }; let dt = either_dt.as_raw()?; let zero_time = Time { hour: 0, @@ -177,7 +183,7 @@ impl DateConstraints { fn convert_pydate(schema: &Bound<'_, PyDict>, key: &Bound<'_, PyString>) -> PyResult> { match schema.get_item(key)? { - Some(value) => match value.validate_date(false) { + Some(value) => match value.validate_date(false, TemporalUnitMode::default()) { Ok(v) => Ok(Some(v.into_inner().as_raw()?)), Err(_) => Err(PyValueError::new_err(format!( "'{key}' must be coercible to a date instance", diff --git a/src/validators/datetime.rs b/src/validators/datetime.rs index f6384cd2c..7fe0fc2f4 100644 --- a/src/validators/datetime.rs +++ b/src/validators/datetime.rs @@ -13,16 +13,17 @@ use crate::errors::ToErrorValue; use crate::errors::{py_err_string, ErrorType, ErrorTypeDefaults, ValError, ValResult}; use crate::input::{EitherDateTime, Input}; -use crate::tools::SchemaDict; - use super::Exactness; use super::{BuildValidator, CombinedValidator, DefinitionsBuilder, ValidationState, Validator}; +use crate::tools::SchemaDict; +use crate::validators::config::TemporalUnitMode; #[derive(Debug, Clone)] pub struct DateTimeValidator { strict: bool, constraints: Option, microseconds_precision: speedate::MicrosecondsPrecisionOverflowBehavior, + val_temporal_unit: TemporalUnitMode, } pub(crate) fn extract_microseconds_precision( @@ -51,6 +52,7 @@ impl BuildValidator for DateTimeValidator { strict: is_strict(schema, config)?, constraints: DateTimeConstraints::from_py(schema)?, microseconds_precision: extract_microseconds_precision(schema, config)?, + val_temporal_unit: TemporalUnitMode::from_config(config)?, } .into()) } @@ -66,7 +68,7 @@ impl Validator for DateTimeValidator { state: &mut ValidationState<'_, 'py>, ) -> ValResult { let strict = state.strict_or(self.strict); - let datetime = match input.validate_datetime(strict, self.microseconds_precision) { + let datetime = match input.validate_datetime(strict, self.microseconds_precision, self.val_temporal_unit) { Ok(val_match) => val_match.unpack(state), // if the error was a parsing error, in lax mode we allow dates and add the time 00:00:00 Err(line_errors @ ValError::LineErrors(..)) if !strict => { @@ -142,7 +144,7 @@ impl Validator for DateTimeValidator { /// In lax mode, if the input is not a datetime, we try parsing the input as a date and add the "00:00:00" time. /// Ok(None) means that this is not relevant to datetimes (the input was not a date nor a string) fn datetime_from_date<'py>(input: &(impl Input<'py> + ?Sized)) -> Result>, ValError> { - let either_date = match input.validate_date(false) { + let either_date = match input.validate_date(false, TemporalUnitMode::default()) { Ok(val_match) => val_match.into_inner(), // if the error was a parsing error, update the error type from DateParsing to DatetimeFromDateParsing Err(ValError::LineErrors(mut line_errors)) => { @@ -211,7 +213,11 @@ impl DateTimeConstraints { fn py_datetime_as_datetime(schema: &Bound<'_, PyDict>, key: &Bound<'_, PyString>) -> PyResult> { match schema.get_item(key)? { - Some(value) => match value.validate_datetime(false, MicrosecondsPrecisionOverflowBehavior::Truncate) { + Some(value) => match value.validate_datetime( + false, + MicrosecondsPrecisionOverflowBehavior::Truncate, + TemporalUnitMode::default(), + ) { Ok(v) => Ok(Some(v.into_inner().as_raw()?)), Err(_) => Err(PyValueError::new_err(format!( "'{key}' must be coercible to a datetime instance", diff --git a/src/validators/mod.rs b/src/validators/mod.rs index 2fd79c495..3d0416d8d 100644 --- a/src/validators/mod.rs +++ b/src/validators/mod.rs @@ -15,7 +15,7 @@ use crate::input::{Input, InputType, StringMapping}; use crate::py_gc::PyGcTraverse; use crate::recursion_guard::RecursionState; use crate::tools::SchemaDict; -pub(crate) use config::ValBytesMode; +pub(crate) use config::{TemporalUnitMode, ValBytesMode}; mod any; mod arguments; diff --git a/tests/validators/test_date.py b/tests/validators/test_date.py index 8300a15ad..3b53d3751 100644 --- a/tests/validators/test_date.py +++ b/tests/validators/test_date.py @@ -303,3 +303,34 @@ def test_date_past_future_today(): assert v.isinstance_python(today) is False assert v.isinstance_python(today - timedelta(days=1)) is False assert v.isinstance_python(today + timedelta(days=1)) is True + + +@pytest.mark.parametrize( + 'val_temporal_unit, input_value, expected', + [ + # 'seconds' mode: treat as seconds since epoch + ('seconds', 1654646400, date(2022, 6, 8)), + ('seconds', '1654646400', date(2022, 6, 8)), + ('seconds', 1654646400.0, date(2022, 6, 8)), + ('seconds', 8640000000.0, date(2243, 10, 17)), + ('seconds', 92534400000.0, date(4902, 4, 20)), + # 'milliseconds' mode: treat as milliseconds since epoch + ('milliseconds', 1654646400000, date(2022, 6, 8)), + ('milliseconds', '1654646400000', date(2022, 6, 8)), + ('milliseconds', 1654646400000.0, date(2022, 6, 8)), + ('milliseconds', 8640000000.0, date(1970, 4, 11)), + ('milliseconds', 92534400000.0, date(1972, 12, 7)), + # 'infer' mode: large numbers are ms, small are s + ('infer', 1654646400, date(2022, 6, 8)), + ('infer', 1654646400000, date(2022, 6, 8)), + ('infer', 8640000000.0, date(2243, 10, 17)), + ('infer', 92534400000.0, date(1972, 12, 7)), + ], +) +def test_val_temporal_unit_date(val_temporal_unit, input_value, expected): + v = SchemaValidator( + cs.date_schema(), + config={'val_temporal_unit': val_temporal_unit}, + ) + output = v.validate_python(input_value) + assert output == expected diff --git a/tests/validators/test_datetime.py b/tests/validators/test_datetime.py index 5e319dc23..495759576 100644 --- a/tests/validators/test_datetime.py +++ b/tests/validators/test_datetime.py @@ -47,6 +47,10 @@ def test_constraints_schema_validation() -> None: (Decimal('1654646400.1234564'), datetime(2022, 6, 8, 0, 0, 0, 123456, tzinfo=timezone.utc)), (Decimal('1654646400.1234568'), datetime(2022, 6, 8, 0, 0, 0, 123457, tzinfo=timezone.utc)), ('1654646400.1234568', datetime(2022, 6, 8, 0, 0, 0, 123457, tzinfo=timezone.utc)), + ( + Decimal('1654646400123.456'), + datetime(2022, 6, 8, 0, 0, 0, 123456, tzinfo=timezone.utc), + ), (253_402_300_800_000, Err('should be a valid datetime, dates after 9999 are not supported as unix timestamps')), ( -80_000_000_000_000, @@ -515,3 +519,44 @@ def test_tz_cmp() -> None: assert validated1 > validated2 assert validated2 < validated1 + + +@pytest.mark.parametrize( + 'val_temporal_unit, input_value, expected', + [ + # 'seconds' mode: treat as seconds since epoch + ('seconds', 1654646400, datetime(2022, 6, 8, tzinfo=timezone.utc)), + ('seconds', '1654646400', datetime(2022, 6, 8, tzinfo=timezone.utc)), + ('seconds', 1654646400.123456, datetime(2022, 6, 8, 0, 0, 0, 123456, tzinfo=timezone.utc)), + ('seconds', 8640000000.0, datetime(2243, 10, 17, tzinfo=timezone.utc)), + ('seconds', 92534400000.0, datetime(4902, 4, 20, tzinfo=timezone.utc)), + # 'milliseconds' mode: treat as milliseconds since epoch + ('milliseconds', 1654646400, datetime(1970, 1, 20, 3, 37, 26, 400000, tzinfo=timezone.utc)), + ('milliseconds', 1654646400123, datetime(2022, 6, 8, 0, 0, 0, 123000, tzinfo=timezone.utc)), + ('milliseconds', '1654646400123', datetime(2022, 6, 8, 0, 0, 0, 123000, tzinfo=timezone.utc)), + ('milliseconds', 8640000000.0, datetime(1970, 4, 11, tzinfo=timezone.utc)), + ('milliseconds', 92534400000.0, datetime(1972, 12, 7, tzinfo=timezone.utc)), + ( + 'milliseconds', + 1654646400123.456, + datetime(2022, 6, 8, 0, 0, 0, 123456, tzinfo=timezone.utc), + ), + # 'infer' mode: large numbers are ms, small are s + ('infer', 1654646400, datetime(2022, 6, 8, tzinfo=timezone.utc)), + ('infer', 1654646400123, datetime(2022, 6, 8, 0, 0, 0, 123000, tzinfo=timezone.utc)), + ( + 'infer', + 1654646400123.456, + datetime(2022, 6, 8, 0, 0, 0, 123456, tzinfo=timezone.utc), + ), + ('infer', 8640000000.0, datetime(2243, 10, 17, tzinfo=timezone.utc)), + ('infer', 92534400000.0, datetime(1972, 12, 7, tzinfo=timezone.utc)), + ], +) +def test_val_temporal_unit_datetime(val_temporal_unit, input_value, expected): + v = SchemaValidator( + cs.datetime_schema(), + config={'val_temporal_unit': val_temporal_unit}, + ) + output = v.validate_python(input_value) + assert output == expected