"""Fetch heliocentric ecliptic (ICRF) state vectors from the JPL Horizons API. Standard library only. For each body and year it saves Horizons' text reply (hourly positions and velocities, AU and AU/day, TDB) and appends a line to fetch-log.jsonl with the query URL, size and SHA-256. Usage: python3 -I fetch_horizons.py [ ...] e.g. python3 -I fetch_horizons.py downloads/jpl-horizons 1h 2009-12-31T23:00 2011-01-01T01:00 199 299 Bodies: 199 Mercury, 299 Venus, 399 Earth, 301 Moon, 499 Mars, 599 Jupiter, 699 Saturn, 799 Uranus, 899 Neptune. The Sun (10) is the coordinate origin. """ import datetime, hashlib, json, pathlib, sys, time, urllib.parse, urllib.request API = "https://ssd.jpl.nasa.gov/api/horizons.api" def query_url(body, start, stop, step): params = { "format": "text", "COMMAND": f"'{body}'", "OBJ_DATA": "'NO'", "MAKE_EPHEM": "'YES'", "EPHEM_TYPE": "'VECTORS'", "CENTER": "'500@10'", "START_TIME": f"'{start.replace('T', ' ')}'", "STOP_TIME": f"'{stop.replace('T', ' ')}'", "STEP_SIZE": f"'{step}'", "REF_PLANE": "'ECLIPTIC'", "REF_SYSTEM": "'ICRF'", "OUT_UNITS": "'AU-D'", "VEC_TABLE": "'2'", "VEC_CORR": "'NONE'", "CSV_FORMAT": "'YES'", "VEC_LABELS": "'NO'", "TIME_DIGITS": "'SECONDS'", } return API + "?" + urllib.parse.urlencode(params, quote_via=urllib.parse.quote) def main(): outdir = pathlib.Path(sys.argv[1]); step, start, stop = sys.argv[2:5]; bodies = sys.argv[5:] outdir.mkdir(parents=True, exist_ok=True) log = outdir / "fetch-log.jsonl" for body in bodies: name = f"{body}_{start[:10]}_{stop[:10]}_{step}.txt" path = outdir / name if path.exists(): print("have", name); continue url = query_url(body, start, stop, step) for attempt in range(4): try: req = urllib.request.Request(url, headers={"User-Agent": "factchecked.science verification (Python urllib)"}) with urllib.request.urlopen(req, timeout=300) as r: data = r.read() break except Exception as e: # network hiccup: wait and retry print("retry", body, e); time.sleep(10 * (attempt + 1)) else: raise SystemExit(f"failed: {url}") text = data.decode("utf-8") if "$$SOE" not in text or "$$EOE" not in text: raise SystemExit(f"no ephemeris in reply for {body}:\n{text[:2000]}") path.write_bytes(data) rec = {"file": name, "url": url, "bytes": len(data), "sha256": hashlib.sha256(data).hexdigest(), "fetched_utc": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")} with log.open("a", encoding="utf-8") as f: f.write(json.dumps(rec) + "\n") print("got", name, len(data)) time.sleep(2) if __name__ == "__main__": main()