Tutorial — Adding missions whose metakernels live outside the default NAIF path

—title: “Tutorial — Adding missions whose metakernels live outside the default NAIF path”format: html: toc: true code-fold: falsejupyter: python3execute: enabled: false—Up to v0.13.4, spice-kernel-db mission add assumed every mission’s metakernels lived at {server_url}{MISSION}/kernels/mk/. That works for the ESA missions mirrored on NAIF (BEPICOLOMBO, JUICE, MEX, ROSETTA, SMART1, VEX, EXOMARS2016) and one NASA mission (LADEE), but not for the 68 other active NASA missions on NAIF — MRO, MAVEN, JUNO, LRO, MARS2020, MSL, ORX, etc. — which publish individual per-type kernels and no curated mission-wide .tm file at all.This tutorial covers the additions in the next release:1. Manual override--mk-dir-url points the tool at whatever metakernel directory you found (usually on a PDS node).2. Non-interactive mission add — positional name + flags, no prompts.3. Curated registrymission_registry.toml records verified alternate paths per mission (deliberately sparse, contributions welcome).4. planetarypy bridge — optional integration for PDS-archived missions, which is what most NASA missions actually need.> A note on what this does not solve. Synthesising a metakernel from per-type kernel listings (so you can use a NASA mission that ships no metakernel at all) is sketched as Tier 2 in Plans/did-we-have-a-abstract-kite.md and is a separate follow-up.

0. Setup — isolated DB for the tutorial

All commands below use --db /tmp/skd-tutorial.duckdb so your real database is untouched. You can copy/paste any of these into your own shell; just drop the --db flag to use your configured default.

import os, pathlib
TUTORIAL_DB = '/tmp/skd-tutorial.duckdb'
pathlib.Path(TUTORIAL_DB).unlink(missing_ok=True)
os.environ['SKD_DB'] = TUTORIAL_DB  # purely cosmetic — passed as --db below
print('Using DB:', TUTORIAL_DB)
!spice-kernel-db --version

1. The classic case — interactive mission add for a supported mission

When a mission has a default kernels/mk/ directory, nothing changes from before. The interactive command still works:

spice-kernel-db mission add
# → choose server [1=NASA, 2=ESA]
# → choose from the listed missions
# → enable dedup? [Y/n]

Notebook output for interactive prompts isn’t useful, so we’ll use the non-interactive form for everything below.

2. Non-interactive add with --mk-dir-url (manual override)

Use this when you already know where a mission’s metakernels live — typically because you’ve browsed the NAIF archive in your browser. The URL is validated with one HEAD request, then stored as-is.

Example: MRO (NASA mission, uses the standard kernels/mk/ path).

!spice-kernel-db --db $SKD_DB mission add MRO \
  --server-url https://naif.jpl.nasa.gov/pub/naif/ \
  --mk-dir-url https://naif.jpl.nasa.gov/pub/naif/MRO/kernels/mk/
!spice-kernel-db --db $SKD_DB mission list

If the URL doesn’t respond, the command exits 1 with a clear error — try a deliberately broken URL to see the validation in action:

!spice-kernel-db --db $SKD_DB mission add NOTAMISSION \
  --server-url https://naif.jpl.nasa.gov/pub/naif/ \
  --mk-dir-url https://naif.jpl.nasa.gov/pub/naif/DOES_NOT_EXIST/kernels/mk/ ; echo "exit code: $?"

Disabling deduplication non-interactively

By default, deduplication is enabled. Pass --no-dedup to opt out:

spice-kernel-db mission add JUNO \
  --server-url https://naif.jpl.nasa.gov/pub/naif/ \
  --mk-dir-url https://naif.jpl.nasa.gov/pub/naif/JUNO/kernels/mk/ \
  --no-dedup

3. Discovery — mission add <name> without --mk-dir-urlWhen you skip --mk-dir-url, the tool consults the curated registry, then probes the standard kernels/mk/ path. It persists if exactly one URL responds, lists candidates and asks you to disambiguate via --mk-dir-url if multiple respond, or exits with an error if none respond.The probed templates are exposed in the public API:

