astro-metadata-translator 29.2025.4300__tar.gz → 29.2025.4800__tar.gz

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 (89) hide show
  1. {astro_metadata_translator-29.2025.4300/python/astro_metadata_translator.egg-info → astro_metadata_translator-29.2025.4800}/PKG-INFO +6 -5
  2. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/pyproject.toml +6 -5
  3. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/bin/translate.py +18 -13
  4. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/bin/writeindex.py +14 -9
  5. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/bin/writesidecar.py +14 -38
  6. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/file_helpers.py +106 -64
  7. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/indexing.py +28 -15
  8. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/tests.py +20 -12
  9. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/translator.py +3 -2
  10. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/translators/decam.py +7 -3
  11. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/translators/helpers.py +14 -0
  12. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/translators/megaprime.py +7 -3
  13. astro_metadata_translator-29.2025.4800/python/astro_metadata_translator/version.py +2 -0
  14. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800/python/astro_metadata_translator.egg-info}/PKG-INFO +6 -5
  15. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator.egg-info/requires.txt +2 -0
  16. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/tests/test_indexing.py +1 -1
  17. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/tests/test_translate_header.py +2 -2
  18. astro_metadata_translator-29.2025.4300/python/astro_metadata_translator/version.py +0 -2
  19. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/CHANGES.md +0 -0
  20. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/LICENSE +0 -0
  21. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/MANIFEST.in +0 -0
  22. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/README.md +0 -0
  23. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/__init__.py +0 -0
  24. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/bin/__init__.py +0 -0
  25. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/cli/__init__.py +0 -0
  26. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/cli/astrometadata.py +0 -0
  27. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/corrections/CFHT/README.md +0 -0
  28. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/corrections/DECam/README.md +0 -0
  29. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/corrections/HSC/HSC-HSCA00042600.yaml +0 -0
  30. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/corrections/HSC/HSC-HSCA00120200.yaml +0 -0
  31. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/corrections/HSC/HSC-HSCA00120400.yaml +0 -0
  32. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/corrections/HSC/HSC-HSCA00120600.yaml +0 -0
  33. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/corrections/HSC/HSC-HSCA00120800.yaml +0 -0
  34. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/corrections/HSC/HSC-HSCA00121000.yaml +0 -0
  35. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/corrections/HSC/HSC-HSCA00121200.yaml +0 -0
  36. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/corrections/HSC/HSC-HSCA00121400.yaml +0 -0
  37. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/corrections/HSC/HSC-HSCA00121600.yaml +0 -0
  38. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/corrections/HSC/HSC-HSCA00121800.yaml +0 -0
  39. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/corrections/HSC/HSC-HSCA00122000.yaml +0 -0
  40. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/corrections/HSC/HSC-HSCA00122800.yaml +0 -0
  41. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/corrections/HSC/HSC-HSCA00123000.yaml +0 -0
  42. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/corrections/HSC/HSC-HSCA00123200.yaml +0 -0
  43. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/corrections/HSC/HSC-HSCA00123800.yaml +0 -0
  44. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/corrections/HSC/HSC-HSCA00124000.yaml +0 -0
  45. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/corrections/HSC/HSC-HSCA00124200.yaml +0 -0
  46. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/corrections/HSC/HSC-HSCA00124400.yaml +0 -0
  47. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/corrections/HSC/HSC-HSCA00124600.yaml +0 -0
  48. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/corrections/HSC/HSC-HSCA00124800.yaml +0 -0
  49. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/corrections/HSC/HSC-HSCA00186800.yaml +0 -0
  50. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/corrections/HSC/HSC-HSCA00187000.yaml +0 -0
  51. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/corrections/HSC/HSC-HSCA00187200.yaml +0 -0
  52. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/corrections/HSC/HSC-HSCA00187400.yaml +0 -0
  53. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/corrections/HSC/HSC-HSCA00187600.yaml +0 -0
  54. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/corrections/HSC/HSC-HSCA00188000.yaml +0 -0
  55. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/corrections/HSC/HSC-HSCA00188200.yaml +0 -0
  56. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/corrections/HSC/README.md +0 -0
  57. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/corrections/SDSS/README.md +0 -0
  58. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/corrections/SuprimeCam/README.md +0 -0
  59. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/headers.py +0 -0
  60. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/observationGroup.py +0 -0
  61. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/observationInfo.py +0 -0
  62. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/properties.py +0 -0
  63. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/py.typed +0 -0
  64. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/serialize/__init__.py +0 -0
  65. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/serialize/fits.py +0 -0
  66. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/translators/__init__.py +0 -0
  67. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/translators/fits.py +0 -0
  68. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/translators/hsc.py +0 -0
  69. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/translators/sdss.py +0 -0
  70. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/translators/subaru.py +0 -0
  71. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator/translators/suprimecam.py +0 -0
  72. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator.egg-info/SOURCES.txt +0 -0
  73. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator.egg-info/dependency_links.txt +0 -0
  74. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator.egg-info/entry_points.txt +0 -0
  75. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator.egg-info/top_level.txt +0 -0
  76. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/python/astro_metadata_translator.egg-info/zip-safe +0 -0
  77. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/setup.cfg +0 -0
  78. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/tests/test_basics.py +0 -0
  79. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/tests/test_cfht.py +0 -0
  80. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/tests/test_cli.py +0 -0
  81. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/tests/test_decam.py +0 -0
  82. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/tests/test_extensions.py +0 -0
  83. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/tests/test_groups.py +0 -0
  84. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/tests/test_headers.py +0 -0
  85. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/tests/test_sdss.py +0 -0
  86. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/tests/test_shadowing.py +0 -0
  87. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/tests/test_subaru.py +0 -0
  88. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/tests/test_translation.py +0 -0
  89. {astro_metadata_translator-29.2025.4300 → astro_metadata_translator-29.2025.4800}/tests/test_translator_helpers.py +0 -0
