Metadata-Version: 2.4
Name: tempoch
Version: 0.2.1
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Rust
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Scientific/Engineering
Classifier: Topic :: Scientific/Engineering :: Astronomy
Classifier: Topic :: Scientific/Engineering :: Physics
License-File: LICENSE
Summary: Astronomical time primitives for Python (powered by Rust)
Keywords: astronomy,time,julian-date,ephemeris,python
Author-email: VPRamon <vallespuigramon@gmail.com>
License: AGPL-3.0
Requires-Python: >=3.8
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Documentation, https://docs.rs/tempoch-py
Project-URL: Repository, https://github.com/Siderust/tempoch-py

# tempoch-py

[![Crates.io](https://img.shields.io/crates/v/tempoch.svg)](https://crates.io/crates/tempoch)
[![Docs.rs](https://docs.rs/tempoch/badge.svg)](https://docs.rs/tempoch)
[![CI](https://github.com/Siderust/tempoch-py/actions/workflows/ci.yml/badge.svg)](https://github.com/Siderust/tempoch-py/actions/workflows/ci.yml)
[![License: AGPL-3.0-only](https://img.shields.io/badge/license-AGPL--3.0--only-blue)](LICENSE)
[![Python 3.8+](https://img.shields.io/badge/python-3.8%2B-blue)](https://www.python.org/)

Python bindings for [tempoch](https://github.com/Siderust/tempoch), providing astronomical time primitives backed by Rust through PyO3.

The Crates.io and docs.rs badges above refer to the underlying `tempoch` Rust crate used by these bindings.

## Features

- **Julian Date** and **Modified Julian Date** objects with full arithmetic
- **UTC conversion** (ISO 8601 strings and Python `datetime` objects)
- **Time-scale conversion** between 11 astronomical scales (JD, MJD, TT, TDB, TAI, TCG, TCB, GPS, UnixTime, UT, JDE)
- **Time periods** (intervals) with duration, intersection, and containment
- **Python exceptions** for errors (no raw FFI/status codes)
- **Pickle** and **hash** support
- All computation in Rust (no reimplementation in Python)
- Reusable PyO3 interop for timezone-aware Python datetimes and `Time<UTC>`

## Installation

The bindings are currently built from source. Clone the repository with its Rust submodule, create a Python environment, and install with Maturin:

```bash
git clone --recurse-submodules https://github.com/Siderust/tempoch-py.git
cd tempoch-py

python -m venv .venv
source .venv/bin/activate  # Windows: .venv\\Scripts\\activate

python -m pip install --upgrade pip maturin
maturin develop --release
```

To build a wheel instead:

```bash
maturin build --release
python -m pip install target/wheels/tempoch-*.whl
```

## Quick Start

```python
from tempoch import JulianDate, ModifiedJulianDate, TimePeriod, TimeScale, convert_timescale

# Julian Date basics
j2000 = JulianDate.j2000()          # J2000.0 epoch
print(j2000)                         # JulianDate(2451545.0)
print(j2000.to_utc())               # 2000-01-01T11:58:55...+00:00

# Modified Julian Date
mjd = j2000.to_mjd()                # ModifiedJulianDate(51544.5)
jd_back = mjd.to_jd()               # JulianDate(2451545.0)

# UTC conversion
jd = JulianDate.from_utc("2024-06-21T12:00:00Z")
dt = jd.to_datetime()               # Python datetime object (UTC)

# Arithmetic
tomorrow = j2000 + 1.0              # Add 1 day
diff = tomorrow - j2000             # 1.0 (days)

# Julian centuries since J2000
centuries = jd.julian_centuries()

# Time-scale conversion
tdb = convert_timescale(2451545.0, TimeScale.JD, TimeScale.TDB)
tai = convert_timescale(2451545.0, TimeScale.JD, TimeScale.TAI)

# Time periods
period = TimePeriod(59000.0, 59010.0)  # MJD-based
print(period.duration_days())           # 10.0
print(period.contains(59005.0))         # True

# Period intersection
p1 = TimePeriod(59000.0, 59010.0)
p2 = TimePeriod(59005.0, 59015.0)
overlap = p1.intersection(p2)           # TimePeriod(59005.0, 59010.0)
```

## Rust / PyO3 interoperability

The package also builds an `rlib` named `tempoch_py`, so another PyO3 crate can
reuse the canonical datetime boundary while depending on `tempoch` directly:

```toml
[dependencies]
pyo3 = { version = "0.29", features = ["extension-module"] }
tempoch = "0.7"
tempoch-py = { git = "https://github.com/Siderust/tempoch-py.git" }
```

```rust
use pyo3::prelude::*;
use tempoch::Time;

fn roundtrip<'py>(value: &Bound<'py, PyAny>) -> PyResult<Bound<'py, PyAny>> {
    let instant: Time<tempoch::UTC> = tempoch_py::interop::datetime_to_time(value)?;
    tempoch_py::interop::time_to_datetime(value.py(), instant)
}
```

`datetime_to_time` intentionally rejects naive datetimes. Aware datetimes with
any valid UTC offset are normalized to UTC without converting through a
floating-point POSIX timestamp. Context-aware variants are available for users
that need to supply a specific `tempoch::TimeContext`, and
`period_to_datetimes` converts both endpoints of a `Period<UTC>`.

The Python `TimeScale` enum retains its historical names. In the 0.7 model,
`JD`, `MJD`, `UnixTime`, and `GPS` describe formats, while `TT`, `TAI`, `TDB`,
`TCG`, `TCB`, and `UT` (`UT1`) describe physical scales. `JDE` remains a
compatibility alias for a TT Julian Date.

## API Reference

### Classes

| Class | Description |
|-------|-------------|
| `JulianDate` | Julian Date (continuous day count) |
| `ModifiedJulianDate` | Modified Julian Date (JD − 2,400,000.5) |
| `TimePeriod` | Time interval defined by start/end MJD |
| `TimeScale` | Enum of astronomical time scales |

### JulianDate

| Method | Description |
|--------|-------------|
| `JulianDate(value)` | Create from day number |
| `JulianDate.j2000()` | J2000.0 epoch constant |
| `JulianDate.from_utc(str)` | Create from UTC string |
| `JulianDate.from_datetime(dt)` | Create from Python datetime |
| `.value` | Raw day number (float) |
| `.to_mjd()` | Convert to ModifiedJulianDate |
| `.to_utc()` | Convert to UTC string (ISO 8601) |
| `.to_datetime()` | Convert to Python datetime (UTC) |
| `.add_days(n)` | Add n days |
| `.difference(other)` | Days between two JDs |
| `.julian_centuries()` | Centuries since J2000.0 |
| `.julian_years()` | Years since J2000.0 |
| `.julian_millennia()` | Millennia since J2000.0 |

### ModifiedJulianDate

| Method | Description |
|--------|-------------|
| `ModifiedJulianDate(value)` | Create from MJD day number |
| `ModifiedJulianDate.from_utc(str)` | Create from UTC string |
| `ModifiedJulianDate.from_datetime(dt)` | Create from Python datetime |
| `.value` | Raw MJD day number (float) |
| `.to_jd()` | Convert to JulianDate |
| `.to_utc()` | Convert to UTC string |
| `.to_datetime()` | Convert to Python datetime |
| `.add_days(n)` | Add n days |
| `.difference(other)` | Days between two MJDs |

### TimePeriod

| Method | Description |
|--------|-------------|
| `TimePeriod(start_mjd, end_mjd)` | Create from MJD values |
| `TimePeriod.from_mjd(start, end)` | Create from MJD objects |
| `TimePeriod.from_jd(start, end)` | Create from JD objects |
| `TimePeriod.from_utc(start, end)` | Create from UTC strings |
| `.start` / `.end` | Start/end as ModifiedJulianDate |
| `.start_mjd` / `.end_mjd` | Start/end as float |
| `.duration_days()` | Duration in days |
| `.duration_hours()` | Duration in hours |
| `.duration_seconds()` | Duration in seconds |
| `.to_utc()` | Start/end as UTC strings |
| `.intersection(other)` | Intersection with another period |
| `.contains(mjd)` | Check if MJD is within period |

### Functions

| Function | Description |
|----------|-------------|
| `convert_timescale(value, from, to)` | Convert between time scales |
| `tai_minus_utc(jd)` | TAI − UTC leap seconds (seconds) |
| `intersect_periods(periods, bounds)` | Intersect period list with bounds |

### Exceptions

| Exception | Base | Description |
|-----------|------|-------------|
| `NonFiniteTimeError` | `ValueError` | NaN or infinite time value |
| `InvalidIntervalError` | `ValueError` | Period start after end |
| `ConversionError` | `ValueError` | UTC/scale conversion out of range |

## Time Scales

| Scale | Description |
|-------|-------------|
| `TimeScale.JD` | Julian Date (identity) |
| `TimeScale.JDE` | Julian Ephemeris Day |
| `TimeScale.MJD` | Modified Julian Date |
| `TimeScale.TDB` | Barycentric Dynamical Time |
| `TimeScale.TT` | Terrestrial Time |
| `TimeScale.TAI` | International Atomic Time |
| `TimeScale.TCG` | Geocentric Coordinate Time |
| `TimeScale.TCB` | Barycentric Coordinate Time |
| `TimeScale.GPS` | GPS Time |
| `TimeScale.UnixTime` | Unix/POSIX Time |
| `TimeScale.UT` | Universal Time (Earth rotation) |

## Relationship with tempoch

`tempoch-py` is a thin Python interface over the [`tempoch`](https://github.com/Siderust/tempoch) Rust crate. Core astronomical time calculations remain implemented in Rust; the Python layer focuses on idiomatic Python types, exceptions, and interoperability.

- Rust crate: [crates.io/crates/tempoch](https://crates.io/crates/tempoch)
- Rust API documentation: [docs.rs/tempoch](https://docs.rs/tempoch)

## Development

```bash
python -m venv .venv
source .venv/bin/activate  # Windows: .venv\\Scripts\\activate

python -m pip install --upgrade pip maturin pytest ruff
maturin develop

# Python tests
pytest tests/ -v

# Rust tests
cargo test --all-targets

# Formatting and linting checks used by CI
cargo fmt --check
cargo clippy --all-targets -- -D warnings
ruff format --check python tests examples
ruff check python tests examples
```

## License

AGPL-3.0 — see [LICENSE](LICENSE).

