Metadata-Version: 2.4
Name: utdfpy
Version: 0.1.3
Summary: Read and manipulate Universal Tracking Data Format (UTDF) spacecraft tracking data
License: Artistic-2.0 OR GPL-1.0-or-later
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: Topic :: Scientific/Engineering :: Astronomy
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-cov; extra == "dev"
Requires-Dist: mypy; extra == "dev"
Requires-Dist: ruff; extra == "dev"
Dynamic: license-file

# UTDFpy

A Python library for reading and manipulating Universal Tracking Data Format (UTDF) satellite tracking data.

A direct conversion of https://github.com/trwyant/perl-Astro-UTDF.
Thanks go to https://github.com/trwyant for creating the Perl module.

## Binary Format

UTDF records are 75-byte big-endian binary structures. The library uses explicit big-endian unpacking (`struct` format `>`), so it produces identical results on any architecture (arm64, amd64, etc.).

## Prerequisites

- [Task](https://taskfile.dev/) (task runner)
- Docker (or Podman)

## Development

All development tasks run inside a Docker container via [Taskfile.yaml](Taskfile.yaml). To use Podman instead of Docker:

```sh
task tests CONTAINER_BIN=podman
```

### Available tasks

```
task tests          # Run lint, format check, typecheck, then pytest
task tests:cov      # Run tests with coverage report
task lint           # Run ruff linter
task lint:fix       # Run ruff linter with auto-fix (modifies host files)
task format         # Run ruff formatter (modifies host files)
task format:check   # Check formatting without changes
task typecheck      # Run mypy type checking
task check          # Run all checks (lint, format, typecheck, tests)
task build          # Build distribution packages
task publish        # Build and publish package to PyPI
task utdfpy:install            # Install utdfpy from PyPI into local venv
task utdfpy:install:from:local # Install utdfpy from local source (editable)
task example_utdf              # Run read_utdf.py example against test data
task utdf-cli                  # Run interactive UTDF query tool
task github:actions:test       # Run GitHub Actions locally with act (alias: act)
task clean                     # Remove build artifacts and caches
task clean:all                 # Remove everything including Docker image
```

### Task dependency chain

`tests` depends on `lint`, `format:check`, and `typecheck`, each of which depends on `docker:build`. Running `task tests` will automatically build the Docker image, run the linter, check formatting, and run mypy before executing the test suite. `publish` depends on `tests`, so it runs the full check suite before uploading to PyPI.

## Installation

```sh
pip install utdfpy
```

Or for development:

```sh
pip install -e '.[dev]'
```

## Usage

```python
from utdfpy import UTDFRecord

# Read all records from a UTDF file
records = UTDFRecord.slurp("tracking_data.utd")

for record in records:
    print(record.sic, record.vid, record.decode("measurement_time"))
    print(f"  azimuth:  {record.azimuth}")
    print(f"  elevation: {record.elevation}")
    print(f"  range:    {record.range}")
```

### Example script

```sh
$ .venv/bin/python examples/read_utdf.py tests/data/data.utd
Read 2 records from tests/data/data.utd

Record 0:
  SIC:             86
  VID:             99
  Time:            2010-03-19T01:01:30+00:00
  Azimuth:         5.839702317578685
  Azimuth (deg):   334.5397498011498
  Elevation:       0.9403839735099665
  Elevation (deg): 53.877694653498
  Range (km):      425.12365727401914

Record 1:
  SIC:             86
  VID:             99
  Time:            2010-03-19T01:01:31+00:00
  Azimuth:         5.852690026751761
  Azimuth (deg):   335.28364953218365
  Elevation:       0.9534989112054779
  Elevation (deg): 54.62862629867479
  Range (km):      421.4236702842119
```

### CLI tool

An interactive query tool is included:

```sh
utdf-cli tracking_data.utd
```

Commands: `load`, `count`, `select`, `next`, `list`, or type any field name to inspect it.

### Running GitHub Actions locally

You can run the CI jobs locally using [act](https://github.com/nektos/act). On macOS, Docker Desktop fails due to its containerd image store, so use Podman instead:

```sh
task clean:all act CONTAINER_BIN=podman
```

This will clean all artifacts, rebuild the container image with Podman, and run the `test`, `lint`, and `typecheck` jobs locally.

To run without a full clean:

```sh
task act CONTAINER_BIN=podman
```

## CI/CD

- **GitHub Actions** ([.github/workflows/ci.yaml](.github/workflows/ci.yaml)): runs lint, tests, and typecheck on push/PR; auto-creates a release and publishes to PyPI when the version in `pyproject.toml` changes.
- **GitLab CI** ([.gitlab-ci.yaml](.gitlab-ci.yaml)): runs lint, tests (Python 3.10-3.13), and typecheck; publishes to PyPI on version tags.

## License

Artistic-2.0 OR GPL-1.0-or-later (same as the original Perl module).

## Architecture

```mermaid
classDiagram
    class UTDFRecord {
        -bytes _front
        -str _router
        -int _year
        -int _sic
        -int _vid
        -int _seconds_of_year
        -int _microseconds_of_year
        -float _transponder_latency
        -bool _enforce_validity
        -UTDFRecord _prior_record
        -float _factor_K
        -float _factor_M
        +azimuth() float
        +elevation() float
        +range_delay() float
        +doppler_count() float
        +doppler_shift() float
        +range() float
        +range_rate() float
        +measurement_time() float
        +transmit_frequency() int
        +raw_record() bytes
        +hex_record() str
        +clone() UTDFRecord
        +slurp(file) list~UTDFRecord~
        +iter_file(file) Iterator~UTDFRecord~
        +decode(field_name) str
    }

    class UTDFError {
        <<exception>>
    }

    class InvalidRecordError {
        <<exception>>
    }

    class FrequencyBand {
        <<IntEnum>>
        UNSPECIFIED
        VHF
        UHF
        S_BAND
        C_BAND
        X_BAND
        KU_BAND
        VISIBLE
    }

    class TransmissionType {
        <<IntEnum>>
        TEST
        SIMULATED
        RESUBMIT
        REAL_TIME
        PLAYBACK
    }

    class TrackingMode {
        <<IntEnum>>
        AUTOTRACK
        PROGRAM_TRACK
        MANUAL
        SLAVED
    }

    class TrackerType {
        <<IntEnum>>
        C_BAND_PULSE
        SRE
        SGLS
        TDRSS
    }

    class AntennaGeometry {
        <<IntEnum>>
        AZ_EL
        XY_SOUTH
        XY_EAST
        RA_DEC
        HR_DEC
    }

    Exception <|-- UTDFError
    UTDFError <|-- InvalidRecordError
    UTDFRecord --> UTDFRecord : prior_record
    UTDFRecord ..> InvalidRecordError : raises
    UTDFRecord ..> parsing : uses
    UTDFRecord ..> decode : uses
```