@@ -1,25 +1,26 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: astro-metadata-translator
3
- Version: 29.2025.4300
3
+ Version: 29.2025.4800
4
4
  Summary: A translator for astronomical metadata.
5
5
  Author-email: Rubin Observatory Data Management <dm-admin@lists.lsst.org>
6
- License: BSD 3-Clause License
6
+ License-Expression: BSD-3-Clause
7
7
  Project-URL: Homepage, https://github.com/lsst/astro_metadata_translator
8
8
  Keywords: lsst
9
9
  Classifier: Intended Audience :: Science/Research
10
10
  Classifier: Topic :: Scientific/Engineering :: Astronomy
11
- Classifier: License :: OSI Approved :: BSD License
12
11
  Classifier: Operating System :: OS Independent
13
12
  Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.14
14
14
  Classifier: Programming Language :: Python :: 3.13
15
15
  Classifier: Programming Language :: Python :: 3.12
16
16
  Classifier: Programming Language :: Python :: 3.11
17
- Classifier: Programming Language :: Python :: 3.10
18
- Requires-Python: >=3.10.0
17
+ Requires-Python: >=3.11.0
19
18
  Description-Content-Type: text/markdown
20
19
  License-File: LICENSE
21
20
  Requires-Dist: astropy>=3.0.5
22
21
  Requires-Dist: pyyaml>=3.13
22
+ Requires-Dist: lsst-resources
23
+ Requires-Dist: fsspec
23
24
  Requires-Dist: click>=8
24
25
  Provides-Extra: test
25
26
  Requires-Dist: pytest; extra == "test"
@@ -4,9 +4,10 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "astro-metadata-translator"
7
- requires-python = ">=3.10.0"
7
+ requires-python = ">=3.11.0"
8
8
  description = "A translator for astronomical metadata."
