Coverage for python/astro_metadata_translator/cli/astrometadata.py: 61%

98 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-03-28 02:59 -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. 

11 

12from __future__ import annotations 

13 

14__all__ = ("main",) 

15 

16import importlib 

17import logging 

18import os 

19from collections.abc import Sequence 

20 

21import click 

22 

23from ..bin.translate import translate_or_dump_headers 

24from ..bin.writeindex import write_index_files 

25from ..bin.writesidecar import write_sidecar_files 

26 

27# Default regex for finding data files 

28re_default = r"\.fit[s]?\b" 

29 

30log = logging.getLogger("astro_metadata_translator") 

31 

32PACKAGES_VAR = "METADATA_TRANSLATORS" 

33 

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) 

60 

61 

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) 

84 

85 logging.basicConfig(level=log_level) 

86 

87 # Traceback needs to be known to subcommands 

88 ctx.obj["TRACEBACK"] = traceback 

89 

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) 

94 

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) 

101 

102 

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" 

130 

131 okay, failed = translate_or_dump_headers(files, regex, hdrnum, ctx.obj["TRACEBACK"], output_mode=mode) 

132 

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) 

137 

138 if not okay: 

139 # Good status if anything was returned in okay 

140 raise click.exceptions.Exit(1) 

141 

142 

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) 

161 

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) 

166 

167 if not okay: 

168 # Good status if anything was returned in okay 

169 raise click.exceptions.Exit(1) 

170 

171 

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"]) 

181 

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) 

186 

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) 

191 

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) 

196 

197 

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 ) 

219 

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) 

224 

225 if not okay: 

226 # Good status if anything was returned in okay 

227 raise click.exceptions.Exit(1)