Source code for astra.time
# astra/time.py
"""ASTRA Core time conversion utilities.
Provides a unified interface between Python ``datetime``, Julian Dates,
``skyfield`` Time objects, and ISO 8601 strings.
The Skyfield ``Timescale`` is the **managed** IERS-backed instance from
``data_pipeline.get_skyfield_timescale()``, not ``builtin=True``.
"""
from __future__ import annotations
from datetime import datetime, timezone
import threading
from typing import Any, Optional, Union, cast
from skyfield import timelib
from astra.jdutil import datetime_utc_to_jd, jd_utc_to_datetime
# J2000 reference epoch: 2000-01-01T12:00:00 UTC.
_J2000_JD: float = 2451545.0
_J2000_EPOCH: datetime = datetime(2000, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
_cached_ts: Optional[Any] = None
_TS_LOCK = threading.RLock()
def _get_timescale() -> Any:
"""Return the managed Skyfield timescale (IERS finals2000A)."""
global _cached_ts
with _TS_LOCK:
if _cached_ts is None:
from astra.data_pipeline import get_skyfield_timescale
_cached_ts = get_skyfield_timescale()
return _cached_ts
[docs]
def prefetch_iers_data_async() -> None:
"""Pre-fetch IERS / ephemeris data asynchronously (non-blocking)."""
def _fetch() -> None:
_get_timescale()
import threading as _threading
_threading.Thread(target=_fetch, daemon=True).start()
def _iso_to_datetime(iso_str: str) -> datetime:
"""Parse an ISO 8601 string to a UTC-aware datetime."""
cleaned = iso_str.strip()
if cleaned.endswith("Z"):
cleaned = cleaned[:-1] + "+00:00"
dt = datetime.fromisoformat(cleaned)
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
return dt
def _datetime_to_jd(dt: datetime) -> float:
"""Convert a UTC datetime to Julian Date."""
return float(datetime_utc_to_jd(dt))
def _jd_to_datetime(jd: float) -> datetime:
"""Convert a Julian Date to a UTC-aware datetime."""
return cast(datetime, jd_utc_to_datetime(jd))
def _datetime_to_iso(dt: datetime) -> str:
"""Convert a datetime to ISO 8601 UTC string."""
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
utc_dt = dt.astimezone(timezone.utc)
return utc_dt.strftime("%Y-%m-%dT%H:%M:%SZ")
[docs]
def convert_time(
value: Union[str, datetime, float],
to_format: str,
) -> Union[float, datetime, timelib.Time, str]:
"""Universal time format converter.
Converts between ISO 8601 strings, Python ``datetime`` objects,
Julian Dates (float), and ``skyfield.timelib.Time`` objects.
Args:
value: The time value to convert. Accepted types:
- ``str`` — ISO 8601 format ``"YYYY-MM-DDTHH:MM:SSZ"``
- ``datetime`` — timezone-aware or naive (assumed UTC)
- ``float`` — Julian Date (UTC)
to_format: Target format. Must be one of:
- ``"jd"`` → returns ``float`` Julian Date
- ``"datetime"`` → returns ``datetime`` (UTC-aware)
- ``"skyfield"`` → returns ``skyfield.timelib.Time``
- ``"iso"`` → returns ISO 8601 string
Returns:
Converted time in the requested format.
Raises:
ValueError: If ``to_format`` is invalid or ``value`` type unsupported.
"""
valid_formats = ("jd", "datetime", "skyfield", "iso")
if to_format not in valid_formats:
raise ValueError(
f"Unsupported to_format={to_format!r}. " f"Must be one of {valid_formats}."
)
if isinstance(value, str):
dt = _iso_to_datetime(value)
jd = _datetime_to_jd(dt)
elif isinstance(value, datetime):
jd = _datetime_to_jd(value)
elif isinstance(value, (int, float)):
jd = float(value)
else:
raise ValueError(
f"Unsupported input type {type(value).__name__!r}. "
"Must be str, datetime, or float."
)
if to_format == "jd":
return jd
if to_format == "datetime":
return _jd_to_datetime(jd)
if to_format == "iso":
return _datetime_to_iso(_jd_to_datetime(jd))
ts = _get_timescale()
dt = _jd_to_datetime(jd)
return ts.utc(
dt.year,
dt.month,
dt.day,
dt.hour,
dt.minute,
dt.second + dt.microsecond * 1e-6,
)