9
- license = {text = "BSD 3-Clause License"}
9
+ license = "BSD-3-Clause"
10
+ license-files = ["LICENSE"]
10
11
  authors = [
11
12
  {name="Rubin Observatory Data Management", email="dm-admin@lists.lsst.org"},
12
13
  ]
@@ -14,19 +15,20 @@ readme = "README.md"
14
15
  classifiers = [
15
16
  "Intended Audience :: Science/Research",
16
17
  "Topic :: Scientific/Engineering :: Astronomy",
17
- "License :: OSI Approved :: BSD License",
18
18
  "Operating System :: OS Independent",
19
19
  "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.14",
20
21
  "Programming Language :: Python :: 3.13",
21
22
  "Programming Language :: Python :: 3.12",
22
23
  "Programming Language :: Python :: 3.11",
23
- "Programming Language :: Python :: 3.10",
24
24
  ]
25
25
  keywords = ["lsst"]
26
26
  dynamic = ["version"]
27
27
  dependencies = [
28
28
  "astropy >=3.0.5",
29
29
  "pyyaml >=3.13",
30
+ "lsst-resources",
31
+ "fsspec", # Do not include S3/http variants by default.
30
32
  "click >= 8"
31
33
  ]
32
34
 
@@ -43,7 +45,6 @@ astrometadata = "astro_metadata_translator.cli.astrometadata:main"
43
45
 
44
46
  [tool.setuptools]
45
47
  zip-safe = true
46
- license-files = ["LICENSE"]
47
48
 
48
49
  [tool.setuptools.packages.find]
49
50
  where = ["python"]
@@ -23,18 +23,22 @@ import math
23
23
  import traceback
24
24
  from collections import defaultdict
25
25
  from collections.abc import Sequence
26
- from typing import IO, Any
26
+ from typing import IO, TYPE_CHECKING, Any
27
27
 
28
28
  import astropy.time
29
29
  import astropy.units as u
30
30
  import yaml
31
31
  from astropy.table import Column, MaskedColumn, QTable
32
+ from lsst.resources import ResourcePath
32
33
 
33
34
  from astro_metadata_translator import MetadataTranslator, ObservationInfo, fix_header
34
35
 
35
36
  from ..file_helpers import find_files, read_basic_metadata_from_file
36
37
  from ..properties import PROPERTIES
37
38
 
39
+ if TYPE_CHECKING:
40
+ from lsst.resources import ResourcePathExpression
41
+
38
42
  log = logging.getLogger(__name__)
39
43
 
40
44
  # Output mode choices
