Coverage for python/astro_metadata_translator/cli/astrometadata.py: 61%
98 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-06 03:48 -0700
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-06 03:48 -0700
1# This file is part of astro_metadata_translator.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (http://www.lsst.org).
6# See the LICENSE file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# Use of this source code is governed by a 3-clause BSD-style
10# license that can be found in the LICENSE file.
12from __future__ import annotations
14__all__ = ("main",)
16import importlib
17import logging
18import os
19from collections.abc import Sequence
21import click
23from ..bin.translate import translate_or_dump_headers
24from ..bin.writeindex import write_index_files
25from ..bin.writesidecar import write_sidecar_files
27# Default regex for finding data files
28re_default = r"\.fit[s]?\b"
30log = logging.getLogger("astro_metadata_translator")
32PACKAGES_VAR = "METADATA_TRANSLATORS"
34hdrnum_option = click.option(
35 "-n",
36 "--hdrnum",
37 default=-1,
38 help="HDU number to read. If the HDU can not be found, a warning is issued but"
39 " reading is attempted using the primary header. The primary header is"
40 " always read and merged with this header. Negative number (the default) "
41 " indicates that the second header will be merged if the FITS file supports"
42 " extended FITS.",
43)
44regex_option = click.option(
45 "-r",
46 "--regex",
47 default=re_default,
48 help="When looking in a directory, regular expression to use to determine whether"
49 f" a file should be examined. Default: '{re_default}'",
50)
51content_option = click.option(
52 "-c",
53 "--content",
54 default="translated",
55 type=click.Choice(["translated", "metadata"], case_sensitive=False),
56 help="Content to store in JSON file. Options are: "
57 "'translated' stores translated metadata in the file; "
58 "'metadata' stores raw FITS headers in the file.",
59)
62@click.group(name="astrometadata", context_settings=dict(help_option_names=["-h", "--help"]))
63@click.option(
64 "--log-level",
65 type=click.Choice(["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"], case_sensitive=False),
66 default="INFO",
67 help="Python logging level to use.",
68)
69@click.option(
70 "--traceback/--no-traceback", default=False, help="Give detailed trace back when any errors encountered."
71)
72@click.option(
73 "-p",
74 "--packages",
75 multiple=True,
76 help="Python packages to import to register additional translators. This is in addition"
77 f" to any packages specified in the {PACKAGES_VAR} environment variable (colon-separated"
78 " python module names).",
79)
80@click.pass_context
81def main(ctx: click.Context, log_level: int, traceback: bool, packages: Sequence[str]) -> None:
82 """Execute main click command-line."""
83 ctx.ensure_object(dict)
85 logging.basicConfig(level=log_level)
87 # Traceback needs to be known to subcommands
88 ctx.obj["TRACEBACK"] = traceback
90 packages_set = set(packages)
91 if PACKAGES_VAR in os.environ:
92 new_packages = os.environ[PACKAGES_VAR].split(":")
93 packages_set.update(new_packages)
95 # Process import requests
96 for m in packages_set:
97 try:
98 importlib.import_module(m)
99 except (ImportError, ModuleNotFoundError):
100 log.warning("Failed to import translator module: %s", m)
103@main.command(help="Translate metadata in supplied files and report.")
104@click.argument("files", nargs=-1)
105@click.option(
106 "-q",
107 "--quiet/--no-quiet",
108 default=False,
109 help="Do not report the translation content from each header. Only report failures.",
110)
111@hdrnum_option
112@click.option(
113 "-m",
114 "--mode",
115 default="auto",
116 type=click.Choice(["auto", "verbose", "table"], case_sensitive=False),
117 help="Output mode. 'verbose' prints all available information for each file found."
118 " 'table' uses tabular output for a cutdown set of metadata."
119 " 'auto' uses 'verbose' if one file found and 'table' if more than one is found.",
120)
121@regex_option
122@click.pass_context
123def translate(
124 ctx: click.Context, files: Sequence[str], quiet: bool, hdrnum: int, mode: str, regex: str
125) -> None:
126 """Translate a header."""
127 # For quiet mode we want to translate everything but report nothing.
128 if quiet:
129 mode = "none"
131 okay, failed = translate_or_dump_headers(files, regex, hdrnum, ctx.obj["TRACEBACK"], output_mode=mode)
133 if failed:
134 click.echo("Files with failed translations:", err=True)
135 for f in failed:
136 click.echo(f"\t{f}", err=True)
138 if not okay:
139 # Good status if anything was returned in okay
140 raise click.exceptions.Exit(1)
143@main.command(help="Dump data header to standard out in YAML format.")
144@click.argument("files", nargs=-1)
145@hdrnum_option
146@click.option(
147 "-m",
148 "--mode",
149 default="yaml",
150 type=click.Choice(["yaml", "fixed", "yamlnative", "fixexnative"], case_sensitive=False),
151 help="Output mode. 'yaml' dumps the header in YAML format (this is the default)."
152 " 'fixed' dumps the header in YAML format after applying header corrections."
153 " 'yamlnative' is as for 'yaml' but dumps the native (astropy vs PropertyList) native form."
154 " 'yamlfixed' is as for 'fixed' but dumps the native (astropy vs PropertyList) native form.",
155)
156@regex_option
157@click.pass_context
158def dump(ctx: click.Context, files: Sequence[str], hdrnum: int, mode: str, regex: str) -> None:
159 """Dump a header."""
160 okay, failed = translate_or_dump_headers(files, regex, hdrnum, ctx.obj["TRACEBACK"], output_mode=mode)
162 if failed:
163 click.echo("Files with failed header extraction:", err=True)
164 for f in failed:
165 click.echo(f"\t{f}", err=True)
167 if not okay:
168 # Good status if anything was returned in okay
169 raise click.exceptions.Exit(1)
172@main.command(help="Write JSON sidecar files alongside each data file.")
173@click.argument("files", nargs=-1)
174@hdrnum_option
175@regex_option
176@content_option
177@click.pass_context
178def write_sidecar(ctx: click.Context, files: Sequence[str], hdrnum: int, regex: str, content: str) -> None:
179 """Write a sidecar file with header information."""
180 okay, failed = write_sidecar_files(files, regex, hdrnum, content, ctx.obj["TRACEBACK"])
182 if failed:
183 click.echo("Files with failed header extraction:", err=True)
184 for f in failed:
185 click.echo(f"\t{f}", err=True)
187 if not okay and not failed:
188 # No files found at all.
189 click.echo("Found no files matching regex.")
190 raise click.exceptions.Exit(1)
192 if not okay:
193 # Good status if anything was returned in okay
194 click.echo(f"No files processed successfully. Found {len(failed)}.", err=True)
195 raise click.exceptions.Exit(1)
198@main.command(help="Write JSON index file for entire directory.")
199@click.argument("files", nargs=-1)
200@hdrnum_option
201@regex_option
202@content_option
203@click.option(
204 "-o",
205 "--outpath",
206 type=str,
207 default=None,
208 help="If given, write a single index with all information in specified location."
209 " Default is to write one index per directory where files are located.",
210)
211@click.pass_context
212def write_index(
213 ctx: click.Context, files: Sequence[str], hdrnum: int, regex: str, content: str, outpath: str
214) -> None:
215 """Write a header index file."""
216 okay, failed = write_index_files(
217 files, regex, hdrnum, ctx.obj["TRACEBACK"], content_mode=content, outpath=outpath
218 )
220 if failed:
221 click.echo("Files with failed header extraction:", err=True)
222 for f in failed:
223 click.echo(f"\t{f}", err=True)
225 if not okay:
226 # Good status if anything was returned in okay
227 raise click.exceptions.Exit(1)