from spice_kernel_db.remote import DEFAULT_ALT_MK_PATHSfor t in DEFAULT_ALT_MK_PATHS:    print(t)# Only one template by default. An empirical NAIF survey found that# the speculative alternates (spice_kernels/mk/, data/spice/mk/, etc.)# you might guess from naming convention are not actually in use anywhere.# Add verified alternates via mission_registry.toml or --mk-dir-url.

Driving it as a CLI is the normal path. For demo purposes here we drive discover_mk_url directly so you can see what hits come back without persisting anything:

from spice_kernel_db.remote import discover_mk_url

hits = discover_mk_url('https://naif.jpl.nasa.gov/pub/naif/', 'MRO')
for h in hits:
    print(h)

The full CLI equivalent:

spice-kernel-db mission add MRO --server-url https://naif.jpl.nasa.gov/pub/naif/

(Skipped here because MRO is already registered above. If multiple hits came back, the CLI lists them and asks you to re-run with --mk-dir-url <one of> to pick one.)

4. The curated registry — mission_registry.toml

The package ships an editable TOML file that records known-good alternate metakernel directories per mission. Registry candidates take priority over the generic probe order, so a mission add OSIRIS-REX for a registered mission will try the curated path first.

Inspect what’s bundled:

from spice_kernel_db import registry
registry.load_registry.cache_clear()
entries = registry.load_registry()
if not entries:
    print('Bundled registry is empty — contributions welcome!')
for name, entry in entries.items():
    print(f'{name}: planetarypy={entry.planetarypy}')
    for c in entry.candidates:
        print(f'   {c}')

Adding a registry entry locally (without rebuilding the package)

The loader resolves the TOML via importlib.resources, so anywhere the package is installed (including editable installs) you can edit src/spice_kernel_db/mission_registry.toml and the change is picked up next time load_registry.cache_clear() is called or a new Python process starts.

Format:

["YOUR-MISSION"]
candidates = [
  "{server}{m}/some/alt/mk/",
  "{server}{m}/another/alt/mk/",
]
planetarypy = false

Placeholders: {server} is the server URL with trailing slash, {m} is the mission directory name (the table key).

If you confirm a working path, please open a PR to src/spice_kernel_db/mission_registry.toml so other users benefit.

Demonstration: an in-memory registry

You can exercise the discovery layer without touching the bundled file by patching registry.load_registry temporarily — useful for testing:

from spice_kernel_db import registry as reg

fake = {
    'MRO': reg.MissionEntry(
        name='MRO',
        candidates=(
            '{server}{m}/kernels/mk/',          # standard path (will hit)
            '{server}{m}/data/spice/mk/',       # speculative (will not hit)
        ),
        planetarypy=True,
    ),
}

# Temporarily swap in the fake registry
real_loader = reg.load_registry
reg.load_registry = lambda: fake
try:
    print('candidates:', reg.registry_candidates('MRO', 'https://naif.jpl.nasa.gov/pub/naif/'))
    print('planetarypy?', reg.is_planetarypy_managed('MRO'))
finally:
    reg.load_registry = real_loader

5. The planetarypy bridge — for PDS-archived missions

Several NASA missions (e.g. Dawn, OSIRIS-REx, Lucy) publish SPICE bundles through PDS rather than as a curated mission-wide metakernel on NAIF. The companion planetarypy library already manages those archives; spice-kernel-db exposes an optional bridge to delegate.

Install the extra:

pip install 'spice-kernel-db[planetarypy]'

Then mark the mission in the registry with planetarypy = true and pass --use-planetarypy on mission add:

spice-kernel-db mission add OSIRIS-REX \
  --server-url https://naif.jpl.nasa.gov/pub/naif/ \
  --use-planetarypy

Current status. The delegation hook is a stub — it emits a notice and falls back to normal discovery. Full delegation is tracked as a follow-up. The wiring is already in place so once it lands, no CLI changes are needed.

You can inspect availability programmatically:

from spice_kernel_db import planetarypy_bridge
print('planetarypy importable?', planetarypy_bridge.is_available())
print('delegation stub returns:', planetarypy_bridge.delegate_mission_add('OSIRIS-REX', 'https://naif.jpl.nasa.gov/pub/naif/'))
print('tracking issue:        ', planetarypy_bridge.tracking_issue())

7. Cleanup

pathlib.Path(TUTORIAL_DB).unlink(missing_ok=True)
print('Removed', TUTORIAL_DB)

Where to go next