psa
psa
Resolve and download ESA PSA products via the Planetary Science Archive TAP.
ESA’s PSA exposes an IVOA EPN-TAP table, psa.epn_core, in which every granule’s access_url is a direct download endpoint. A PDS product id appears inside the granule’s granule_uid (DATA_SET_ID:DATA:PRODUCT_ID::version), so a substring match resolves a product id straight to a download URL — no per-instrument rules, no harvest.
Implements the resolver contract resolve(product_id) -> access_url | None. Needs only requests (a core dependency); no IVOA/pyVO stack.
Functions
| Name | Description |
|---|---|
| dataset_group | The logical group id for a PSA dataset: its DATA_SET_ID minus -EXTn. |
| datasets | List PSA datasets for a mission and optional instrument. |
| examples | Return up to n example PSA products for a product type. |
| fetch_psa_product | Download an ESA PSA product by id; return the local file paths. |
| geometry_index | Build a filterable geometry/index table for a PSA dataset (group). |
| group_members | Every DATA_SET_ID belonging to dataset’s group, sorted. |
| instruments | List PSA instruments with their product counts, optionally for one mission. |
| missions | List the PSA missions (instrument hosts) with their product counts. |
| query | Run an ADQL query against the PSA TAP service; return rows as dicts. |
| resolve | Resolve a product id to its PSA download URL, or None if not found. |
| resolve_all | Every PSA granule whose granule_uid contains product_id. |
dataset_group
psa.dataset_group(dataset)The logical group id for a PSA dataset: its DATA_SET_ID minus -EXTn.
The base dataset and its mission-phase extensions share a group, so MEX-M-HRSC-3-RDR-V4.0 and MEX-M-HRSC-3-RDR-EXT4-V4.0 both map to MEX-M-HRSC-3-RDR-V4.0. The archive version is preserved — different versions (V3.0 vs V4.0) stay distinct groups.
datasets
psa.datasets(mission, instrument=None)List PSA datasets for a mission and optional instrument.
A PSA dataset is the archival grouping that holds many products — a PDS3 data set (DATA_SET_ID, e.g. MEX-M-ASPERA3-2-EDR-IMA-EXT4-V1.0:DATA) or a PDS4 collection (LID, e.g. urn:esa:psa:bc_mpo_berm:data_calibrated). Each row is a dataset with its product count, busiest first.
mission and instrument are matched as case-insensitive substrings of the PSA host/instrument names listed by :func:missions / :func:instruments. A returned dataset can be handed straight to :func:examples — so the PSA browse chain stays entirely in PSA’s own vocabulary, no catalog key needed.
examples
psa.examples(key, n=5)Return up to n example PSA products for a product type.
key may be either:
- a PSA dataset (a PDS3
DATA_SET_IDor PDS4 collection LID, as listed by :func:datasets) — used directly, fully within PSA’s vocabulary; or - a catalog key
mission.instrument.product_type(e.g."mex.aspera.els_edr_high") — the catalog maps it to a seed product whose granule identifier reveals the PSA dataset, so no mission-name translation is needed.
Returns a DataFrame with product_id, granule_uid and access_url. Empty if the key isn’t resolvable to a PSA dataset.
fetch_psa_product
psa.fetch_psa_product(
product_id,
dest=None,
*,
direct=True,
extract=True,
skip_online_check=False,
)Download an ESA PSA product by id; return the local file paths.
Two delivery routes, both landing in the same faithful tree under {storage_root}/psa/<DATA_SET_ID>/DATA/<volume>/<files> (volume sharding preserved, real DATA_SET_ID as the folder). The files open with :func:planetarypy.open.
- Direct FTP file (default,
direct=True): fetch the product’s own file(s) straight from the PSA archive via the granule’slabel_url— no zip, no redundant volume manifest. For an attached-label product that URL is the data file; for a detached-label one it is the.LBLwhose^POINTERnames the co-located data file, which is fetched alongside it. - Zip bundle (
direct=False, or automatic fallback when the granule has nolabel_url— e.g. PDS4 missions): download the PSA product zip and unpack it faithfully. Dataset-level docs (VOLDESC.CAT/AAREADME.TXT) are written once; the zip’s owninventory.txtmanifest is dropped.extract=Falsekeeps the raw zip instead.
A per-product marker under psa/.fetched/ records the fetched paths, so a repeat fetch returns them without re-downloading. Pass dest to use a different root.
geometry_index
psa.geometry_index(dataset, *, aggregate=True, force=False)Build a filterable geometry/index table for a PSA dataset (group).
Downloads the per-dataset PDS3 geometry table(s) (GEO_*.TAB, falling back to INDEX.TAB), parses them with the standard PDS index reader, and returns one concatenated pandas.DataFrame carrying the per-product discovery columns. Each member dataset is parquet-cached under {storage_root}/psa/.indexes/ so the (potentially large) first build is paid once; pass force=True to rebuild.
With aggregate=True (default) the whole group is unioned — base plus all -EXTn mission-phase volumes (see :func:group_members); pass aggregate=False for the single named dataset only. Filter the result on any column and feed the resulting PRODUCT_ID values to :func:fetch_psa_product.
group_members
psa.group_members(dataset)Every DATA_SET_ID belonging to dataset’s group, sorted.
Enumerated from EPN-TAP (the instrument’s distinct granule_gids) and filtered by :func:dataset_group, so base + all -EXTn volumes come back together. Returns [dataset] if the dataset can’t be located.
instruments
psa.instruments(mission=None)List PSA instruments with their product counts, optionally for one mission.
products is the number of downloadable data products. mission is matched as a case-insensitive substring of the instrument host name (e.g. "Mars Express", "rosetta").
missions
psa.missions()List the PSA missions (instrument hosts) with their product counts.
Busiest first. products is the number of individually downloadable data products the PSA holds for that mission (one PSA “granule” = one product). The catalog column is a best-effort hint: the planetarypy catalog mission code (the prefix of a mission.instrument.product_type key, e.g. mex), found by matching the PSA host name against the curated MISSION_FULL_NAMES display strings. It is blank when the PSA name doesn’t match (different spelling, or the mission isn’t in the catalog) — the match is coincidental, not contractual. The reliable way to find a key is catalog-first: plp catalog list → plp catalog list <mission> → plp catalog list <mission>.<instrument>. The fetch/examples paths do not depend on this column.
query
psa.query(adql, *, timeout=60)Run an ADQL query against the PSA TAP service; return rows as dicts.
resolve
psa.resolve(product_id)Resolve a product id to its PSA download URL, or None if not found.
The resolver contract: identity → access_url | None (NotResolvable). When the id matches several granules, the first match’s URL is returned — use :func:resolve_all to inspect all matches.
resolve_all
psa.resolve_all(product_id, *, limit=20)Every PSA granule whose granule_uid contains product_id.
Each row has granule_uid, access_url (the zip-bundle download), access_format, and label_url (the direct file URL on the PSA FTP archive — the product’s PDS label, which for an attached-label product is the data file). Returns an empty list when nothing matches. ADQL LIKE does substring matching, so the bare product id is enough. Results are ordered by granule_uid so the same id resolves deterministically — a filename can occur in several datasets (e.g. across processing levels), and a stable order makes :func:resolve and :func:fetch_psa_product reproducible.