@@ -74,7 +78,7 @@ TABLE_COLUMNS = (
74
78
 
75
79
 
76
80
  def read_file(
77
- file: str,
81
+ file: ResourcePathExpression,
78
82
  hdrnum: int,
79
83
  print_trace: bool,
80
84
  output_columns: defaultdict[str, list],
@@ -86,7 +90,7 @@ def read_file(
86
90
 
87
91
  Parameters
88
92
  ----------
89
- file : `str`
93
+ file : `str` or `lsst.resources.ResourcePathExpression`
90
94
  The file from which the header is to be read.
91
95
  hdrnum : `int`
92
96
  The HDU number to read. The primary header is always read and
@@ -127,10 +131,11 @@ def read_file(
127
131
  if output_mode != "table":
128
132
  log.info("Analyzing %s...", file)
129
133
 
134
+ uri = ResourcePath(file, forceDirectory=False)
130
135
  try:
131
- md = read_basic_metadata_from_file(file, hdrnum, can_raise=True)
136
+ md = read_basic_metadata_from_file(uri, hdrnum, can_raise=True)
132
137
  if md is None:
133
- raise RuntimeError(f"Failed to read file {file} HDU={hdrnum}")
138
+ raise RuntimeError(f"Failed to read file {uri} HDU={hdrnum}")
134
139
 
135
140
  if output_mode.endswith("native"):
136
141
  # Strip native and don't change type of md
@@ -141,21 +146,21 @@ def read_file(
141
146
 
142
147
  if output_mode in ("yaml", "fixed"):
143
148
  if output_mode == "fixed":
144
- fix_header(md, filename=file)
149
+ fix_header(md, filename=str(uri))
145
150
 
146
151
  # The header should be written out in the insertion order
147
152
  print(yaml.dump(md, sort_keys=False), file=outstream)
148
153
  return True
149
154
 
150
155
  # Try to work out a translator class.
151
- translator_class = MetadataTranslator.determine_translator(md, filename=file)
156
+ translator_class = MetadataTranslator.determine_translator(md, filename=str(uri))
152
157
 
153
158
  # Work out which headers to translate, assuming the default if
154
159
  # we have a YAML test file.
155
- if file.endswith(".yaml"):
160
+ if uri.getExtension() == ".yaml":
156
161
  headers = [md]
157
162
  else:
158
- headers = list(translator_class.determine_translatable_headers(file, md))
163
+ headers = list(translator_class.determine_translatable_headers(uri, md))
159
164
  if output_mode == "auto":
160
165
  output_mode = "table" if len(headers) > 1 else "verbose"
161
166
 
@@ -163,7 +168,7 @@ def read_file(
163
168
  raise ValueError("Table mode requires output columns")
164
169
 
165
170
  for md in headers:
166
- obs_info = ObservationInfo(md, pedantic=False, filename=file)
171
+ obs_info = ObservationInfo(md, pedantic=False, filename=str(uri))
167
172
  if output_mode == "table":
168
173
  for c in TABLE_COLUMNS:
169
174
  output_columns[c["label"]].append(getattr(obs_info, c["attr"]))
@@ -178,7 +183,7 @@ def read_file(
178
183
  if print_trace:
179
184
  traceback.print_exc(file=outstream)
180
185
  else:
181
- print(f"Failure processing {file}: {e}", file=outstream)
186
+ print(f"Failure processing {uri}: {e}", file=outstream)
182
187
  return False
183
188
  return True
184
189
 
@@ -308,9 +313,9 @@ def translate_or_dump_headers(
308
313
  isok = read_file(path, hdrnum, print_trace, output_columns, outstream, output_mode, heading)
309
314
  heading = False
310
315
  if isok:
311
- okay.append(path)
316
+ okay.append(str(path))
312
317
  else:
313
- failed.append(path)
318
+ failed.append(str(path))
314
319
 
315
320
  # Check if we have exceeded the page size and should dump the table.
316
321
  if output_mode == "table" and len(output_columns[TABLE_COLUMNS[0]["label"]]) >= MAX_TABLE_PAGE_SIZE:
@@ -19,6 +19,8 @@ import os
19
19
  from collections.abc import MutableMapping, Sequence
20
20
  from typing import IO
21
21
 
22
+ from lsst.resources import ResourcePath
23
+
22
24
  from ..file_helpers import find_files
23
25
  from ..indexing import index_files
24
26
 
@@ -87,15 +89,19 @@ def write_index_files(
87
89
 
88
90
  failed = []
89
91
  okay = []
90
- files_per_directory: MutableMapping[str, list[str]] = {}
92
+ files_per_directory: MutableMapping[ResourcePath, list[ResourcePath]] = {}
91
93
 
92
94
  # Group each file by directory if no explicit output path
93
95
  if outpath is None:
94
96
  for path in found_files:
95
- head, tail = os.path.split(path)
96
- files_per_directory.setdefault(head, []).append(tail)
97
+ head, tail = path.split()
98
+ files_per_directory.setdefault(head, []).append(ResourcePath(tail, forceAbsolute=False))
97
99
  else:
98
- files_per_directory["."] = list(found_files)
100
+ # We want the requested files to be paths relative to the current
101
+ # directory. For now this assumes that all the input files are
102
+ # local -- we are not trying to discover a shared root directory.
103
+ cwd = ResourcePath(".", forceAbsolute=True, forceDirectory=True)
104
+ files_per_directory[cwd] = list(found_files)
99
105
 
100
106
  # Extract translated metadata for each file in each directory
101
107
  for directory, files_in_dir in files_per_directory.items():
@@ -113,11 +119,10 @@ def write_index_files(
113
119
 
114
120
  # Write the index file
115
121
  if outpath is None:
116
- index_file = os.path.join(directory, "_index.json")
122
+ index_file = directory.join("_index.json")
117
123
  else:
118
- index_file = outpath
119
- with open(index_file, "w") as fd:
120
- print(json.dumps(output), file=fd)
121
- log.info("Wrote index file to %s", index_file)
124
+ index_file = ResourcePath(outpath, forceAbsolute=False)
125
+ index_file.write(json.dumps(output).encode())
126
+ log.info("Wrote index file to %s", index_file)
122
127
 
123
128
  return okay, failed
@@ -14,45 +14,22 @@ from __future__ import annotations
14
14
  __all__ = ("write_sidecar_file", "write_sidecar_files")
15
15
 
16
16
  import logging
17
- import os
18
17
  import traceback
19
18
  from collections.abc import Sequence
20
- from typing import IO
19
+ from typing import IO, TYPE_CHECKING
21
20
 
22
- from ..file_helpers import find_files, read_file_info
23
-
24
- log = logging.getLogger(__name__)
25
-
26
-
27
- def _split_ext(file: str) -> tuple[str, str]:
28
- """Split the extension from the file name and return it and the root.
29
-
30
- Parameters
31
- ----------
32
- file : `str`
33
- The file name to examine.
21
+ from lsst.resources import ResourcePath
34
22
 
35
- Returns
36
- -------
37
- root : `str`
38
- The root of the file name.
39
- ext : `str`
40
- The file extension. There is special case handling of .gz and other
41
- compression extensions.
42
- """
43
- special = {".gz", ".bz2", ".xz", ".fz"}
44
-
45
- root, ext = os.path.splitext(file)
23
+ from ..file_helpers import find_files, read_file_info
46
24
 
47
- if ext in special:
48
- root, second_ext = os.path.splitext(root)
49
- ext = second_ext + ext
25
+ if TYPE_CHECKING:
26
+ from lsst.resources import ResourcePathExpression
50
27
 
51
- return root, ext
28
+ log = logging.getLogger(__name__)
52
29
 
53
30
 
54
31
  def write_sidecar_file(
55
- file: str,
32
+ file: ResourcePathExpression,
56
33
  hdrnum: int,
57
34
  content_mode: str,
58
35
  print_trace: bool,
@@ -62,7 +39,7 @@ def write_sidecar_file(
62
39
 
63
40
  Parameters
64
41
  ----------
65
- file : `str`
42
+ file : `str` or `lsst.resources.ResourcePathExpression`
66
43
  The file from which the header is to be read.
67
44
  hdrnum : `int`
68
45
  The HDU number to read. The primary header is always read and
@@ -103,12 +80,11 @@ def write_sidecar_file(
103
80
  return False
104
81
 
105
82
  # Calculate sidecar file name derived from this file.
106
- # Match the ButlerURI behavior in that .fits.gz should be replaced
83
+ # .fits.gz should be replaced
107
84
  # with .json, and not resulting in .fits.json.
108
- root, ext = _split_ext(file)
109
- newfile = root + ".json"
110
- with open(newfile, "w") as fd:
111
- print(json_str, file=fd)
85
+ uri = ResourcePath(file)
86
+ newfile = uri.updatedExtension(".json")
87
+ newfile.write(str(json_str).encode())
112
88
  log.debug("Writing sidecar file %s", newfile)
113
89
 
114
90
  except Exception as e:
@@ -169,8 +145,8 @@ def write_sidecar_files(
169
145
  for path in sorted(found_files):
170
146
  isok = write_sidecar_file(path, hdrnum, content_mode, print_trace, outstream)
171
147
  if isok:
172
- okay.append(path)
148
+ okay.append(str(path))
173
149
  else:
174
- failed.append(path)
150
+ failed.append(str(path))
175
151
 
176
152
  return okay, failed
@@ -20,16 +20,21 @@ __all__ = ("find_files", "read_basic_metadata_from_file", "read_file_info")
20
20
 
21
21
  import json
22
22
  import logging
23
- import os
24
23
  import re
25
24
  import traceback
26
25
  from collections.abc import Iterable, MutableMapping
27
- from typing import IO, Any
26
+ from typing import IO, TYPE_CHECKING, Any
27
+
28
+ from astropy.io import fits
29
+ from lsst.resources import ResourcePath
28
30
 
29
31
  from .headers import merge_headers
30
32
  from .observationInfo import ObservationInfo
31
33
  from .tests import read_test_file
32
34
 
35
+ if TYPE_CHECKING:
36
+ from lsst.resources import ResourcePathExpression
37
+
33
38
  log = logging.getLogger(__name__)
34
39
 
35
40
  # Prefer afw over Astropy
@@ -37,30 +42,15 @@ try:
37
42
  import lsst.daf.base # noqa: F401 need PropertyBase for readMetadata
38
43
  from lsst.afw.fits import FitsError, readMetadata
39
44
 
40
- def _read_fits_metadata(file: str, hdu: int, can_raise: bool = False) -> MutableMapping[str, Any] | None:
41
- """Read a FITS header using afw.
42
-
43
- Parameters
44
- ----------
45
- file : `str`
46
- The file to read.
47
- hdu : `int`
48
- The header number to read.
49
- can_raise : `bool`, optional
50
- Indicate whether the function can raise and exception (default)
51
- or should return `None` on error. Can still raise if an unexpected
52
- error is encountered.
53
-
54
- Returns
55
- -------
56
- md : `dict`
57
- The requested header. `None` if it could not be read and
58
- ``can_raise`` is `False`.
59
-
60
- Notes
61
- -----
62
- Tries to catch a FitsError 104 and convert to `FileNotFoundError`.
63
- """
45
+ have_afw = True
46
+
47
+ def _read_fits_metadata_afw(
48
+ file: str, hdu: int, can_raise: bool = False
49
+ ) -> MutableMapping[str, Any] | None:
50
+ # Only works with local files.
51
+ # Tries to catch a FitsError 104 and convert to `FileNotFoundError`.
52
+ # For detailed docstrings see the _read_fits_metadata implementation
53
+ # below.
64
54
  try:
65
55
  return readMetadata(file, hdu=hdu)
66
56
  except FitsError as e:
@@ -72,31 +62,84 @@ try:
72
62
  return None
73
63
 
74
64
  except ImportError:
75
- from astropy.io import fits
65
+ have_afw = False
66
+
67
+
68
+ def _read_fits_metadata_astropy(
69
+ file: ResourcePathExpression, hdu: int, can_raise: bool = False
70
+ ) -> MutableMapping[str, Any] | None:
71
+ """Read a FITS header using astropy.
72
+
73
+ Parameters
74
+ ----------
75
+ file : `str` or `lsst.resources.ResourcePathExpression`
76
+ The file to read.
77
+ hdu : `int`
78
+ The header number to read.
79
+ can_raise : `bool`, optional
80
+ Indicate whether the function can raise and exception (default)
81
+ or should return `None` on error. Can still raise if an unexpected
82
+ error is encountered.
83
+
84
+ Returns
85
+ -------
86
+ md : `dict`
87
+ The requested header. `None` if it could not be read and
88
+ ``can_raise`` is `False`.
89
+ """
90
+ header = None
91
+ uri = ResourcePath(file, forceDirectory=False)
92
+ try:
93
+ fs, fspath = uri.to_fsspec()
94
+ with fs.open(fspath) as f, fits.open(f) as fits_file:
95
+ try:
96
+ # Copy forces a download of the remote resource.
97
+ header = fits_file[hdu].header.copy()
98
+ except IndexError as e:
99
+ if can_raise:
100
+ raise e
101
+ except Exception as e:
102
+ if can_raise:
103
+ raise e
104
+ return header
76
105
 
77
- def _read_fits_metadata(file: str, hdu: int, can_raise: bool = False) -> MutableMapping[str, Any] | None:
78
- """Read a FITS header using astropy."""
79
- # For detailed docstrings see the afw implementation above
80
- header = None
81
- try:
82
- with fits.open(file) as fits_file:
83
- try:
84
- header = fits_file[hdu].header
85
- except IndexError as e:
86
- if can_raise:
87
- raise e
88
- except Exception as e:
89
- if can_raise:
90
- raise e
91
- return header
92
106
 
107
+ def _read_fits_metadata(
108
+ file: ResourcePathExpression, hdu: int, can_raise: bool = False
109
+ ) -> MutableMapping[str, Any] | None:
110
+ """Read a FITS header using afw or astropy.
111
+
112
+ Prefer afw for local reads if available.
93
113
 
94
- def find_files(files: Iterable[str], regex: str) -> list[str]:
114
+ Parameters
115
+ ----------
116
+ file : `str` or `lsst.resources.ResourcePathExpression`
117
+ The file to read.
118
+ hdu : `int`
119
+ The header number to read.
120
+ can_raise : `bool`, optional
121
+ Indicate whether the function can raise and exception (default)
122
+ or should return `None` on error. Can still raise if an unexpected
123
+ error is encountered.
124
+
125
+ Returns
126
+ -------
127
+ md : `dict`
128
+ The requested header. `None` if it could not be read and
129
+ ``can_raise`` is `False`.
130
+ """
131
+ uri = ResourcePath(file, forceAbsolute=False)
132
+ if have_afw and uri.isLocal:
133
+ return _read_fits_metadata_afw(uri.ospath, hdu, can_raise=can_raise)
134
+ return _read_fits_metadata_astropy(uri, hdu, can_raise=can_raise)
135
+
136
+
137
+ def find_files(files: Iterable[ResourcePathExpression], regex: str) -> list[ResourcePath]:
95
138
  """Find files for processing.
96
139
 
97
140
  Parameters
98
141
  ----------
99
- files : iterable of `str`
142
+ files : iterable of `lsst.resources.ResourcePathExpression`
100
143
  The files or directories from which the headers are to be read.
101
144
  regex : `str`
102
145
  Regular expression string used to filter files when a directory is
@@ -104,34 +147,31 @@ def find_files(files: Iterable[str], regex: str) -> list[str]:
104
147
 
105
148
  Returns
106
149
  -------
107
- found_files : `list` of `str`
150
+ found_files : `list` of `lsst.resources.ResourcePath`
108
151
  The files that were found.
109
152
  """
110
153
  file_regex = re.compile(regex)
111
- found_files = []
154
+ found_files: list[ResourcePath] = []
112
155
 
113
156
  # Find all the files of interest
114
- for file in files:
115
- if os.path.isdir(file):
116
- for root, _, files in os.walk(file):
117
- for name in files:
118
- path = os.path.join(root, name)
119
- if os.path.isfile(path) and file_regex.search(name):
120
- found_files.append(path)
157
+ for candidate in files:
158
+ uri = ResourcePath(candidate, forceAbsolute=False)
159
+ if uri.isdir():
160
+ found_files.extend(ResourcePath.findFileResources([uri], file_filter=file_regex, grouped=False))
121
161
  else:
122
- found_files.append(file)
162
+ found_files.append(uri)
123
163
 
124
164
  return found_files
125
165
 
126
166
 
127
167
  def read_basic_metadata_from_file(
128
- file: str, hdrnum: int, can_raise: bool = True
168
+ file: ResourcePathExpression, hdrnum: int, can_raise: bool = True
129
169
  ) -> MutableMapping[str, Any] | None:
130
170
  """Read a raw header from a file, merging if necessary.
131
171
 
132
172
  Parameters
133
173
  ----------
134
- file : `str`
174
+ file : `str` or `lsst.resources.ResourcePathExpression`
135
175
  Name of file to read. Can be FITS, YAML or JSON. YAML or JSON must be
136
176
  a simple top-level dict.
137
177
  hdrnum : `int`
@@ -151,10 +191,11 @@ def read_basic_metadata_from_file(
151
191
  The header as a dict. Can be `None` if there was a problem reading
152
192
  the file.
153
193
  """
154
- if file.endswith(".yaml") or file.endswith(".json"):
194
+ uri = ResourcePath(file, forceAbsolute=False)
195
+ if uri.getExtension() in (".yaml", ".json"):
155
196
  try:
156
197
  md = read_test_file(
157
- file,
198
+ uri,
158
199
  )
159
200
  except Exception as e:
160
201
  if not can_raise:
@@ -165,7 +206,7 @@ def read_basic_metadata_from_file(
165
206
  # YAML can't have HDUs so skip merging below
166
207
  hdrnum = 0
167
208
  else:
168
- md = _read_fits_metadata(file, 0, can_raise=can_raise)
209
+ md = _read_fits_metadata(uri, 0, can_raise=can_raise)
169
210
  if md is None:
170
211
  log.warning("Unable to open file %s", file)
171
212
  return None
@@ -174,19 +215,19 @@ def read_basic_metadata_from_file(
174
215
  hdrnum = 1
175
216
  if hdrnum > 0:
176
217
  # Allow this to fail
177
- mdn = _read_fits_metadata(file, int(hdrnum), can_raise=False)
218
+ mdn = _read_fits_metadata(uri, int(hdrnum), can_raise=False)
178
219
  # Astropy does not allow append mode since it does not
179
220
  # convert lists to multiple cards. Overwrite for now
180
221
  if mdn is not None:
181
222
  md = merge_headers([md, mdn], mode="overwrite")
182
223
  else:
183
- log.warning("HDU %d was not found in file %s. Ignoring request.", hdrnum, file)
224
+ log.warning("HDU %d was not found in file %s. Ignoring request.", hdrnum, uri)
184
225
 
185
226
  return md
186
227
 
187
228
 
188
229
  def read_file_info(
189
- file: str,
230
+ file: ResourcePathExpression,
190
231
  hdrnum: int,
191
232
  print_trace: bool | None = None,
192
233
  content_mode: str = "translated",
@@ -232,9 +273,10 @@ def read_file_info(
232
273
  if content_type not in ("native", "simple", "json"):
233
274
  raise ValueError(f"Unrecognized content type request {content_type}")
234
275
 
276
+ uri = ResourcePath(file, forceAbsolute=False)
235
277
  try:
236
278
  # Calculate the JSON from the file
237
- md = read_basic_metadata_from_file(file, hdrnum, can_raise=True if print_trace is None else False)
279
+ md = read_basic_metadata_from_file(uri, hdrnum, can_raise=True if print_trace is None else False)
238
280
  if md is None:
239
281
  return None
240
282
  if content_mode == "metadata":
@@ -249,7 +291,7 @@ def read_file_info(
249
291
  json_str = json.dumps(dict(md))
250
292
  return json_str
251
293
  return md
252
- obs_info = ObservationInfo(md, pedantic=True, filename=file)
294
+ obs_info = ObservationInfo(md, pedantic=True, filename=str(uri))
253
295
  if content_type == "native":
254
296
  return obs_info
255
297
  simple = obs_info.to_simple()