pyreduce-astro 0.7a4__cp314-cp314-win_amd64.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.
Files changed (182) hide show
  1. pyreduce/__init__.py +67 -0
  2. pyreduce/__main__.py +322 -0
  3. pyreduce/cli.py +342 -0
  4. pyreduce/clib/Release/_slitfunc_2d.cp311-win_amd64.exp +0 -0
  5. pyreduce/clib/Release/_slitfunc_2d.cp311-win_amd64.lib +0 -0
  6. pyreduce/clib/Release/_slitfunc_2d.cp312-win_amd64.exp +0 -0
  7. pyreduce/clib/Release/_slitfunc_2d.cp312-win_amd64.lib +0 -0
  8. pyreduce/clib/Release/_slitfunc_2d.cp313-win_amd64.exp +0 -0
  9. pyreduce/clib/Release/_slitfunc_2d.cp313-win_amd64.lib +0 -0
  10. pyreduce/clib/Release/_slitfunc_2d.cp314-win_amd64.exp +0 -0
  11. pyreduce/clib/Release/_slitfunc_2d.cp314-win_amd64.lib +0 -0
  12. pyreduce/clib/Release/_slitfunc_2d.obj +0 -0
  13. pyreduce/clib/Release/_slitfunc_bd.cp311-win_amd64.exp +0 -0
  14. pyreduce/clib/Release/_slitfunc_bd.cp311-win_amd64.lib +0 -0
  15. pyreduce/clib/Release/_slitfunc_bd.cp312-win_amd64.exp +0 -0
  16. pyreduce/clib/Release/_slitfunc_bd.cp312-win_amd64.lib +0 -0
  17. pyreduce/clib/Release/_slitfunc_bd.cp313-win_amd64.exp +0 -0
  18. pyreduce/clib/Release/_slitfunc_bd.cp313-win_amd64.lib +0 -0
  19. pyreduce/clib/Release/_slitfunc_bd.cp314-win_amd64.exp +0 -0
  20. pyreduce/clib/Release/_slitfunc_bd.cp314-win_amd64.lib +0 -0
  21. pyreduce/clib/Release/_slitfunc_bd.obj +0 -0
  22. pyreduce/clib/__init__.py +0 -0
  23. pyreduce/clib/_slitfunc_2d.cp311-win_amd64.pyd +0 -0
  24. pyreduce/clib/_slitfunc_2d.cp312-win_amd64.pyd +0 -0
  25. pyreduce/clib/_slitfunc_2d.cp313-win_amd64.pyd +0 -0
  26. pyreduce/clib/_slitfunc_2d.cp314-win_amd64.pyd +0 -0
  27. pyreduce/clib/_slitfunc_bd.cp311-win_amd64.pyd +0 -0
  28. pyreduce/clib/_slitfunc_bd.cp312-win_amd64.pyd +0 -0
  29. pyreduce/clib/_slitfunc_bd.cp313-win_amd64.pyd +0 -0
  30. pyreduce/clib/_slitfunc_bd.cp314-win_amd64.pyd +0 -0
  31. pyreduce/clib/build_extract.py +75 -0
  32. pyreduce/clib/slit_func_2d_xi_zeta_bd.c +1313 -0
  33. pyreduce/clib/slit_func_2d_xi_zeta_bd.h +55 -0
  34. pyreduce/clib/slit_func_bd.c +362 -0
  35. pyreduce/clib/slit_func_bd.h +17 -0
  36. pyreduce/clipnflip.py +147 -0
  37. pyreduce/combine_frames.py +861 -0
  38. pyreduce/configuration.py +191 -0
  39. pyreduce/continuum_normalization.py +329 -0
  40. pyreduce/cwrappers.py +404 -0
  41. pyreduce/datasets.py +238 -0
  42. pyreduce/echelle.py +413 -0
  43. pyreduce/estimate_background_scatter.py +130 -0
  44. pyreduce/extract.py +1362 -0
  45. pyreduce/extraction_width.py +77 -0
  46. pyreduce/instruments/__init__.py +0 -0
  47. pyreduce/instruments/aj.py +9 -0
  48. pyreduce/instruments/aj.yaml +51 -0
  49. pyreduce/instruments/andes.py +102 -0
  50. pyreduce/instruments/andes.yaml +72 -0
  51. pyreduce/instruments/common.py +711 -0
  52. pyreduce/instruments/common.yaml +57 -0
  53. pyreduce/instruments/crires_plus.py +103 -0
  54. pyreduce/instruments/crires_plus.yaml +101 -0
  55. pyreduce/instruments/filters.py +195 -0
  56. pyreduce/instruments/harpn.py +203 -0
  57. pyreduce/instruments/harpn.yaml +140 -0
  58. pyreduce/instruments/harps.py +312 -0
  59. pyreduce/instruments/harps.yaml +144 -0
  60. pyreduce/instruments/instrument_info.py +140 -0
  61. pyreduce/instruments/jwst_miri.py +29 -0
  62. pyreduce/instruments/jwst_miri.yaml +53 -0
  63. pyreduce/instruments/jwst_niriss.py +98 -0
  64. pyreduce/instruments/jwst_niriss.yaml +60 -0
  65. pyreduce/instruments/lick_apf.py +35 -0
  66. pyreduce/instruments/lick_apf.yaml +60 -0
  67. pyreduce/instruments/mcdonald.py +123 -0
  68. pyreduce/instruments/mcdonald.yaml +56 -0
  69. pyreduce/instruments/metis_ifu.py +45 -0
  70. pyreduce/instruments/metis_ifu.yaml +62 -0
  71. pyreduce/instruments/metis_lss.py +45 -0
  72. pyreduce/instruments/metis_lss.yaml +62 -0
  73. pyreduce/instruments/micado.py +45 -0
  74. pyreduce/instruments/micado.yaml +62 -0
  75. pyreduce/instruments/models.py +257 -0
  76. pyreduce/instruments/neid.py +156 -0
  77. pyreduce/instruments/neid.yaml +61 -0
  78. pyreduce/instruments/nirspec.py +215 -0
  79. pyreduce/instruments/nirspec.yaml +63 -0
  80. pyreduce/instruments/nte.py +42 -0
  81. pyreduce/instruments/nte.yaml +55 -0
  82. pyreduce/instruments/uves.py +46 -0
  83. pyreduce/instruments/uves.yaml +65 -0
  84. pyreduce/instruments/xshooter.py +39 -0
  85. pyreduce/instruments/xshooter.yaml +63 -0
  86. pyreduce/make_shear.py +607 -0
  87. pyreduce/masks/mask_crires_plus_det1.fits.gz +0 -0
  88. pyreduce/masks/mask_crires_plus_det2.fits.gz +0 -0
  89. pyreduce/masks/mask_crires_plus_det3.fits.gz +0 -0
  90. pyreduce/masks/mask_ctio_chiron.fits.gz +0 -0
  91. pyreduce/masks/mask_elodie.fits.gz +0 -0
  92. pyreduce/masks/mask_feros3.fits.gz +0 -0
  93. pyreduce/masks/mask_flames_giraffe.fits.gz +0 -0
  94. pyreduce/masks/mask_harps_blue.fits.gz +0 -0
  95. pyreduce/masks/mask_harps_red.fits.gz +0 -0
  96. pyreduce/masks/mask_hds_blue.fits.gz +0 -0
  97. pyreduce/masks/mask_hds_red.fits.gz +0 -0
  98. pyreduce/masks/mask_het_hrs_2x5.fits.gz +0 -0
  99. pyreduce/masks/mask_jwst_miri_lrs_slitless.fits.gz +0 -0
  100. pyreduce/masks/mask_jwst_niriss_gr700xd.fits.gz +0 -0
  101. pyreduce/masks/mask_lick_apf_.fits.gz +0 -0
  102. pyreduce/masks/mask_mcdonald.fits.gz +0 -0
  103. pyreduce/masks/mask_nes.fits.gz +0 -0
  104. pyreduce/masks/mask_nirspec_nirspec.fits.gz +0 -0
  105. pyreduce/masks/mask_sarg.fits.gz +0 -0
  106. pyreduce/masks/mask_sarg_2x2a.fits.gz +0 -0
  107. pyreduce/masks/mask_sarg_2x2b.fits.gz +0 -0
  108. pyreduce/masks/mask_subaru_hds_red.fits.gz +0 -0
  109. pyreduce/masks/mask_uves_blue.fits.gz +0 -0
  110. pyreduce/masks/mask_uves_blue_binned_2_2.fits.gz +0 -0
  111. pyreduce/masks/mask_uves_middle.fits.gz +0 -0
  112. pyreduce/masks/mask_uves_middle_2x2_split.fits.gz +0 -0
  113. pyreduce/masks/mask_uves_middle_binned_2_2.fits.gz +0 -0
  114. pyreduce/masks/mask_uves_red.fits.gz +0 -0
  115. pyreduce/masks/mask_uves_red_2x2.fits.gz +0 -0
  116. pyreduce/masks/mask_uves_red_2x2_split.fits.gz +0 -0
  117. pyreduce/masks/mask_uves_red_binned_2_2.fits.gz +0 -0
  118. pyreduce/masks/mask_xshooter_nir.fits.gz +0 -0
  119. pyreduce/pipeline.py +619 -0
  120. pyreduce/rectify.py +138 -0
  121. pyreduce/reduce.py +2065 -0
  122. pyreduce/settings/settings_AJ.json +19 -0
  123. pyreduce/settings/settings_ANDES.json +89 -0
  124. pyreduce/settings/settings_CRIRES_PLUS.json +89 -0
  125. pyreduce/settings/settings_HARPN.json +73 -0
  126. pyreduce/settings/settings_HARPS.json +69 -0
  127. pyreduce/settings/settings_JWST_MIRI.json +55 -0
  128. pyreduce/settings/settings_JWST_NIRISS.json +55 -0
  129. pyreduce/settings/settings_LICK_APF.json +62 -0
  130. pyreduce/settings/settings_MCDONALD.json +58 -0
  131. pyreduce/settings/settings_METIS_IFU.json +77 -0
  132. pyreduce/settings/settings_METIS_LSS.json +77 -0
  133. pyreduce/settings/settings_MICADO.json +78 -0
  134. pyreduce/settings/settings_NEID.json +73 -0
  135. pyreduce/settings/settings_NIRSPEC.json +58 -0
  136. pyreduce/settings/settings_NTE.json +60 -0
  137. pyreduce/settings/settings_UVES.json +54 -0
  138. pyreduce/settings/settings_XSHOOTER.json +78 -0
  139. pyreduce/settings/settings_pyreduce.json +184 -0
  140. pyreduce/settings/settings_schema.json +850 -0
  141. pyreduce/tools/__init__.py +0 -0
  142. pyreduce/tools/combine.py +117 -0
  143. pyreduce/trace.py +979 -0
  144. pyreduce/util.py +1366 -0
  145. pyreduce/wavecal/MICADO_HK_3arcsec_chip5.npz +0 -0
  146. pyreduce/wavecal/atlas/thar.fits +4946 -13
  147. pyreduce/wavecal/atlas/thar_list.txt +4172 -0
  148. pyreduce/wavecal/atlas/une.fits +0 -0
  149. pyreduce/wavecal/convert.py +38 -0
  150. pyreduce/wavecal/crires_plus_J1228_Open_det1.npz +0 -0
  151. pyreduce/wavecal/crires_plus_J1228_Open_det2.npz +0 -0
  152. pyreduce/wavecal/crires_plus_J1228_Open_det3.npz +0 -0
  153. pyreduce/wavecal/harpn_harpn_2D.npz +0 -0
  154. pyreduce/wavecal/harps_blue_2D.npz +0 -0
  155. pyreduce/wavecal/harps_blue_pol_2D.npz +0 -0
  156. pyreduce/wavecal/harps_red_2D.npz +0 -0
  157. pyreduce/wavecal/harps_red_pol_2D.npz +0 -0
  158. pyreduce/wavecal/mcdonald.npz +0 -0
  159. pyreduce/wavecal/metis_lss_l_2D.npz +0 -0
  160. pyreduce/wavecal/metis_lss_m_2D.npz +0 -0
  161. pyreduce/wavecal/nirspec_K2.npz +0 -0
  162. pyreduce/wavecal/uves_blue_360nm_2D.npz +0 -0
  163. pyreduce/wavecal/uves_blue_390nm_2D.npz +0 -0
  164. pyreduce/wavecal/uves_blue_437nm_2D.npz +0 -0
  165. pyreduce/wavecal/uves_middle_2x2_2D.npz +0 -0
  166. pyreduce/wavecal/uves_middle_565nm_2D.npz +0 -0
  167. pyreduce/wavecal/uves_middle_580nm_2D.npz +0 -0
  168. pyreduce/wavecal/uves_middle_600nm_2D.npz +0 -0
  169. pyreduce/wavecal/uves_middle_665nm_2D.npz +0 -0
  170. pyreduce/wavecal/uves_middle_860nm_2D.npz +0 -0
  171. pyreduce/wavecal/uves_red_580nm_2D.npz +0 -0
  172. pyreduce/wavecal/uves_red_600nm_2D.npz +0 -0
  173. pyreduce/wavecal/uves_red_665nm_2D.npz +0 -0
  174. pyreduce/wavecal/uves_red_760nm_2D.npz +0 -0
  175. pyreduce/wavecal/uves_red_860nm_2D.npz +0 -0
  176. pyreduce/wavecal/xshooter_nir.npz +0 -0
  177. pyreduce/wavelength_calibration.py +1871 -0
  178. pyreduce_astro-0.7a4.dist-info/METADATA +106 -0
  179. pyreduce_astro-0.7a4.dist-info/RECORD +182 -0
  180. pyreduce_astro-0.7a4.dist-info/WHEEL +4 -0
  181. pyreduce_astro-0.7a4.dist-info/entry_points.txt +2 -0
  182. pyreduce_astro-0.7a4.dist-info/licenses/LICENSE +674 -0
