Skip to content

Commit 387de0c

Browse files
authored
Add spice instructions (#299)
1 parent 231bbe6 commit 387de0c

6 files changed

Lines changed: 258 additions & 13 deletions

File tree

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,27 @@ $ imap-data-access query --start-date 20240101 --end-date 20241231 --output-form
4747
[{'file_path': 'imap/swe/l0/2024/01/imap_swe_l0_sci_20240105_v001.pkts', 'instrument': 'swe', 'data_level': 'l0', 'descriptor': 'sci', 'start_date': '20240105', 'version': 'v001', 'extension': 'pkts'}, {'file_path': 'imap/swe/l0/2024/01/imap_swe_l0_sci_20240105_v001.pkts', 'instrument': 'swe', 'data_level': 'l0', 'descriptor': 'sci', 'start_date': '20240105', 'version': 'v001', 'extension': 'pkts'}]
4848
```
4949

50+
Find all latest predicted ephemeris kernels
51+
52+
```bash
53+
$ imap-data-access query --table spice --ingestion-start-date 20260301 --type ephemeris_predicted --version latest
54+
```
55+
56+
An equivalent curl command is shown.
57+
```bash
58+
$ curl "https://api.imap-mission.com/spice-query?start_ingest_date=20260301&type=ephemeris_predicted&latest=true"
59+
```
60+
61+
Download a predicted ephemeris kernel
62+
```bash
63+
$ imap-data-access download spk/imap_pred_od025_20260303_20260414_v01.bsp
64+
```
65+
66+
An equivalent curl command is shown.
67+
```bash
68+
$ curl "https://api.imap-mission.com/download/imap/spice/spk/imap_pred_od025_20260303_20260414_v01.bsp" --output imap_pred_od025_20260303_20260414_v01.bsp
69+
```
70+
5071
### Download a file
5172

5273
Download a level 0 SWE file on 2024/01/05

imap_data_access/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
ScienceFilePath,
1919
SPICEFilePath,
2020
)
21-
from imap_data_access.io import download, query, reprocess, upload
21+
from imap_data_access.io import download, query, reprocess, spice_query, upload
2222
from imap_data_access.processing_input import (
2323
AncillaryInput,
2424
ProcessingInputCollection,
@@ -47,6 +47,7 @@
4747
"download",
4848
"query",
4949
"reprocess",
50+
"spice_query",
5051
"upload",
5152
]
5253

imap_data_access/cli.py

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
ScienceFilePath,
3030
generate_imap_file_path,
3131
)
32-
from imap_data_access.io import query
32+
from imap_data_access.io import query, spice_query
3333
from imap_data_access.webpoda import download_daily_data
3434

3535

@@ -45,7 +45,7 @@ def _download_parser(args: argparse.Namespace):
4545
print(f"Successfully downloaded the file to: {output_path}")
4646

4747

48-
# ruff: noqa: PLR0912
48+
# ruff: noqa: PLR0912, PLR0915
4949
def _print_query_results_table(query_results: list[dict]):
5050
"""Print the query results in a table.
5151
@@ -62,7 +62,9 @@ def _print_query_results_table(query_results: list[dict]):
6262

6363
# Get the database table
6464
query_table = ""
65-
if "end_date" in query_results[0]:
65+
if "kernel_type" in query_results[0]:
66+
query_table = "spice"
67+
elif "end_date" in query_results[0]:
6668
query_table = "ancillary"
6769
elif "repointing" in query_results[0]:
6870
query_table = "science"
@@ -86,6 +88,14 @@ def _print_query_results_table(query_results: list[dict]):
8688
"Version",
8789
"Filename",
8890
]
91+
spice_header_keys = {
92+
"Kernel Type": "kernel_type",
93+
"Min Date": "min_date_datetime",
94+
"Max Date": "max_date_datetime",
95+
"Ingestion Date": "ingestion_date",
96+
"Version": "version",
97+
}
98+
headers_spice = [*spice_header_keys.keys(), "Filename"]
8999
# Boolean to check if CR is present in any science files
90100
cr_flag = query_table == "science" and any(
91101
item.get("cr") not in (None, "", []) for item in query_results
@@ -104,16 +114,22 @@ def _print_query_results_table(query_results: list[dict]):
104114
# Set appropriate headers for desired table
105115
if query_table == "science":
106116
headers = headers_science
117+
elif query_table == "spice":
118+
headers = headers_spice
107119
else:
108120
headers = headers_ancillary
109121

110122
# Calculate the maximum width for each column based on the header and the data
111123
# have to adjust Ingestion Date, Filename, and CR to properly align
112124
column_widths = {}
113125
for header in headers[:-1]:
126+
if query_table == "spice":
127+
data_key = spice_header_keys[header]
128+
else:
129+
data_key = header.lower()
114130
column_widths[header] = max(
115131
len(header),
116-
*(len(str(item.get(header.lower(), ""))) for item in query_results),
132+
*(len(str(item.get(data_key, ""))) for item in query_results),
117133
)
118134

119135
column_widths["Ingestion Date"] = max(
@@ -128,12 +144,10 @@ def _print_query_results_table(query_results: list[dict]):
128144
len("CR"), *(len(str(item.get("cr", ""))) for item in query_results)
129145
)
130146

147+
file_key = "file_name" if query_table == "spice" else "file_path"
131148
column_widths["Filename"] = max(
132149
len("Filename"),
133-
*(
134-
len(os.path.basename(item.get("file_path", "")))
135-
for item in query_results
136-
),
150+
*(len(os.path.basename(item.get(file_key, ""))) for item in query_results),
137151
)
138152

139153
# Create the format string dynamically based on the number of columns
@@ -153,7 +167,11 @@ def _print_query_results_table(query_results: list[dict]):
153167

154168
# Print data
155169
for item in query_results:
156-
if query_table == "ancillary":
170+
if query_table == "spice":
171+
values = [
172+
str(item.get(spice_header_keys[header], "")) for header in headers[:-1]
173+
] + [str(item.get("file_name", ""))]
174+
elif query_table == "ancillary":
157175
values = [
158176
str(item.get("instrument", "")),
159177
str(item.get("descriptor", "")),
@@ -210,6 +228,7 @@ def _query_parser(args: argparse.Namespace):
210228
"version",
211229
"extension",
212230
"filename",
231+
"type",
213232
]
214233

215234
# Filter to get the arguments of interest from the namespace
@@ -262,9 +281,21 @@ def _query_parser(args: argparse.Namespace):
262281

263282
# SPICE query table
264283
elif args.table == "spice":
265-
raise NotImplementedError("SPICE query not implemented yet.")
284+
spice_valid_args = [
285+
"start_date",
286+
"end_date",
287+
"ingestion_start_date",
288+
"ingestion_end_date",
289+
"type",
290+
"version",
291+
]
292+
query_params = {
293+
key: value for key, value in query_params.items() if key in spice_valid_args
294+
}
295+
query_results = spice_query(**query_params)
266296

267-
query_results = query(**query_params)
297+
else:
298+
query_results = query(**query_params)
268299

269300
if args.output_format == "table":
270301
_print_query_results_table(query_results)
@@ -401,6 +432,18 @@ def add_query_args(subparser: ArgumentParser) -> None:
401432
"processing.readthedocs.io/en/latest/development-guide/style-guide/naming-conventions"
402433
".html#data-product-file-naming-conventions",
403434
)
435+
subparser.add_argument(
436+
"--type",
437+
type=str,
438+
required=False,
439+
help="SPICE kernel type. Only used with --table spice. "
440+
"Valid types: attitude_history, attitude_predict, spin, repoint, "
441+
"ephemeris_reconstructed, ephemeris_nominal, ephemeris_predicted, "
442+
"ephemeris_90days, ephemeris_long, ephemeris_launch, planetary_ephemeris, "
443+
"planetary_constants, leapseconds, pointing_attitude, spacecraft_clock, "
444+
"imap_frames, science_frames, metakernel, thruster, lagrange_point, "
445+
"earth_attitude.",
446+
)
404447
subparser.set_defaults(func=_query_parser)
405448

406449

@@ -430,7 +473,7 @@ def _reprocess_parser(args: argparse.Namespace):
430473

431474

432475
# PLR0915: too many statements
433-
def main(): # noqa: PLR0915
476+
def main():
434477
"""Parse the command line arguments.
435478
436479
Run the command line interface to the IMAP Data Access API.

imap_data_access/io.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,74 @@ def _validate_query_parameters(**kwargs) -> None:
219219
)
220220

221221

222+
def spice_query(
223+
*,
224+
start_date: Optional[str] = None,
225+
end_date: Optional[str] = None,
226+
ingestion_start_date: Optional[str] = None,
227+
ingestion_end_date: Optional[str] = None,
228+
type: Optional[str] = None,
229+
version: Optional[str] = None,
230+
) -> list[dict[str, str]]:
231+
"""Query the SPICE data archive via the /spice-query endpoint.
232+
233+
Parameters
234+
----------
235+
start_date : str, optional
236+
Start date in YYYYMMDD format.
237+
end_date : str, optional
238+
End date in YYYYMMDD format.
239+
ingestion_start_date : str, optional
240+
Ingestion start date in YYYYMMDD format.
241+
ingestion_end_date : str, optional
242+
Ingestion end date in YYYYMMDD format.
243+
type : str, optional
244+
SPICE kernel type (e.g. ``ephemeris_predicted``).
245+
version : str, optional
246+
Version in the format ``vXXX`` or ``latest``.
247+
248+
Returns
249+
-------
250+
list
251+
List of SPICE files matching the query
252+
"""
253+
# locals() gives us the keyword arguments passed to the function
254+
# and allows us to filter out the None values
255+
query_params = {key: value for key, value in locals().items() if value is not None}
256+
logger.debug("Input query parameters: %s", query_params)
257+
258+
# removing version from query if it is 'latest',
259+
# ensuring other parameters are passed
260+
if version == "latest":
261+
del query_params["version"]
262+
if not query_params:
263+
raise ValueError("One other parameter must be run with 'version'")
264+
query_params["latest"] = "true"
265+
266+
if "type" not in query_params:
267+
raise ValueError(
268+
"The 'type' parameter is required for SPICE queries. "
269+
"Run 'query -h' for more information."
270+
)
271+
272+
# Remap ingestion date params to /spice-query naming convention
273+
if "ingestion_start_date" in query_params:
274+
query_params["start_ingest_date"] = query_params.pop("ingestion_start_date")
275+
if "ingestion_end_date" in query_params:
276+
query_params["end_ingest_date"] = query_params.pop("ingestion_end_date")
277+
278+
url = f"{_get_base_url()}/spice-query"
279+
request = requests.Request(method="GET", url=url, params=query_params).prepare()
280+
281+
logger.info("Querying data archive for %s with url %s", query_params, request.url)
282+
with _make_request(request) as response:
283+
# Decode the JSON response as a list of items
284+
items = response.json()
285+
logger.debug("Received JSON: %s", items)
286+
287+
return items
288+
289+
222290
def query(
223291
*,
224292
table: Optional[str] = "science",

tests/test_cli.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,29 @@ def test_cli_works():
1616
cli.main()
1717

1818

19+
def test_cli_spice_query(capsys):
20+
"""Test the CLI SPICE query command."""
21+
with mock.patch.object(
22+
sys,
23+
"argv",
24+
[
25+
"imap-data-access",
26+
"query",
27+
"--table",
28+
"spice",
29+
"--type",
30+
"ephemeris_predicted",
31+
],
32+
):
33+
with mock.patch(
34+
"imap_data_access.cli.spice_query", return_value=[]
35+
) as mock_spice_query:
36+
cli.main()
37+
captured = capsys.readouterr()
38+
assert "Found [0] matching files" in captured.out
39+
mock_spice_query.assert_called_once_with(type="ephemeris_predicted")
40+
41+
1942
def test_cli_error_message(capsys):
2043
"""Test the CLI error message when no arguments are passed."""
2144
with mock.patch.object(

0 commit comments

Comments
 (0)