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 planetarypyThe 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 ctxqv — CTX 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_1950plp 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 --forceplp 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 treeplp 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_TIMEplp 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_NAMEplp 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.txtplp 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.edrplp 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 MarsSingle 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.tpcDiscovery — list 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 reachedCategories: 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.