Command-Line Interface

PlanetaryPy provides a unified CLI via the plp command.

Design philosophy: API first, CLI wraps thin

Every plp verb is a thin wrapper over a public Python API. The library function gets built and tested first; the CLI command then forwards arguments and formats output. Useful logic — parsing PID lists, building catalogs, batching downloads, filtering indexes by PIDs, parallel execution — lives in planetarypy.* modules, not under cli.py.

The reason is reuse. Notebooks, batch scripts, and downstream tooling should pick up new capabilities the moment they ship — without screen scraping or shelling out to plp. If a feature is only reachable from the command line, it’s rotting code: the people who would benefit from it most (the ones already inside Python) can’t use it. So when you read the source for any plp command, expect a few argument-validation lines followed by a direct call into planetarypy.pds, .catalog, .utils, or .instruments.<mission>. If you find substantial logic inside cli.py, that’s a bug — please factor it down to the API layer.

Installation

pip install planetarypy

The plp command is automatically available after installation.

Commands

plp fetch — Download a PDS product

plp fetch <key> <product_id> [OPTIONS]

Download a PDS data product by its dotted key and product identifier.

Arguments:

Argument Description
key Dotted product key, e.g. mro.ctx.edr
product_ids One or more product identifiers (variadic), e.g. P02_001916_2221_XI_42N027W. Optional when --pids-from is given.

Options:

Option Short Description
--force -f Re-download even if already cached locally
--label-only -l Download only the label file
--here -H Download into current directory instead of planetarypy storage
--folder -d Print the local folder on stdout instead of the per-file paths (single-PID only; composes with cd)
--prefix Treat a PID that doesn’t match a full PRODUCT_ID but is a leading prefix of real ones as a request for all matching products (e.g. a HiRISE obsid → every CCD product). Reads the index to resolve; requires key to be a registered index. Off by default to avoid surprise bulk downloads.
--workers -w Parallel download threads in batch mode (default 4)
--report Batch outcome report: errors-only (default) | full | jsonl | csv. Single-PID calls ignore this

It also accepts the shared PID-input family described under Batch PID input--pids-from, --pid-key, --pid-suffix.

Stdout behaviour: for a single PID, plp fetch emits one absolute file path per line on stdout, so shell substitution like qgis (plp fetch ...) passes every downloaded file as an argument. With --folder it emits a single line — the local directory — so cd (plp fetch --folder ...) works. In batch mode (multiple PIDs), per-PID outcomes go to stderr per --report.

Examples:

# Download a CTX EDR image to planetarypy storage
plp fetch mro.ctx.edr P02_001916_2221_XI_42N027W

# Batch: several PIDs at once (variadic)
plp fetch mro.ctx.edr P02_001916_2221_XI_42N027W J03_046269_1842_XN_04N222W

# Batch from a file (one PID per line) or stdin
plp fetch mro.ctx.edr --pids-from my_targets.txt
cat ids.txt | plp fetch mro.ctx.edr --pids-from -

# A HiRISE obsid → every CCD product of that observation
plp fetch mro.hirise.edr ESP_075205_0930 --prefix

# Download into the current working directory
plp fetch --here mro.ctx.edr P02_001916_2221_XI_42N027W

# Download only the label file
plp fetch --label-only cassini.iss.edr_sat 1_N1523786525.118

# cd into the product folder (--folder = single-line stdout)
cd (plp fetch --folder mro.ctx.edr P02_001916_2221_XI_42N027W)

To discover available product keys:

from planetarypy.catalog import list_missions, list_instruments, list_products

list_missions()              # → ['cassini', 'mro', 'lro', ...]
list_instruments("mro")      # → ['ctx', 'hirise', 'crism', ...]
list_products("mro.ctx")     # → ['edr']

plp ctxqvCTX quickview

plp ctxqv <imgid> [OPTIONS]

Show a downsampled quickview of a CTX image. Automatically uses the best available processing level: map-projected > calibrated > cube > raw EDR.

Arguments:

Argument Description
imgid CTX product ID (short or full), e.g. J05_046771_1950

Options:

Option Short Description
--stride -s Downsample factor (default: 10)
--save -o Save to PNG file instead of displaying
--stretch -p Percentile stretch as low,high (default: 1,99). Use none to disable
--edr Force raw EDR quickview, skip calibrated files

