imap-data-access 0.15.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,99 @@
1
+ """Data Access for the IMAP Mission.
2
+
3
+ The Interstellar Mapping and Acceleration Probe (IMAP) is a NASA mission to study the
4
+ heliosphere. This package contains the data access tools for the IMAP mission. It
5
+ provides a convenient way to query the IMAP data archive and download data files.
6
+ """
7
+
8
+ import os
9
+ from pathlib import Path
10
+
11
+ from imap_data_access.file_validation import (
12
+ AncillaryFilePath,
13
+ ImapFilePath,
14
+ ScienceFilePath,
15
+ SPICEFilePath,
16
+ )
17
+ from imap_data_access.io import download, query, upload
18
+
19
+ __all__ = [
20
+ "query",
21
+ "download",
22
+ "upload",
23
+ "ImapFilePath",
24
+ "ScienceFilePath",
25
+ "SPICEFilePath",
26
+ "AncillaryFilePath",
27
+ "VALID_INSTRUMENTS",
28
+ "VALID_DATALEVELS",
29
+ "VALID_FILE_EXTENSION",
30
+ "FILENAME_CONVENTION",
31
+ ]
32
+
33
+ __version__ = "0.15.1"
34
+
35
+
36
+ config = {
37
+ "DATA_ACCESS_URL": os.getenv("IMAP_DATA_ACCESS_URL")
38
+ or "https://api.dev.imap-mission.com",
39
+ "DATA_DIR": Path(os.getenv("IMAP_DATA_DIR") or Path.cwd() / "data"),
40
+ "API_KEY": os.getenv("IMAP_API_KEY"),
41
+ }
42
+ """Settings configuration dictionary.
43
+
44
+ DATA_ACCESS_URL : This is the URL of the data access API.
45
+ DATA_DIR : This is where the file data is stored and organized by instrument and level.
46
+ The default location is a 'data/' folder in the current working directory,
47
+ "but this can be set on the command line using the --data-dir option, or through
48
+ the environment variable IMAP_DATA_DIR.
49
+ API_KEY : This is the API key used to authenticate with the data access API.
50
+ It can be set on the command line using the --api-key option, or through the
51
+ environment variable IMAP_API_KEY. It is only necessary for uploading files.
52
+ """
53
+
54
+
55
+ VALID_INSTRUMENTS = {
56
+ "codice",
57
+ "glows",
58
+ "hit",
59
+ "hi",
60
+ "idex",
61
+ "lo",
62
+ "mag",
63
+ "swapi",
64
+ "swe",
65
+ "ultra",
66
+ }
67
+
68
+ VALID_DATALEVELS = {
69
+ "l0",
70
+ "l1",
71
+ "l1a",
72
+ "l1b",
73
+ "l1c",
74
+ "l1ca",
75
+ "l1cb",
76
+ "l1d",
77
+ "l2",
78
+ "l2a",
79
+ "l2b",
80
+ "l3",
81
+ "l3a",
82
+ "l3b",
83
+ "l3c",
84
+ "l3d",
85
+ }
86
+
87
+ VALID_FILE_EXTENSION = {"pkts", "cdf"}
88
+
89
+ FILENAME_CONVENTION = (
90
+ "<mission>_<instrument>_<datalevel>_<descriptor>_"
91
+ "<startdate>(-<repointing>)_<version>.<extension>"
92
+ )
93
+
94
+ ANCILLARY_FILENAME_CONVENTION = (
95
+ "<mission>_<instrument>_<description>_"
96
+ "<start_date>(_<end_date>)_<version>.<extension>"
97
+ )
98
+
99
+ VALID_ANCILLARY_FILE_EXTENSION = {"cdf", "csv", "json"}
@@ -0,0 +1,392 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """Command line interface to the IMAP Data Access API.
4
+
5
+ This module serves as a command line utility to invoke the IMAP Data Access API.
6
+ It provides the ability to interact with the Science Data Center (SDC)
7
+ by querying, downloading, and uploading files to the data center.
8
+
9
+ Use
10
+ ---
11
+ imap-data-access <command> [<args>]
12
+ imap-data-access --help
13
+ imap-data-access download <file_path>
14
+ imap-data-access query <query-parameters>
15
+ imap-data-access upload <file_path>
16
+ """
17
+
18
+ import argparse
19
+ import logging
20
+ import os
21
+ from pathlib import Path
22
+
23
+ import imap_data_access
24
+ from imap_data_access.file_validation import ScienceFilePath
25
+
26
+
27
+ def _download_parser(args: argparse.Namespace):
28
+ """Download a file from the IMAP SDC.
29
+
30
+ Parameters
31
+ ----------
32
+ args : argparse.Namespace
33
+ An object containing the parsed arguments and their values
34
+ """
35
+ output_path = imap_data_access.download(args.file_path)
36
+ print(f"Successfully downloaded the file to: {output_path}")
37
+
38
+
39
+ def _print_query_results_table(query_results: list[dict]):
40
+ """Print the query results in a table.
41
+
42
+ Parameters
43
+ ----------
44
+ query_results : list
45
+ A list of dictionaries containing the query results
46
+ """
47
+ num_files = len(query_results)
48
+ print(f"Found [{num_files}] matching files")
49
+ if num_files == 0:
50
+ return
51
+
52
+ # Use the keys of the first item in query_results for the header
53
+ headers = [
54
+ "Instrument",
55
+ "Data Level",
56
+ "Descriptor",
57
+ "Start Date",
58
+ "Repointing",
59
+ "Version",
60
+ "Filename",
61
+ ]
62
+
63
+ # Calculate the maximum width for each column based on the header and the data
64
+ column_widths = {}
65
+ for header in headers[:-1]:
66
+ column_widths[header] = max(
67
+ len(header),
68
+ *(len(str(item.get(header.lower(), ""))) for item in query_results),
69
+ )
70
+ # Calculate the maximum width for each column based on the header and the data
71
+
72
+ column_widths["Filename"] = max(
73
+ len("Filename"),
74
+ *(
75
+ len(os.path.basename(item.get("file_path", "")))
76
+ for item in query_results
77
+ ),
78
+ )
79
+
80
+ # Create the format string dynamically based on the number of columns
81
+ format_string = (
82
+ "| "
83
+ + " | ".join([f"{{:<{column_widths[header]}}}" for header in headers])
84
+ + " |"
85
+ )
86
+
87
+ # Add hyphens for a separator between header and data
88
+ hyphens = "|" + "-" * (sum(column_widths.values()) + 3 * len(headers) - 1) + "|"
89
+ print(hyphens)
90
+
91
+ # Print header
92
+ print(format_string.format(*headers))
93
+ print(hyphens)
94
+
95
+ # Print data
96
+ for item in query_results:
97
+ values = [
98
+ str(item.get("instrument", "")),
99
+ str(item.get("data_level", "")),
100
+ str(item.get("descriptor", "")),
101
+ str(item.get("start_date", "")),
102
+ str(item.get("repointing", "")) or "",
103
+ str(item.get("version", "")),
104
+ os.path.basename(item.get("file_path", "")),
105
+ ]
106
+ print(format_string.format(*values))
107
+
108
+ # Close the table
109
+ print(hyphens)
110
+
111
+
112
+ def _query_parser(args: argparse.Namespace):
113
+ """Query the IMAP SDC.
114
+
115
+ Parameters
116
+ ----------
117
+ args : argparse.Namespace
118
+ An object containing the parsed arguments and their values
119
+ """
120
+ # Filter to get the arguments of interest from the namespace
121
+ valid_args = [
122
+ "instrument",
123
+ "data_level",
124
+ "descriptor",
125
+ "start_date",
126
+ "end_date",
127
+ "repointing",
128
+ "version",
129
+ "extension",
130
+ "filename",
131
+ ]
132
+
133
+ query_params = {
134
+ key: value
135
+ for key, value in vars(args).items()
136
+ if key in valid_args and value is not None
137
+ }
138
+
139
+ # Checking to see if a filename was passed.
140
+ if args.filename is not None:
141
+ del query_params["filename"]
142
+ if query_params:
143
+ raise TypeError("Too many arguments, '--filename' should be ran by itself")
144
+
145
+ file_path = ScienceFilePath(args.filename)
146
+ query_params = {
147
+ "instrument": file_path.instrument,
148
+ "data_level": file_path.data_level,
149
+ "descriptor": file_path.descriptor,
150
+ "start_date": file_path.start_date,
151
+ "repointing": file_path.repointing,
152
+ "version": file_path.version,
153
+ "extension": file_path.extension,
154
+ }
155
+
156
+ query_results = imap_data_access.query(**query_params)
157
+
158
+ if args.output_format == "table":
159
+ _print_query_results_table(query_results)
160
+ elif args.output_format == "json":
161
+ # Dump the content directly
162
+ print(query_results)
163
+
164
+
165
+ def _upload_parser(args: argparse.Namespace):
166
+ """Upload a file to the IMAP SDC.
167
+
168
+ Parameters
169
+ ----------
170
+ args : argparse.Namespace
171
+ An object containing the parsed arguments and their values
172
+ """
173
+ imap_data_access.upload(args.file_path)
174
+ print("Successfully uploaded the file to the IMAP SDC")
175
+
176
+
177
+ # PLR0915: too many statements
178
+ def main(): # noqa: PLR0915
179
+ """Parse the command line arguments.
180
+
181
+ Run the command line interface to the IMAP Data Access API.
182
+ """
183
+ api_key_help = (
184
+ "API key to authenticate with the IMAP SDC. "
185
+ "This can also be set using the IMAP_API_KEY environment variable. "
186
+ "It is only necessary for uploading files."
187
+ )
188
+ data_dir_help = (
189
+ "Directory to use for reading and writing IMAP data. "
190
+ "The default is a 'data/' folder in the "
191
+ "current working directory. This can also be "
192
+ "set using the IMAP_DATA_DIR environment variable."
193
+ )
194
+ description = (
195
+ "This command line program accesses the IMAP SDC APIs to query, download, "
196
+ "and upload data files."
197
+ )
198
+ download_help = (
199
+ "Download a file from the IMAP SDC to the locally configured data directory. "
200
+ "Run 'download -h' for more information. "
201
+ )
202
+ help_menu_for_download = (
203
+ "Download a file from the IMAP SDC to the locally configured data directory. "
204
+ )
205
+ file_path_help = (
206
+ "This must be the full path to the file."
207
+ "\nE.g. imap/mag/l0/2025/01/imap_mag_l0_raw_20250101_v001.pkts"
208
+ )
209
+ query_help = (
210
+ "Query the IMAP SDC for files matching the query parameters. "
211
+ "The query parameters are optional, but at least one must be provided. "
212
+ "Run 'query -h' for more information."
213
+ )
214
+ help_menu_for_query = (
215
+ "Query the IMAP SDC for files matching the query parameters. "
216
+ "The query parameters are optional, but at least one must be provided. "
217
+ )
218
+ upload_help = (
219
+ "Upload a file to the IMAP SDC. This must be the full path to the file."
220
+ "\nE.g. imap/mag/l0/2025/01/imap_mag_l0_raw_20250101_v001.pkts. "
221
+ "Run 'upload -h' for more information."
222
+ )
223
+ help_menu_for_upload = (
224
+ "Upload a file to the IMAP SDC. This must be the full path to the file."
225
+ "\nE.g. imap/mag/l0/2025/01/imap_mag_l0_raw_20250101_v001.pkts. "
226
+ )
227
+ url_help = (
228
+ "URL of the IMAP SDC API. "
229
+ "The default is https://api.dev.imap-mission.com. This can also be "
230
+ "set using the IMAP_DATA_ACCESS_URL environment variable."
231
+ )
232
+
233
+ parser = argparse.ArgumentParser(prog="imap-data-access", description=description)
234
+ parser.add_argument(
235
+ "--version",
236
+ action="version",
237
+ version=f"%(prog)s {imap_data_access.__version__}",
238
+ help="Show programs version number and exit. No other parameters needed.",
239
+ )
240
+ parser.add_argument("--api-key", type=str, required=False, help=api_key_help)
241
+ parser.add_argument("--data-dir", type=Path, required=False, help=data_dir_help)
242
+ parser.add_argument("--url", type=str, required=False, help=url_help)
243
+ # Logging level
244
+ parser.add_argument(
245
+ "--debug",
246
+ help="Print lots of debugging statements.",
247
+ action="store_const",
248
+ dest="loglevel",
249
+ const=logging.DEBUG,
250
+ default=logging.WARNING,
251
+ )
252
+ parser.add_argument(
253
+ "-v",
254
+ "--verbose",
255
+ help="Add verbose output",
256
+ action="store_const",
257
+ dest="loglevel",
258
+ const=logging.INFO,
259
+ )
260
+
261
+ # Download command
262
+ subparsers = parser.add_subparsers(required=True)
263
+ parser_download = subparsers.add_parser(
264
+ "download", help=download_help, description=help_menu_for_download
265
+ )
266
+ parser_download.add_argument("file_path", type=Path, help=file_path_help)
267
+ parser_download.set_defaults(func=_download_parser)
268
+
269
+ # Query command (with optional arguments)
270
+ query_parser = subparsers.add_parser(
271
+ "query", help=query_help, description=help_menu_for_query
272
+ )
273
+ query_parser.add_argument(
274
+ "--instrument",
275
+ type=str,
276
+ required=False,
277
+ help="Name of the instrument",
278
+ choices=[
279
+ "codice",
280
+ "glows",
281
+ "hi",
282
+ "hit",
283
+ "idex",
284
+ "lo",
285
+ "mag",
286
+ "swapi",
287
+ "swe",
288
+ "ultra",
289
+ ],
290
+ )
291
+ query_parser.add_argument(
292
+ "--data-level",
293
+ type=str,
294
+ required=False,
295
+ help="Data level of the product (l0, l1a, l2, etc.)",
296
+ )
297
+ query_parser.add_argument(
298
+ "--descriptor",
299
+ type=str,
300
+ required=False,
301
+ help="Descriptor of the product (raw, burst, etc.)",
302
+ )
303
+ query_parser.add_argument(
304
+ "--start-date",
305
+ type=str,
306
+ required=False,
307
+ help="Start date for files in YYYYMMDD format",
308
+ )
309
+ query_parser.add_argument(
310
+ "--end-date",
311
+ type=str,
312
+ required=False,
313
+ help="End date for a range of file timestamps in YYYYMMDD format",
314
+ )
315
+ query_parser.add_argument(
316
+ "--repointing", type=int, required=False, help="Repointing number (int)"
317
+ )
318
+ query_parser.add_argument(
319
+ "--version",
320
+ type=str,
321
+ required=False,
322
+ help="Version of the product in the format 'v001'."
323
+ " Must have one other parameter to run."
324
+ " Passing 'latest' will return latest version of a file",
325
+ )
326
+ query_parser.add_argument(
327
+ "--extension", type=str, required=False, help="File extension (cdf, pkts)"
328
+ )
329
+ query_parser.add_argument(
330
+ "--output-format",
331
+ type=str,
332
+ required=False,
333
+ help="How to format the output, default is 'table'",
334
+ choices=["table", "json"],
335
+ default="table",
336
+ )
337
+ query_parser.add_argument(
338
+ "--filename",
339
+ type=str,
340
+ required=False,
341
+ help="Name of a file to be searched for. For convention standards see https://imap-"
342
+ "processing.readthedocs.io/en/latest/development-guide/style-guide/naming-conventions"
343
+ ".html#data-product-file-naming-conventions",
344
+ )
345
+ query_parser.set_defaults(func=_query_parser)
346
+
347
+ # Upload command
348
+ parser_upload = subparsers.add_parser(
349
+ "upload", help=upload_help, description=help_menu_for_upload
350
+ )
351
+ parser_upload.add_argument("file_path", type=Path, help=file_path_help)
352
+ parser_upload.set_defaults(func=_upload_parser)
353
+
354
+ # Parse the arguments and set the values
355
+ try:
356
+ args = parser.parse_args()
357
+ except TypeError:
358
+ parser.exit(
359
+ status=1,
360
+ message="Please provide input parameters, "
361
+ "or use '-h' for more information.",
362
+ )
363
+
364
+ logging.basicConfig(level=args.loglevel)
365
+
366
+ if args.data_dir:
367
+ # We got an explicit data directory from the command line
368
+ data_path = args.data_dir.resolve()
369
+ if not data_path.exists():
370
+ parser.error(f"Data directory {args.data_dir} does not exist")
371
+ # Set the data directory to the user-supplied value
372
+ imap_data_access.config["DATA_DIR"] = data_path
373
+
374
+ if args.url:
375
+ # We got an explicit url from the command line
376
+ imap_data_access.config["DATA_ACCESS_URL"] = args.url
377
+
378
+ if args.api_key:
379
+ # We got an explicit api key from the command line
380
+ imap_data_access.config["API_KEY"] = args.api_key
381
+
382
+ # Now process through the respective function for the invoked command
383
+ # (set with set_defaults on the subparsers above)
384
+ try:
385
+ args.func(args)
386
+ except Exception as e:
387
+ # Make sure we are exiting with non-zero exit code and printing the message
388
+ parser.exit(status=1, message=str(e) + "\n")
389
+
390
+
391
+ if __name__ == "__main__":
392
+ main()