pyreduce/__init__.py ADDED
@@ -0,0 +1,67 @@
1
+ try:
2
+ from importlib.metadata import PackageNotFoundError, version
3
+ except ImportError: # for Python<3.8
4
+ from importlib_metadata import PackageNotFoundError, version
5
+
6
+ try:
7
+ __version__ = version("pyreduce-astro")
8
+ except PackageNotFoundError:
9
+ __version__ = "unknown"
10
+
11
+
12
+ # add logger to console
13
+ import logging
14
+
15
+ import tqdm
16
+
17
+
18
+ # We need to use this to have logging messages handle properly with the progressbar
19
+ class TqdmLoggingHandler(logging.Handler):
20
+ def __init__(self, level=logging.NOTSET):
21
+ super().__init__(level)
22
+
23
+ def emit(self, record):
24
+ try:
25
+ msg = self.format(record)
26
+ tqdm.tqdm.write(msg)
27
+ self.flush()
28
+ except (KeyboardInterrupt, SystemExit):
29
+ raise
30
+ except:
31
+ self.handleError(record)
32
+
33
+
34
+ logger = logging.getLogger(__name__)
35
+ logger.setLevel(logging.DEBUG)
36
+ logging.captureWarnings(True)
37
+
38
+ console = TqdmLoggingHandler()
39
+ console.setLevel(logging.INFO)
40
+
41
+ try:
42
+ import colorlog
43
+
44
+ console.setFormatter(
45
+ colorlog.ColoredFormatter("%(log_color)s%(levelname)s - %(message)s")
46
+ )
47
+ del colorlog
48
+ except ImportError:
49
+ console.setFormatter("%(levelname)s - %(message)s")
50
+ print("Install colorlog for colored logging output")
51
+
52
+ logger.addHandler(console)
53
+
54
+ del logging
55
+ # do not del tqdm, it is needed in the Log Handler
56
+
57
+
58
+ # Lazy loading for faster imports
59
+ def __getattr__(name):
60
+ """Lazy load submodules on first access."""
61
+ if name in ("configuration", "datasets", "reduce", "util"):
62
+ import importlib
63
+
64
+ module = importlib.import_module(f".{name}", __name__)
65
+ globals()[name] = module
66
+ return module
67
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
pyreduce/__main__.py ADDED
@@ -0,0 +1,322 @@
1
+ """
2
+ PyReduce command-line interface.
3
+
4
+ Usage:
5
+ uv run reduce --help
6
+ uv run reduce run UVES HD132205 --night 2010-04-01
7
+ uv run reduce run UVES HD132205 --steps bias,flat,orders
8
+ uv run reduce bias UVES HD132205
9
+ uv run reduce combine --output combined.fits *.final.fits
10
+ """
11
+
12
+ import click
13
+
14
+ ALL_STEPS = (
15
+ "bias",
16
+ "flat",
17
+ "orders",
18
+ "curvature",
19
+ "scatter",
20
+ "norm_flat",
21
+ "wavecal_master",
22
+ "wavecal_init",
23
+ "wavecal",
24
+ "freq_comb_master",
25
+ "freq_comb",
26
+ "science",
27
+ "continuum",
28
+ "finalize",
29
+ )
30
+
31
+
32
+ @click.group()
33
+ @click.version_option(package_name="pyreduce-astro")
34
+ def cli():
35
+ """PyReduce - Echelle spectrograph data reduction pipeline."""
36
+ pass
37
+
38
+
39
+ @cli.command()
40
+ @click.argument("instrument")
41
+ @click.argument("target")
42
+ @click.option("--night", "-n", default=None, help="Observation night (YYYY-MM-DD)")
43
+ @click.option("--arm", "-a", default=None, help="Instrument arm/detector")
44
+ @click.option(
45
+ "--steps",
46
+ "-s",
47
+ default=None,
48
+ help="Comma-separated steps to run (default: all)",
49
+ )
50
+ @click.option(
51
+ "--base-dir",
52
+ "-b",
53
+ default=None,
54
+ help="Base directory for data (default: $REDUCE_DATA or ~/REDUCE_DATA)",
55
+ )
56
+ @click.option(
57
+ "--input-dir", "-i", default="raw", help="Input directory relative to base"
58
+ )
59
+ @click.option(
60
+ "--output-dir", "-o", default="reduced", help="Output directory relative to base"
61
+ )
62
+ @click.option(
63
+ "--plot", "-p", default=0, help="Plot level (0=none, 1=save, 2=interactive)"
64
+ )
65
+ @click.option(
66
+ "--order-range",
67
+ default=None,
68
+ help="Order range to process (e.g., '1,21')",
69
+ )
70
+ def run(
71
+ instrument,
72
+ target,
73
+ night,
74
+ arm,
75
+ steps,
76
+ base_dir,
77
+ input_dir,
78
+ output_dir,
79
+ plot,
80
+ order_range,
81
+ ):
82
+ """Run the reduction pipeline.
83
+
84
+ INSTRUMENT: Name of the instrument (e.g., UVES, HARPS, XSHOOTER)
85
+ TARGET: Target star name or regex pattern
86
+ """
87
+ from .configuration import get_configuration_for_instrument
88
+ from .reduce import main as reduce_main
89
+
90
+ # Parse steps
91
+ if steps:
92
+ steps = tuple(s.strip() for s in steps.split(","))
93
+ else:
94
+ steps = "all"
95
+
96
+ # Parse order range
97
+ if order_range:
98
+ parts = order_range.split(",")
99
+ order_range = (int(parts[0]), int(parts[1]))
100
+
101
+ # Load configuration
102
+ config = get_configuration_for_instrument(instrument)
103
+
104
+ # Run reduction
105
+ reduce_main(
106
+ instrument=instrument,
107
+ target=target,
108
+ night=night,
109
+ arms=arm,
110
+ steps=steps,
111
+ base_dir=base_dir or "",
112
+ input_dir=input_dir,
113
+ output_dir=output_dir,
114
+ configuration=config,
115
+ order_range=order_range,
116
+ plot=plot,
117
+ )
118
+
119
+
120
+ @cli.command()
121
+ @click.argument("files", nargs=-1, required=True)
122
+ @click.option("--output", "-o", default="combined.fits", help="Output filename")
123
+ @click.option("--plot", "-p", default=None, type=int, help="Plot specific order")
124
+ def combine(files, output, plot):
125
+ """Combine multiple reduced spectra.
126
+
127
+ FILES: Input .final.fits files to combine
128
+ """
129
+ from .tools.combine import combine as tools_combine
130
+
131
+ tools_combine(list(files), output, plot=plot)
132
+
133
+
134
+ @cli.command()
135
+ @click.argument("instrument")
136
+ def download(instrument):
137
+ """Download sample dataset for an instrument.
138
+
139
+ INSTRUMENT: Name of the instrument (e.g., UVES, HARPS)
140
+ """
141
+ from . import datasets
142
+
143
+ instrument = instrument.upper()
144
+ dataset_func = getattr(datasets, instrument, None)
145
+ if dataset_func is None:
146
+ available = [
147
+ name
148
+ for name in dir(datasets)
149
+ if name.isupper() and not name.startswith("_")
150
+ ]
151
+ raise click.ClickException(
152
+ f"Unknown instrument '{instrument}'. Available: {', '.join(available)}"
153
+ )
154
+ path = dataset_func()
155
+ click.echo(f"Dataset downloaded to: {path}")
156
+
157
+
158
+ @cli.command()
159
+ @click.argument("filename", required=False)
160
+ @click.option(
161
+ "--list", "-l", "list_examples", is_flag=True, help="List available examples"
162
+ )
163
+ @click.option("--all", "-a", "download_all", is_flag=True, help="Download all examples")
164
+ @click.option("--run", "-r", is_flag=True, help="Run the example after downloading")
165
+ @click.option("--output", "-o", default=".", help="Output directory")
166
+ def examples(filename, list_examples, download_all, run, output):
167
+ """List, download, or run example scripts from GitHub.
168
+
169
+ Downloads examples matching your installed PyReduce version.
170
+
171
+ \b
172
+ Examples:
173
+ reduce examples # List available examples
174
+ reduce examples uves_example.py # Download to current dir
175
+ reduce examples -r uves_example.py # Download and run
176
+ reduce examples --all -o ~/scripts # Download all to ~/scripts
177
+ """
178
+ import json
179
+ import os
180
+ import subprocess
181
+ import sys
182
+ import tempfile
183
+ import urllib.request
184
+ from urllib.error import HTTPError
185
+
186
+ from pyreduce import __version__
187
+
188
+ version = __version__.split("+")[0]
189
+ if version == "unknown":
190
+ raise click.ClickException(
191
+ "Cannot determine package version. Install from PyPI or a tagged release."
192
+ )
193
+
194
+ github_api = (
195
+ f"https://api.github.com/repos/ivh/PyReduce/contents/examples?ref=v{version}"
196
+ )
197
+ github_raw = f"https://raw.githubusercontent.com/ivh/PyReduce/v{version}/examples"
198
+
199
+ # Fetch list of examples from GitHub API
200
+ try:
201
+ with urllib.request.urlopen(github_api) as resp:
202
+ contents = json.loads(resp.read().decode())
203
+ except HTTPError as e:
204
+ if e.code == 404:
205
+ raise click.ClickException(
206
+ f"Tag v{version} not found on GitHub. "
207
+ "Try installing a released version."
208
+ ) from None
209
+ raise click.ClickException(f"GitHub API error: {e}") from None
210
+
211
+ example_files = sorted(f["name"] for f in contents if f["name"].endswith(".py"))
212
+
213
+ # List mode
214
+ if list_examples or (not filename and not download_all):
215
+ click.echo(f"Available examples for v{version}:")
216
+ for name in example_files:
217
+ click.echo(f" {name}")
218
+ return
219
+
220
+ if run and download_all:
221
+ raise click.ClickException("Cannot use --run with --all")
222
+
223
+ # Run mode: download to temp and execute
224
+ if run:
225
+ if filename not in example_files:
226
+ raise click.ClickException(
227
+ f"Unknown example '{filename}'. Use 'reduce examples --list' to see available."
228
+ )
229
+ url = f"{github_raw}/{filename}"
230
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
231
+ try:
232
+ with urllib.request.urlopen(url) as resp:
233
+ f.write(resp.read().decode())
234
+ temp_path = f.name
235
+ except HTTPError as e:
236
+ raise click.ClickException(
237
+ f"Failed to download {filename}: {e}"
238
+ ) from None
239
+ try:
240
+ click.echo(f"Running {filename}...")
241
+ result = subprocess.run([sys.executable, temp_path], check=False)
242
+ sys.exit(result.returncode)
243
+ finally:
244
+ os.unlink(temp_path)
245
+
246
+ # Ensure output directory exists
247
+ os.makedirs(output, exist_ok=True)
248
+
249
+ def download_file(name):
250
+ url = f"{github_raw}/{name}"
251
+ dest = os.path.join(output, name)
252
+ try:
253
+ urllib.request.urlretrieve(url, dest)
254
+ click.echo(f"Downloaded: {dest}")
255
+ except HTTPError as e:
256
+ click.echo(f"Failed to download {name}: {e}", err=True)
257
+
258
+ if download_all:
259
+ for name in example_files:
260
+ download_file(name)
261
+ else:
262
+ if filename not in example_files:
263
+ raise click.ClickException(
264
+ f"Unknown example '{filename}'. Use 'reduce examples --list' to see available."
265
+ )
266
+ download_file(filename)
267
+
268
+
269
+ @cli.command("list-steps")
270
+ def list_steps():
271
+ """List all available reduction steps."""
272
+ click.echo("Available reduction steps:")
273
+ for step in ALL_STEPS:
274
+ click.echo(f" - {step}")
275
+
276
+
277
+ def make_step_command(step_name):
278
+ """Factory to create a command for a single step."""
279
+
280
+ @click.command(name=step_name)
281
+ @click.argument("instrument")
282
+ @click.argument("target")
283
+ @click.option("--night", "-n", default=None, help="Observation night")
284
+ @click.option("--arm", "-a", default=None, help="Instrument arm")
285
+ @click.option("--base-dir", "-b", default=None, help="Base directory")
286
+ @click.option("--input-dir", "-i", default="raw", help="Input directory")
287
+ @click.option("--output-dir", "-o", default="reduced", help="Output directory")
288
+ @click.option("--plot", "-p", default=0, help="Plot level")
289
+ def cmd(instrument, target, night, arm, base_dir, input_dir, output_dir, plot):
290
+ from .configuration import get_configuration_for_instrument
291
+ from .reduce import main as reduce_main
292
+
293
+ config = get_configuration_for_instrument(instrument)
294
+ reduce_main(
295
+ instrument=instrument,
296
+ target=target,
297
+ night=night,
298
+ arms=arm,
299
+ steps=(step_name,),
300
+ base_dir=base_dir or "",
301
+ input_dir=input_dir,
302
+ output_dir=output_dir,
303
+ configuration=config,
304
+ plot=plot,
305
+ )
306
+
307
+ cmd.__doc__ = f"Run the '{step_name}' step."
308
+ return cmd
309
+
310
+
311
+ # Register individual step commands
312
+ for _step in ALL_STEPS:
313
+ cli.add_command(make_step_command(_step))
314
+
315
+
316
+ def main():
317
+ """Entry point for the CLI."""
318
+ cli()
319
+
320
+
321
+ if __name__ == "__main__":
322
+ main()