Examples:

# Display quickview in a window
plp ctxqv J05_046771_1950

# Save to file with higher resolution
plp ctxqv J05_046771_1950 --stride 5 --save output.png

# Force raw EDR (skip calibrated products)
plp ctxqv --edr J05_046771_1950

plp catalog build — Build the PDS catalog

plp catalog build [OPTIONS]

Build the PDS catalog database from pdr-tests definitions. The catalog is stored as a DuckDB file under ~/planetarypy_data/catalog/.

Options:

Option Description
--force Force rebuild from scratch (otherwise skips if already built)
--validate-urls Run URL validation via HTTP HEAD after building

Examples:

# Build catalog (first time or incremental)
plp catalog build

# Force full rebuild
plp catalog build --force

plp indexes — Browse and read PDS index tables

The indexes sub-app is the operational surface for the PDS3 index files (.lbl + .tab) that planetarypy downloads, parses, and caches as parquet. Indexes are addressed by the dotted {mission}.{instrument}.{indexname} key, e.g. mro.ctx.edr, cassini.iss.ring_summary, mro.hirise.rdr.

Every verb prints help and exits 0 when invoked with no arguments, so plp indexes select (no key) shows usage rather than an error.

Verb Purpose
list Browse which indexes exist (by mission / instrument) and their cache status
peek Inspect one index: shape, column names, a few random rows (transposed)
last Show the last (newest) entries of an index, transposed
counts Tabulate value frequencies of one or more columns (value_counts)
select Filter an index to specific PIDs and render the matching rows
info Config + cache status for one index (URLs, local cache, freshness, update-available)
refresh Refresh the upstream URL config, or re-download a single index’s cache

plp indexes list

plp indexes list [SCOPE] [--tree]

SCOPE narrows the view: omit it for a mission summary, give a mission (cassini) for its instruments, or a mission.instrument (cassini.iss) for its indexes with cache status. --tree prints the full legacy tree.

plp indexes list                     # all missions, summary table
plp indexes list cassini             # cassini instruments
plp indexes list cassini.iss         # cassini.iss indexes (with cache status)
plp indexes list --tree              # full tree

plp indexes peek / plp indexes last

plp indexes peek <key> [-c COL[,COL...]]
plp indexes last <key> [--rows N] [--sort] [-c COL[,COL...]]

peek shows a few random rows; last shows the trailing rows (most PDS indexes are appended chronologically, so the last row is the newest — pass --sort to sort by a time column when the file isn’t ordered). Both render transposed (one index row per output column) so wide schemas stay readable, and both accept --columns/-c for projection.

plp indexes peek mro.ctx.edr
plp indexes last mro.hirise.rdr --rows 5
plp indexes last mro.ctx.edr -c PRODUCT_ID,IMAGE_TIME

plp indexes counts

plp indexes counts <key> [COLUMN] [-c COL[,COL...]] [--top N] [--dropna]

A pandas.value_counts view: how many rows carry each distinct value of a column, with percent-of-total. Handy for categorical columns (TARGET_NAME, MISSION_PHASE_NAME, INSTRUMENT_MODE_ID) before filtering. --top N limits to the N most frequent (default 10; 0 for all); --dropna excludes missing values.

plp indexes counts mro.ctx.edr TARGET_NAME
plp indexes counts mro.ctx.edr MISSION_PHASE_NAME --top 0
plp indexes counts mro.ctx.edr -c TARGET_NAME,MISSION_PHASE_NAME

plp indexes select

plp indexes select <key> [PIDS...] [--format FMT] [-c COL[,COL...]] [--report MODE]

Filter an index to specific PIDs and render the matching rows — the natural companion to plp fetch ... --pids-from. Rows go to stdout (so it pipes cleanly); the resolution report (prefix expansions, missing PIDs) goes to stderr regardless of format.

Prefix expansion (automatic): a PID that matches no full PRODUCT_ID exactly but is a leading prefix of real ones expands to all matching products — so a HiRISE obsid handed to the per-CCD EDR index returns every CCD product. An exact match is never expanded. (plp fetch does the same, but only behind the explicit --prefix flag, since it downloads.)

