Skip to content

Commit

Permalink
Merge pull request #333 from nyx-space/gh-185-py-datetime
Browse files Browse the repository at this point in the history
Add Python datetime interop
  • Loading branch information
ChristopherRabotin authored Oct 10, 2024
2 parents 1033bb0 + 73942e3 commit 70b3692
Show file tree
Hide file tree
Showing 4 changed files with 70 additions and 3 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@ jobs:
python-version: '3.11'
check-latest: false
allow-prereleases: false

- name: Fix openssl regression
run: cargo update openssl-src --precise 300.3.1+3.3.1

- name: Build wheels
uses: PyO3/maturin-action@v1
with:
Expand Down
46 changes: 45 additions & 1 deletion src/epoch/python.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use core::str::FromStr;
use crate::epoch::leap_seconds_file::LeapSecondsFile;
use pyo3::prelude::*;
use pyo3::pyclass::CompareOp;
use pyo3::types::PyType;
use pyo3::types::{PyDateAccess, PyDateTime, PyTimeAccess, PyType, PyTzInfoAccess};

#[pymethods]
impl Epoch {
Expand Down Expand Up @@ -502,4 +502,48 @@ impl Epoch {
CompareOp::Ge => *self >= other,
}
}

/// Returns a Python datetime object from this Epoch (truncating the nanoseconds away)
fn todatetime<'py>(&self, py: Python<'py>) -> Result<Bound<'py, PyDateTime>, PyErr> {
let (y, mm, dd, hh, min, s, nanos) =
Epoch::compute_gregorian(self.duration, TimeScale::UTC);

let datetime = PyDateTime::new_bound(py, y, mm, dd, hh, min, s, nanos / 1_000, None)?;

Ok(datetime)
}

/// Builds an Epoch in UTC from the provided datetime after timezone correction if any is present.
#[classmethod]
fn fromdatetime(
_cls: &Bound<'_, PyType>,
dt: &Bound<'_, PyAny>,
) -> Result<Self, HifitimeError> {
let dt = dt
.downcast::<PyDateTime>()
.map_err(|e| HifitimeError::PythonError {
reason: e.to_string(),
})?;

// If the user tries to convert a timezone aware datetime into a naive one,
// we return a hard error. We could silently remove tzinfo, or assume local timezone
// and do a conversion, but better leave this decision to the user of the library.
let has_tzinfo = dt.get_tzinfo_bound().is_some();
if has_tzinfo {
return Err(HifitimeError::PythonError {
reason: "expected a datetime without tzinfo, call my_datetime.replace(tzinfo=None)"
.to_string(),
});
}

Epoch::maybe_from_gregorian_utc(
dt.get_year(),
dt.get_month().into(),
dt.get_day().into(),
dt.get_hour().into(),
dt.get_minute().into(),
dt.get_second().into(),
dt.get_microsecond() * 1_000,
)
}
}
5 changes: 5 additions & 0 deletions src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ pub enum HifitimeError {
Duration {
source: DurationError,
},
#[cfg(feature = "python")]
#[snafu(display("python interop error: {reason}"))]
PythonError {
reason: String,
},
}

#[cfg_attr(kani, derive(kani::Arbitrary))]
Expand Down
20 changes: 18 additions & 2 deletions tests/python/test_epoch.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from hifitime import Duration, Epoch, HifitimeError, ParsingError, TimeScale, TimeSeries, Unit
from datetime import datetime
from datetime import datetime, timezone
import pickle


Expand Down Expand Up @@ -108,4 +108,20 @@ def test_regression_gh249():
e = Epoch.init_from_gregorian(year=2022, month=3, day=1, hour=1, minute=1, second=59, nanos=1, time_scale=TimeScale.GPST)
assert e.strftime("%Y %m %d %H %M %S %f %T") == "2022 03 01 01 01 59 000000001 GPST"
e = Epoch.init_from_gregorian(year=2022, month=3, day=1, hour=1, minute=1, second=59, nanos=1, time_scale=TimeScale.UTC)
assert e.strftime("%Y %m %d %H %M %S %f %T") == "2022 03 01 01 01 59 000000001 UTC"
assert e.strftime("%Y %m %d %H %M %S %f %T") == "2022 03 01 01 01 59 000000001 UTC"

def test_interop():
hifinow = Epoch.system_now()
lofinow = hifinow.todatetime()
hifirtn = Epoch.fromdatetime(lofinow)
assert hifirtn.timedelta(hifinow).abs() < Unit.Microsecond * 1
# Now test with timezone, expect an error
tz_datetime = datetime(2023, 10, 8, 15, 30, tzinfo=timezone.utc)
try:
Epoch.fromdatetime(tz_datetime)
except Exception as e:
print(e)
else:
assert False, "tz aware dt did not fail"
# Repeat after the strip
assert Epoch.fromdatetime(tz_datetime.replace(tzinfo=None)) == Epoch("2023-10-08 15:30:00")

0 comments on commit 70b3692

Please sign in to comment.