Skip to content
Merged
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
2 changes: 1 addition & 1 deletion generate_stubs.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ def class_stubs(
except ValueError as e:
if "no signature found" not in str(e):
raise ValueError(
f"Error while parsing signature of {cls_name}.__init_"
f"Error while parsing signature of {cls_name}.__init__"
) from e
elif (
member_value == OBJECT_MEMBERS.get(member_name)
Expand Down
51 changes: 46 additions & 5 deletions hifitime.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,10 @@ This may be useful for time keeping devices that use BDT as a time source."""
"""Initialize an Epoch from the number of seconds since the BeiDou Time Epoch,
defined as January 1st 2006 (cf. <https://gssc.esa.int/navipedia/index.php/Time_References_in_GNSS>)."""

@staticmethod
def from_datetime(dt: datetime.datetime) -> Epoch:
"""Builds an Epoch in UTC from the provided datetime after timezone correction if any is present."""

@staticmethod
def from_et_duration(duration_since_j2000: Duration) -> Epoch:
"""Initialize an Epoch from the Ephemeris Time duration past 2000 JAN 01 (J2000 reference)"""
Expand Down Expand Up @@ -508,6 +512,10 @@ In fact, SPICE algorithm is precise +/- 30 microseconds for a century whereas ES
def from_unix_seconds(seconds: float) -> Epoch:
"""Initialize an Epoch from the provided UNIX second timestamp since UTC midnight 1970 January 01."""

@staticmethod
def from_ut1_duration(duration: Duration, provider: Ut1Provider) -> Epoch:
"""Initialize a new Epoch from a duration in UT1"""

@staticmethod
def from_utc_days(days: float) -> Epoch:
"""Initialize an Epoch from the provided UTC days since 1900 January 01 at midnight"""
Expand All @@ -518,7 +526,7 @@ In fact, SPICE algorithm is precise +/- 30 microseconds for a century whereas ES

@staticmethod
def fromdatetime(dt: datetime.datetime) -> Epoch:
"""Builds an Epoch in UTC from the provided datetime after timezone correction if any is present."""
"""Builds an Epoch in UTC from the provided datetime. Datetime must either NOT have any timezone, or timezone MUST be UTC."""

def hours(self) -> int:
"""Returns the hours of the Gregorian representation of this epoch in the time scale it was initialized in."""
Expand Down Expand Up @@ -942,6 +950,10 @@ NOTE: This function will return an error if the centuries past GST time are not
def to_bdt_seconds(self) -> float:
"""Returns seconds past BDT (BeiDou) Time Epoch"""

def to_datetime(self, set_tz: bool=None) -> datetime.datetime:
"""Returns a Python datetime object from this Epoch (truncating the nanoseconds away)
If set_tz is True, then this will return a time zone aware datetime object"""

def to_duration_in_time_scale(self, ts: TimeScale) -> Duration:
"""Returns this epoch with respect to the provided time scale.
This is needed to correctly perform duration conversions in dynamical time scales (e.g. TDB)."""
Expand Down Expand Up @@ -1167,6 +1179,12 @@ this borrows an Epoch and returns an owned Epoch."""
def to_unix_seconds(self) -> float:
"""Returns the number seconds since the UNIX epoch defined 01 Jan 1970 midnight UTC."""

def to_ut1(self, provider: Ut1Provider) -> Epoch:
"""Convert this epoch to Ut1"""

def to_ut1_duration(self, provider: Ut1Provider) -> Duration:
"""Returns this time in a Duration past J1900 counted in UT1"""

def to_utc(self, unit: Unit) -> float:
"""Returns the number of UTC seconds since the TAI epoch"""

Expand All @@ -1179,8 +1197,12 @@ this borrows an Epoch and returns an owned Epoch."""
def to_utc_seconds(self) -> float:
"""Returns the number of UTC seconds since the TAI epoch"""

def todatetime(self) -> datetime.datetime:
"""Returns a Python datetime object from this Epoch (truncating the nanoseconds away)"""
def todatetime(self, set_tz: bool=None) -> datetime.datetime:
"""Returns a Python datetime object from this Epoch (truncating the nanoseconds away).
If set_tz is True, then this will return a time zone aware datetime object"""

def ut1_offset(self, provider: Ut1Provider) -> Duration:
"""Get the accumulated offset between this epoch and UT1."""

def weekday(self) -> Weekday:
"""Returns weekday (uses the TAI representation for this calculation)."""
Expand Down Expand Up @@ -1436,9 +1458,11 @@ set self.__traceback__ to tb and return self."""
@typing.final
class Polynomial:
"""Interpolation [Polynomial] used for example in [TimeScale]
maintenance, precise monitoring or conversions.
maintenance, precise monitoring or conversions."""

(Python documentation hints)"""
def __init__(self) -> None:
"""Interpolation [Polynomial] used for example in [TimeScale]
maintenance, precise monitoring or conversions."""

def correction_duration(self, time_interval: Duration) -> Duration:
"""Calculate the correction (as [Duration] once again) from [Self] and given
Expand Down Expand Up @@ -1485,6 +1509,9 @@ the interpolation time interval"""
class TimeScale:
"""Enum of the different time systems available"""

def __init__(self) -> None:
"""Enum of the different time systems available"""

def uses_leap_seconds(self) -> bool:
"""Returns true if self takes leap seconds into account"""

Expand Down Expand Up @@ -1568,6 +1595,9 @@ class TimeSeries:
class Unit:
"""An Enum to perform time unit conversions."""

def __init__(self) -> None:
"""An Enum to perform time unit conversions."""

def from_seconds(self):...

def in_seconds(self):...
Expand Down Expand Up @@ -1626,16 +1656,27 @@ class Unit:
@typing.final
class Ut1Provider:
"""A structure storing all of the TAI-UT1 data"""
data: list
iter_pos: int

def __init__(self) -> None:
"""A structure storing all of the TAI-UT1 data"""

def as_list(self) -> list:
"""Returns the list of Delta TAI-UT1 values"""

@staticmethod
def from_eop_file(path: str) -> Ut1Provider:
"""Builds a UT1 provider from the provided path to an EOP file."""

def __repr__(self) -> str:
"""Return repr(self)."""

@typing.final
class Weekday:

def __init__(self):...

def __eq__(self, value: typing.Any) -> bool:
"""Return self==value."""

Expand Down
2 changes: 1 addition & 1 deletion src/epoch/leap_seconds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ impl LatestLeapSeconds {
)
.collect::<Vec<()>>();

Ok(ls_diff.len() == 0)
Ok(ls_diff.is_empty())
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/epoch/leap_seconds_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ impl LeapSecondsFile {
Self::from_content(response)
}
Err(Error::StatusCode(code)) => Err(HifitimeError::Parse {
source: ParsingError::DownloadError { code: code },
source: ParsingError::DownloadError { code },
details: "server returned an error",
}),
Err(_) => Err(HifitimeError::Parse {
Expand Down
102 changes: 82 additions & 20 deletions src/epoch/python.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ use core::str::FromStr;
use crate::epoch::leap_seconds_file::LeapSecondsFile;
use pyo3::prelude::*;
use pyo3::pyclass::CompareOp;
use pyo3::types::{PyDateAccess, PyDateTime, PyTimeAccess, PyType, PyTzInfoAccess};
use pyo3::types::{
PyDateAccess, PyDateTime, PyDelta, PyDeltaAccess, PyTimeAccess, PyType, PyTzInfo,
PyTzInfoAccess,
};

#[pymethods]
impl Epoch {
Expand Down Expand Up @@ -858,7 +861,7 @@ impl Epoch {
/// :type format_str: str
/// :rtype: Epoch
fn strptime(_cls: &Bound<'_, PyType>, epoch_str: String, format_str: String) -> PyResult<Self> {
Self::from_format_str(&epoch_str, &format_str).map_err(|e| PyErr::from(e))
Self::from_format_str(&epoch_str, &format_str).map_err(PyErr::from)
}

/// Formats the epoch according to the given format string. Supports a subset of C89 and hifitime-specific format codes. Refer to <https://docs.rs/hifitime/latest/hifitime/efmt/format/struct.Format.html> for available format options.
Expand Down Expand Up @@ -960,18 +963,49 @@ impl Epoch {
}
}

/// Returns a Python datetime object from this Epoch (truncating the nanoseconds away)
/// Returns a Python datetime object from this Epoch (truncating the nanoseconds away).
/// If set_tz is True, then this will return a time zone aware datetime object
/// :type set_tz: bool, optional
/// :rtype: datetime.datetime
fn todatetime<'py>(&self, py: Python<'py>) -> Result<Bound<'py, PyDateTime>, PyErr> {
#[pyo3(signature=(set_tz=None))]
fn todatetime<'py>(
&self,
py: Python<'py>,
set_tz: Option<bool>,
) -> Result<Bound<'py, PyDateTime>, PyErr> {
let (y, mm, dd, hh, min, s, nanos) =
Epoch::compute_gregorian(self.to_utc_duration(), TimeScale::UTC);

let datetime = PyDateTime::new(py, y, mm, dd, hh, min, s, nanos / 1_000, None)?;
let tz_opt = if let Some(tz) = set_tz {
if tz {
Some(PyTzInfo::utc(py)?)
} else {
None
}
} else {
None
};

let datetime =
PyDateTime::new(py, y, mm, dd, hh, min, s, nanos / 1_000, tz_opt.as_deref())?;

Ok(datetime)
}

/// Builds an Epoch in UTC from the provided datetime after timezone correction if any is present.
/// Returns a Python datetime object from this Epoch (truncating the nanoseconds away)
/// If set_tz is True, then this will return a time zone aware datetime object
/// :type set_tz: bool, optional
/// :rtype: datetime.datetime
#[pyo3(signature=(set_tz=None))]
fn to_datetime<'py>(
&self,
py: Python<'py>,
set_tz: Option<bool>,
) -> Result<Bound<'py, PyDateTime>, PyErr> {
self.todatetime(py, set_tz)
}

/// Builds an Epoch in UTC from the provided datetime. Datetime must either NOT have any timezone, or timezone MUST be UTC.
/// :type dt: datetime.datetime
/// :rtype: Epoch
#[classmethod]
Expand All @@ -985,28 +1019,56 @@ impl Epoch {
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().is_some();
if has_tzinfo {
return Err(HifitimeError::PythonError {
reason: "expected a datetime without tzinfo, call my_datetime.replace(tzinfo=None)"
.to_string(),
});
if let Some(tzinfo) = dt.get_tzinfo() {
// Timezone is present, let's check if it's UTC.
// `utcoffset` returns the offset from UTC. For a UTC datetime, this must be zero.
let offset_any = tzinfo.call_method1("utcoffset", (dt,))?;

if offset_any.is_none() {
// This case should not happen for a timezone-aware object that returns a tzinfo, but we'll handle it.
return Err(HifitimeError::PythonError {
reason: "datetime has tzinfo but utcoffset() returned None".to_string(),
});
}

// The result should be a timedelta.
let offset_delta =
offset_any
.downcast::<PyDelta>()
.map_err(|e| HifitimeError::PythonError {
reason: format!("utcoffset did not return a timedelta: {e}"),
})?;

if offset_delta.get_seconds().abs() > 0 {
return Err(HifitimeError::PythonError {
reason: "only UTC timezone is supported for datetime conversion".to_string(),
});
}
// If we are here, offset is zero, so we can proceed.
}

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_month(),
dt.get_day(),
dt.get_hour(),
dt.get_minute(),
dt.get_second(),
dt.get_microsecond() * 1_000,
)
}

/// Builds an Epoch in UTC from the provided datetime after timezone correction if any is present.
/// :type dt: datetime.datetime
/// :rtype: Epoch
#[classmethod]
fn from_datetime(
cls: &Bound<'_, PyType>,
dt: &Bound<'_, PyAny>,
) -> Result<Self, HifitimeError> {
Self::fromdatetime(cls, dt)
}

/// Converts the Epoch to the Gregorian parts in the (optionally) provided time scale as (year, month, day, hour, minute, second).
///
/// :type time_scale: TimeScale, optional
Expand Down
36 changes: 30 additions & 6 deletions src/epoch/ut1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,27 +90,44 @@ impl Epoch {
impl Epoch {
#[classmethod]
#[pyo3(name = "from_ut1_duration")]
/// Initialize a new Epoch from a duration in UT1
///
/// :type duration: Duration
/// :type provider: Ut1Provider
/// :rtype: Epoch
pub fn py_from_ut1_duration(
_cls: &Bound<'_, PyType>,
duration: Duration,
provider: PyRef<Ut1Provider>,
) -> PyResult<Self> {
Ok(Epoch::from_ut1_duration(duration, &*provider))
Ok(Epoch::from_ut1_duration(duration, &provider))
}

/// Get the accumulated offset between this epoch and UT1.
///
/// :type provider: Ut1Provider
/// :rtype: Duration
#[pyo3(name = "ut1_offset")]
pub fn py_ut1_offset(&self, provider: PyRef<Ut1Provider>) -> Option<Duration> {
self.ut1_offset(&*provider)
self.ut1_offset(&provider)
}

/// Returns this time in a Duration past J1900 counted in UT1
///
/// :type provider: Ut1Provider
/// :rtype: Duration
#[pyo3(name = "to_ut1_duration")]
pub fn py_to_ut1_duration(&self, provider: PyRef<Ut1Provider>) -> Duration {
self.to_ut1_duration(&*provider)
self.to_ut1_duration(&provider)
}

/// Convert this epoch to Ut1
///
/// :type provider: Ut1Provider
/// :rtype: Epoch
#[pyo3(name = "to_ut1")]
pub fn py_to_ut1(&self, provider: PyRef<Ut1Provider>) -> Self {
self.to_ut1(&*provider)
self.to_ut1(&provider)
}
}

Expand All @@ -127,10 +144,15 @@ pub struct DeltaTaiUt1 {

#[repr(C)]
#[cfg_attr(feature = "python", pyclass)]
#[cfg_attr(feature = "python", pyo3(get_all))]
#[derive(Clone, Debug, Default)]
/// A structure storing all of the TAI-UT1 data
pub struct Ut1Provider {
/// vector of Delta TAI-UT1 values
/// :rtype: list
data: Vec<DeltaTaiUt1>,
/// current position of the iterator
/// :rtype: int
iter_pos: usize,
}

Expand Down Expand Up @@ -310,15 +332,17 @@ impl Ut1Provider {
format!("{self:?} @ {self:p}")
}

// For Python, return a list of owned objects.
// Option A: return Python class instances
/// Returns the list of Delta TAI-UT1 values
/// :rtype: list
pub fn as_list(&self, py: Python<'_>) -> PyResult<Vec<Py<DeltaTaiUt1>>> {
self.data.iter().map(|rec| Py::new(py, *rec)).collect()
}

#[classmethod]
#[pyo3(name = "from_eop_file")]
/// Builds a UT1 provider from the provided path to an EOP file.
/// :type path: str
/// :rtype: Ut1Provider
pub fn py_from_eop_file(_cls: &Bound<'_, PyType>, path: &str) -> Result<Self, HifitimeError> {
Ut1Provider::from_eop_file(path)
}
Expand Down
Loading