Skip to content

Commit

Permalink
Dates rust type (#82)
Browse files Browse the repository at this point in the history
* using speedate types for all dates

* using enum
  • Loading branch information
samuelcolvin authored Jun 14, 2022
1 parent b25e2bb commit d0eb564
Show file tree
Hide file tree
Showing 11 changed files with 405 additions and 247 deletions.
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ enum_dispatch = "0.3.8"
serde = "1.0.137"
indexmap = "1.8.1"
mimalloc = { version = "0.1.29", default-features = false, optional = true }
speedate = "0.3.0"
speedate = "0.4.0"

[lib]
name = "_pydantic_core"
Expand Down
172 changes: 172 additions & 0 deletions src/input/datetime.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use pyo3::types::{PyDate, PyDateTime, PyDelta, PyTzInfo};
use speedate::{Date, DateTime};
use strum::EnumMessage;

use super::Input;
use crate::errors::{context, err_val_error, ErrorKind, InputValue, ValResult};

pub enum EitherDate<'a> {
Speedate(Date),
Python(&'a PyDate),
}

impl<'a> EitherDate<'a> {
pub fn as_speedate(&self) -> PyResult<Date> {
match self {
Self::Speedate(date) => Ok(date.clone()),
Self::Python(py_date) => {
let date_str: &str = py_date.str()?.extract()?;
match Date::parse_str(date_str) {
Ok(date) => Ok(date),
Err(err) => {
let error_description = err.get_documentation().unwrap_or_default();
let msg = format!("Unable to parse date {}, error: {}", date_str, error_description);
Err(PyValueError::new_err(msg))
}
}
}
}
}

pub fn as_python(&self, py: Python<'a>) -> PyResult<&'a PyDate> {
match self {
Self::Speedate(date) => PyDate::new(py, date.year as i32, date.month, date.day),
Self::Python(date) => Ok(date),
}
}
}

pub enum EitherDateTime<'a> {
Speedate(DateTime),
Python(&'a PyDateTime),
}

impl<'a> EitherDateTime<'a> {
pub fn as_speedate(&self) -> PyResult<DateTime> {
match self {
Self::Speedate(dt) => Ok(dt.clone()),
Self::Python(py_dt) => {
let dt_str: &str = py_dt.str()?.extract()?;
match DateTime::parse_str(dt_str) {
Ok(dt) => Ok(dt),
Err(err) => {
let error_description = err.get_documentation().unwrap_or_default();
let msg = format!("Unable to parse datetime {}, error: {}", dt_str, error_description);
Err(PyValueError::new_err(msg))
}
}
}
}
}

pub fn as_python(&self, py: Python<'a>) -> PyResult<&'a PyDateTime> {
match self {
Self::Speedate(datetime) => {
let tz: Option<PyObject> = match datetime.offset {
Some(offset) => {
let tz_info = TzClass::new(offset);
Some(Py::new(py, tz_info)?.to_object(py))
}
None => None,
};
PyDateTime::new(
py,
datetime.date.year as i32,
datetime.date.month,
datetime.date.day,
datetime.time.hour,
datetime.time.minute,
datetime.time.second,
datetime.time.microsecond,
tz.as_ref(),
)
}
Self::Python(dt) => Ok(dt),
}
}
}

pub fn bytes_as_date<'a>(input: &'a dyn Input, bytes: &[u8]) -> ValResult<'a, EitherDate<'a>> {
match Date::parse_bytes(bytes) {
Ok(date) => Ok(EitherDate::Speedate(date)),
Err(err) => {
err_val_error!(
input_value = InputValue::InputRef(input),
kind = ErrorKind::DateParsing,
context = context!("parsing_error" => err.get_documentation().unwrap_or_default())
)
}
}
}

pub fn bytes_as_datetime<'a, 'b>(input: &'a dyn Input, bytes: &'b [u8]) -> ValResult<'a, EitherDateTime<'a>> {
match DateTime::parse_bytes(bytes) {
Ok(dt) => Ok(EitherDateTime::Speedate(dt)),
Err(err) => {
err_val_error!(
input_value = InputValue::InputRef(input),
kind = ErrorKind::DateTimeParsing,
context = context!("parsing_error" => err.get_documentation().unwrap_or_default())
)
}
}
}

pub fn int_as_datetime(input: &dyn Input, timestamp: i64, timestamp_microseconds: u32) -> ValResult<EitherDateTime> {
match DateTime::from_timestamp(timestamp, timestamp_microseconds) {
Ok(dt) => Ok(EitherDateTime::Speedate(dt)),
Err(err) => {
err_val_error!(
input_value = InputValue::InputRef(input),
kind = ErrorKind::DateTimeParsing,
context = context!("parsing_error" => err.get_documentation().unwrap_or_default())
)
}
}
}

pub fn float_as_datetime(input: &dyn Input, timestamp: f64) -> ValResult<EitherDateTime> {
let microseconds = timestamp.fract().abs() * 1_000_000.0;
if microseconds % 1.0 > 1e-3 {
return err_val_error!(
input_value = InputValue::InputRef(input),
kind = ErrorKind::DateTimeParsing,
// message copied from speedate
context = context!("parsing_error" => "second fraction value is more than 6 digits long")
);
}
int_as_datetime(input, timestamp.floor() as i64, microseconds as u32)
}

#[pyclass(module = "pydantic_core._pydantic_core", extends = PyTzInfo)]
#[derive(Debug, Clone)]
struct TzClass {
seconds: i32,
}

#[pymethods]
impl TzClass {
#[new]
fn new(seconds: i32) -> Self {
Self { seconds }
}

fn utcoffset<'p>(&self, py: Python<'p>, _dt: &PyDateTime) -> PyResult<&'p PyDelta> {
PyDelta::new(py, 0, self.seconds, 0, true)
}

fn tzname(&self, _py: Python<'_>, _dt: &PyDateTime) -> String {
if self.seconds == 0 {
"UTC".to_string()
} else {
let mins = self.seconds / 60;
format!("{:+03}:{:02}", mins / 60, (mins % 60).abs())
}
}

fn dst(&self, _py: Python<'_>, _dt: &PyDateTime) -> Option<&PyDelta> {
None
}
}
16 changes: 8 additions & 8 deletions src/input/input_abstract.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use std::fmt;