Option Short Description
--format auto (default; transposed table when few rows, else CSV) | table | csv | jsonl
--columns -c Column projection (comma-separated and/or repeated)
--max-table-rows Row count above which auto switches to CSV (default: max_table_rows config, 3)
--report errors-only (default) | full (list every missing PID)

Plus the shared Batch PID input flags.

plp indexes select mro.ctx.edr P02_001916_2221_XI_42N027W
plp indexes select mro.hirise.edr ESP_075205_0930          # obsid → all CCD products
plp indexes select mro.ctx.edr --pids-from my_targets.txt
plp indexes select mro.ctx.edr --pids-from - --format jsonl < pids.txt

plp indexes info / plp indexes refresh

plp indexes info <key>
plp indexes refresh [--config] [--cache KEY]

info shows one index’s remote URL, remote type, local cache path + size, when it was last updated and last checked, and whether a newer version is available upstream. refresh --config re-fetches the upstream URL config; refresh --cache KEY force-re-downloads that index’s parquet.

plp indexes info mro.hirise.edr
plp indexes refresh --config
plp indexes refresh --cache mro.ctx.edr

plp constants — Per-body planetary constants

Print the planetary constants planetarypy composes for a body from PCK polynomial fields, JPL DE-series GMs, and the NSSDC fact-sheet archive. The command has three forms.

Body table — every scalar constant for one body, with its source:

plp constants Mars

Single value — pipe-safe: the value goes to stdout, the source/reference lines to stderr, so you can feed it straight into awk/jq:

plp constants Mars.GM
plp constants Mars.GM | awk '{print $1}'

Body matching is case-insensitive (mars == Mars == MARS), and a misspelt body or field exits non-zero with a “did you mean …?” suggestion. --at <date> (YYYY, YYYY-MM, or YYYY-MM-DD) time-travels the value, walking PCK editions and NSSDC capture history:

plp constants Mars.pole_dec --at 2012     # IAU 2009-era value via pck00010.tpc

Discoverylist browses the registry by category. Bare list prints the category menu with counts; add a category to see its bodies; for moons, add a planet to restrict to its satellites:

plp constants list                  # category menu + counts
plp constants list planets
plp constants list moons            # every moon
plp constants list moons saturn     # moons of Saturn only
plp constants list dwarf_planets    # flag-based, spans classes (Pluto + Ceres)
plp constants list mission_visited  # small bodies a mission has reached

Categories: planets, moons, asteroids, comets, dwarf_planets, mission_visited, sun. They overlap by design — Pluto is both a planet and a dwarf_planet, matching the IAU’s dual classification. Each maps directly to a discovery helper in planetarypy.constants (planets(), moons(of=…), asteroids(), …), so the same browsing is available from Python.

Batch PID input (shared flags)

plp fetch and plp indexes select share one family of flags for feeding a batch of product IDs from a file or stdin instead of (or in addition to) positional PIDs:

Option Description
--pids-from PATH \| - Read PIDs from a file or stdin (-). Plain text → one PID per line (blank lines and #-comments ignored). .csv / .tsv / .tab → parsed as a table (comma vs tab auto-detected from the header), PID column auto-detected from the index registry (or set --pid-key). Mutually exclusive with positional PIDs.
--pid-key NAME Column name to read PIDs from when --pids-from is a CSV; also forces CSV parsing of a .txt/stdin source.
--pid-suffix STR String appended to every PID read from --pids-from (e.g. --pid-suffix _RED turns a HiRISE obsid into its RED product). Not applied to positional PIDs — hand-typed PIDs are passed through verbatim.

Tabular mode is auto-detected when the source has a .csv / .tsv / .tab extension, when --pid-key is given, or when stdin’s first non-blank line contains a comma or a tab. The delimiter itself (comma vs tab) is then sniffed from the header, so tab-separated exports work without any flag. Otherwise the source is read as plain text.

# plain-text list, one PID per line
plp indexes select mro.ctx.edr --pids-from targets.txt

# CSV with an explicit PID column
plp fetch mro.hirise.rdr --pids-from obs.csv --pid-key OBSERVATION_ID --pid-suffix _RED

# stdin (comma in first line → CSV auto-detected)
head -5 catalog.csv | plp indexes select mro.ctx.edr --pids-from -