use pyo3::prelude::*;
use pyo3::types::{PyDate, PyDateTime, PyType};
use pyo3::types::PyType;

use crate::errors::ValResult;

use super::datetime::{EitherDate, EitherDateTime};
use super::{GenericMapping, GenericSequence, ToLocItem, ToPy};

pub trait Input: fmt::Debug + ToPy + ToLocItem {
Expand Down Expand Up @@ -46,15 +46,15 @@ pub trait Input: fmt::Debug + ToPy + ToLocItem {
self.strict_set()
}

fn strict_date<'data>(&'data self, py: Python<'data>) -> ValResult<&'data PyDate>;
fn strict_date(&self) -> ValResult<EitherDate>;

fn lax_date<'data>(&'data self, py: Python<'data>) -> ValResult<&'data PyDate> {
self.strict_date(py)
fn lax_date(&self) -> ValResult<EitherDate> {
self.strict_date()
}

fn strict_datetime<'data>(&'data self, py: Python<'data>) -> ValResult<&'data PyDateTime>;
fn strict_datetime(&self) -> ValResult<EitherDateTime>;

fn lax_datetime<'data>(&'data self, py: Python<'data>) -> ValResult<&'data PyDateTime> {
self.strict_datetime(py)
fn lax_datetime(&self) -> ValResult<EitherDateTime> {
self.strict_datetime()
}
}
29 changes: 12 additions & 17 deletions src/input/input_json.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
use pyo3::prelude::*;
use pyo3::types::{PyDate, PyDateTime, PyType};
use pyo3::types::PyType;

use crate::errors::{err_val_error, ErrorKind, InputValue, ValResult};

use super::datetime::{
bytes_as_date, bytes_as_datetime, float_as_datetime, int_as_datetime, EitherDate, EitherDateTime,
};
use super::generics::{GenericMapping, GenericSequence};
use super::input_abstract::Input;
use super::parse_json::JsonInput;
use super::shared::{
bytes_as_date, bytes_as_datetime, date_as_py_date, datetime_as_py_datetime, float_as_datetime, float_as_int,
int_as_bool, int_as_datetime, str_as_bool, str_as_int,
};
use super::shared::{float_as_int, int_as_bool, str_as_bool, str_as_int};

impl Input for JsonInput {
fn is_none(&self) -> bool {
Expand Down Expand Up @@ -119,24 +118,20 @@ impl Input for JsonInput {
}
}

fn strict_date<'data>(&'data self, py: Python<'data>) -> ValResult<&'data PyDate> {
fn strict_date(&self) -> ValResult<EitherDate> {
match self {
JsonInput::String(v) => {
let date = bytes_as_date(self, v.as_bytes())?;
date_as_py_date!(py, date)
}
JsonInput::String(v) => bytes_as_date(self, v.as_bytes()),
_ => err_val_error!(input_value = InputValue::InputRef(self), kind = ErrorKind::DateType),
}
}

fn strict_datetime<'data>(&'data self, py: Python<'data>) -> ValResult<&'data PyDateTime> {
let dt = match self {
fn strict_datetime(&self) -> ValResult<EitherDateTime> {
match self {
JsonInput::String(v) => bytes_as_datetime(self, v.as_bytes()),
JsonInput::Int(v) => int_as_datetime(self, *v, 0),
JsonInput::Float(v) => float_as_datetime(self, *v),
_ => err_val_error!(input_value = InputValue::InputRef(self), kind = ErrorKind::DateTimeType),
}?;
datetime_as_py_datetime!(py, dt)
}
}
}

Expand Down Expand Up @@ -214,12 +209,12 @@ impl Input for String {
}

#[no_coverage]
fn strict_date<'data>(&'data self, _py: Python<'data>) -> ValResult<&'data PyDate> {
fn strict_date(&self) -> ValResult<EitherDate> {
err_val_error!(input_value = InputValue::InputRef(self), kind = ErrorKind::DateType)
}

#[no_coverage]
fn strict_datetime<'data>(&'data self, _py: Python<'data>) -> ValResult<&'data PyDateTime> {
fn strict_datetime(&self) -> ValResult<EitherDateTime> {
err_val_error!(input_value = InputValue::InputRef(self), kind = ErrorKind::DateTimeType)
}
}
49 changes: 20 additions & 29 deletions src/input/input_python.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,13 @@ use pyo3::types::{
};

use crate::errors::{as_internal, err_val_error, ErrorKind, InputValue, ValResult};
use crate::input::shared::bytes_as_datetime;

use super::datetime::{
bytes_as_date, bytes_as_datetime, float_as_datetime, int_as_datetime, EitherDate, EitherDateTime,
};
use super::generics::{GenericMapping, GenericSequence};
use super::input_abstract::Input;
use super::shared::{
bytes_as_date, date_as_py_date, datetime_as_py_datetime, float_as_datetime, float_as_int, int_as_bool,
int_as_datetime, str_as_bool, str_as_int,
};
use super::shared::{float_as_int, int_as_bool, str_as_bool, str_as_int};

impl Input for PyAny {
fn is_none(&self) -> bool {
Expand Down Expand Up @@ -211,62 +210,54 @@ impl Input for PyAny {
}
}

fn strict_date<'data>(&'data self, _py: Python<'data>) -> ValResult<&'data PyDate> {
fn strict_date(&self) -> ValResult<EitherDate> {
if self.cast_as::<PyDateTime>().is_ok() {
// have to check if it's a datetime first, otherwise the line below converts to a date
err_val_error!(input_value = InputValue::InputRef(self), kind = ErrorKind::DateType)
} else if let Ok(date) = self.cast_as::<PyDate>() {
Ok(date)
Ok(EitherDate::Python(date))
} else {
err_val_error!(input_value = InputValue::InputRef(self), kind = ErrorKind::DateType)
}
}

fn lax_date<'data>(&'data self, py: Python<'data>) -> ValResult<&'data PyDate> {
fn lax_date(&self) -> ValResult<EitherDate> {
if self.cast_as::<PyDateTime>().is_ok() {
// have to check if it's a datetime first, otherwise the line below converts to a date
// even if we later try coercion from a datetime, we don't want to return a datetime now
return err_val_error!(input_value = InputValue::InputRef(self), kind = ErrorKind::DateType);
err_val_error!(input_value = InputValue::InputRef(self), kind = ErrorKind::DateType)
} else if let Ok(date) = self.cast_as::<PyDate>() {
return Ok(date);
}

if let Ok(str) = self.extract::<String>() {
let date = bytes_as_date(self, str.as_bytes())?;
date_as_py_date!(py, date)
Ok(EitherDate::Python(date))
} else if let Ok(str) = self.extract::<String>() {
bytes_as_date(self, str.as_bytes())
} else if let Ok(py_bytes) = self.cast_as::<PyBytes>() {
let date = bytes_as_date(self, py_bytes.as_bytes())?;
date_as_py_date!(py, date)
bytes_as_date(self, py_bytes.as_bytes())
} else {
err_val_error!(input_value = InputValue::InputRef(self), kind = ErrorKind::DateType)
}
}

fn strict_datetime<'data>(&'data self, _py: Python<'data>) -> ValResult<&'data PyDateTime> {
fn strict_datetime(&self) -> ValResult<EitherDateTime> {
if let Ok(dt) = self.cast_as::<PyDateTime>() {
Ok(dt)
Ok(EitherDateTime::Python(dt))
} else {
err_val_error!(input_value = InputValue::InputRef(self), kind = ErrorKind::DateTimeType)
}
}

fn lax_datetime<'data>(&'data self, py: Python<'data>) -> ValResult<&'data PyDateTime> {
fn lax_datetime(&self) -> ValResult<EitherDateTime> {
if let Ok(dt) = self.cast_as::<PyDateTime>() {
Ok(dt)
Ok(EitherDateTime::Python(dt))
} else if let Ok(str) = self.extract::<String>() {
let dt = bytes_as_datetime(self, str.as_bytes())?;
datetime_as_py_datetime!(py, dt)
bytes_as_datetime(self, str.as_bytes())
} else if let Ok(py_bytes) = self.cast_as::<PyBytes>() {
let dt = bytes_as_datetime(self, py_bytes.as_bytes())?;
datetime_as_py_datetime!(py, dt)
bytes_as_datetime(self, py_bytes.as_bytes())
} else if self.cast_as::<PyBool>().is_ok() {
err_val_error!(input_value = InputValue::InputRef(self), kind = ErrorKind::DateTimeType)
} else if let Ok(int) = self.extract::<i64>() {
let dt = int_as_datetime(self, int, 0)?;
datetime_as_py_datetime!(py, dt)
int_as_datetime(self, int, 0)
} else if let Ok(float) = self.extract::<f64>() {
let dt = float_as_datetime(self, float)?;
datetime_as_py_datetime!(py, dt)
float_as_datetime(self, float)
} else {
err_val_error!(input_value = InputValue::InputRef(self), kind = ErrorKind::DateTimeType)
}
Expand Down
Loading

0 comments on commit d0eb564

Please sign in to comment.