caom2utils 1.7.2__tar.gz → 1.7.4__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.
- {caom2utils-1.7.2/caom2utils.egg-info → caom2utils-1.7.4}/PKG-INFO +11 -5
- {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/blueprints.py +6 -4
- {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/caom2blueprint.py +19 -12
- {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/data_util.py +24 -18
- {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/legacy.py +13 -5
- {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/parsers.py +106 -48
- {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/data/help.txt +3 -1
- caom2utils-1.7.4/caom2utils/tests/test_cli_parsers.py +146 -0
- {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/test_collections.py +4 -1
- {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/test_data_util.py +26 -6
- {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/test_fits2caom2.py +37 -46
- caom2utils-1.7.4/caom2utils/version.py +1 -0
- {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/wcs_parsers.py +3 -2
- {caom2utils-1.7.2 → caom2utils-1.7.4/caom2utils.egg-info}/PKG-INFO +11 -5
- {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils.egg-info/SOURCES.txt +2 -4
- {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils.egg-info/requires.txt +2 -2
- {caom2utils-1.7.2 → caom2utils-1.7.4}/setup.cfg +6 -6
- {caom2utils-1.7.2 → caom2utils-1.7.4}/setup.py +6 -1
- caom2utils-1.7.2/caom2utils/tests/data/missing_observation_help.txt +0 -8
- caom2utils-1.7.2/caom2utils/tests/data/missing_positional_argument_help.txt +0 -10
- caom2utils-1.7.2/caom2utils/tests/data/too_few_arguments_help.txt +0 -0
- caom2utils-1.7.2/caom2utils/version.py +0 -1
- {caom2utils-1.7.2 → caom2utils-1.7.4}/LICENSE +0 -0
- {caom2utils-1.7.2 → caom2utils-1.7.4}/MANIFEST.in +0 -0
- {caom2utils-1.7.2 → caom2utils-1.7.4}/README.rst +0 -0
- {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/__init__.py +0 -0
- {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/caomvalidator.py +0 -0
- {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/polygonvalidator.py +0 -0
- {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/__init__.py +0 -0
- {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/conftest.py +0 -0
- {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/data/4axes.fits +0 -0
- {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/data/4axes.override +0 -0
- {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/data/4axes_obs.fits +0 -0
- {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/data/SampleComposite-CAOM-2.3.xml +0 -0
- {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/data/bad_product_id.txt +0 -0
- {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/data/bad_product_id.xml +0 -0
- {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/data/edge_case.blueprint +0 -0
- {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/data/fits2caom2.config +0 -0
- {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/data/java.config +0 -0
- {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/data/missing_product_id.txt +0 -0
- {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/data/nonconformant.py +0 -0
- {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/data/test.override +0 -0
- {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/data/test_plugin.py +0 -0
- {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/data/test_plugin_class.py +0 -0
- {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/data/text.override +0 -0
- {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/data/time_axes.fits +0 -0
- {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/test_caomvalidator.py +0 -0
- {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/test_convert_from_java.py +0 -0
- {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/test_custom_axis_util.py +0 -0
- {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/test_obs_blueprint.py +0 -0
- {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/test_polygonvalidator.py +0 -0
- {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/test_si_uris.py +0 -0
- {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/test_wcsvalidator.py +0 -0
- {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/wcs_util.py +0 -0
- {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/wcsvalidator.py +0 -0
- {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils.egg-info/dependency_links.txt +0 -0
- {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils.egg-info/entry_points.txt +0 -0
- {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils.egg-info/not-zip-safe +0 -0
- {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: caom2utils
|
|
3
|
-
Version: 1.7.
|
|
3
|
+
Version: 1.7.4
|
|
4
4
|
Summary: CAOM-2.4 utils
|
|
5
5
|
Home-page: https://www.cadc-ccda.hia-iha.nrc-cnrc.gc.ca/caom2
|
|
6
6
|
Author: Canadian Astronomy Data Centre
|
|
@@ -10,10 +10,15 @@ Classifier: Natural Language :: English
|
|
|
10
10
|
Classifier: License :: OSI Approved :: GNU Affero General Public License v3
|
|
11
11
|
Classifier: Programming Language :: Python
|
|
12
12
|
Classifier: Programming Language :: Python :: 3
|
|
13
|
-
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Requires-Python: >=3.10, <3.15
|
|
14
19
|
License-File: LICENSE
|
|
15
|
-
Requires-Dist: cadcdata>=2.
|
|
16
|
-
Requires-Dist: caom2
|
|
20
|
+
Requires-Dist: cadcdata>=2.5.2
|
|
21
|
+
Requires-Dist: caom2<3,>=2.6
|
|
17
22
|
Requires-Dist: astropy>=2.0
|
|
18
23
|
Requires-Dist: spherical-geometry>=1.2.11
|
|
19
24
|
Requires-Dist: vos>=3.1.1
|
|
@@ -30,6 +35,7 @@ Dynamic: classifier
|
|
|
30
35
|
Dynamic: description
|
|
31
36
|
Dynamic: home-page
|
|
32
37
|
Dynamic: license
|
|
38
|
+
Dynamic: license-file
|
|
33
39
|
Dynamic: requires-python
|
|
34
40
|
Dynamic: summary
|
|
35
41
|
|
|
@@ -982,9 +982,7 @@ class ObsBlueprint:
|
|
|
982
982
|
self._extensions[extension][caom2_element][1].insert(0, ttype_attribute)
|
|
983
983
|
else:
|
|
984
984
|
raise AttributeError(
|
|
985
|
-
|
|
986
|
-
extension, caom2_element
|
|
987
|
-
)
|
|
985
|
+
f'No TTYPE attributes in extension {extension} associated with keyword {caom2_element}'
|
|
988
986
|
)
|
|
989
987
|
else:
|
|
990
988
|
self._extensions[extension][caom2_element] = ('BINTABLE', [ttype_attribute], index)
|
|
@@ -1187,10 +1185,14 @@ class Hdf5ObsBlueprint(ObsBlueprint):
|
|
|
1187
1185
|
# lookup value starting with // means rooted at base of the hdf5 file
|
|
1188
1186
|
ob.add_attribute('Observation.target.name', '//header/object/obj_id')
|
|
1189
1187
|
|
|
1190
|
-
# lookup value starting with / means rooted at the base of the
|
|
1188
|
+
# lookup value starting with / means rooted at the base of one of the extension_names parameter for Hdf5Parser
|
|
1191
1189
|
# (integer) means return only the value with the index of "integer" from a list
|
|
1192
1190
|
ob.add_attribute('Chunk.position.axis.function.refCoord.coord1.pix', '/header/wcs/crpix(0)')
|
|
1193
1191
|
|
|
1192
|
+
# lookup values starting with / and with "{}" in the path will cause the blueprint application to attempt to
|
|
1193
|
+
# guess the extension names from the file content
|
|
1194
|
+
ob.add_attribute('Chunk.position.axis.function.refCoord.coord1.pix', '/sitedata/site{}/header/wcs/crpix(0)')
|
|
1195
|
+
|
|
1194
1196
|
# (integer:integer) means return only the value with the index of "integer" from a list, followed by "integer"
|
|
1195
1197
|
# from the list in the list
|
|
1196
1198
|
ob.add_attribute('Chunk.position.axis.function.cd11', '/header/wcs/cd(0:0)')
|
|
@@ -467,7 +467,10 @@ def _load_module(module):
|
|
|
467
467
|
raise e
|
|
468
468
|
|
|
469
469
|
|
|
470
|
-
def
|
|
470
|
+
def build_caom2gen_parser():
|
|
471
|
+
"""
|
|
472
|
+
Build the ArgumentParser for caom2gen (without parsing argv).
|
|
473
|
+
"""
|
|
471
474
|
parser = get_gen_proc_arg_parser()
|
|
472
475
|
parser.add_argument(
|
|
473
476
|
'--blueprint',
|
|
@@ -479,6 +482,11 @@ def caom2gen():
|
|
|
479
482
|
'per lineage entry.'
|
|
480
483
|
),
|
|
481
484
|
)
|
|
485
|
+
return parser
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def caom2gen():
|
|
489
|
+
parser = build_caom2gen_parser()
|
|
482
490
|
|
|
483
491
|
if len(sys.argv) < 2:
|
|
484
492
|
parser.print_usage(file=sys.stderr)
|
|
@@ -528,10 +536,11 @@ def caom2gen():
|
|
|
528
536
|
try:
|
|
529
537
|
gen_proc(args, blueprints)
|
|
530
538
|
except Exception as e:
|
|
539
|
+
from cadcutils.util.cli_errors import format_user_error
|
|
531
540
|
logging.error('Failed caom2gen execution.')
|
|
532
|
-
logging.error(e)
|
|
533
|
-
|
|
534
|
-
|
|
541
|
+
logging.error(format_user_error(e))
|
|
542
|
+
if logging.getLogger().isEnabledFor(logging.DEBUG):
|
|
543
|
+
logging.debug(traceback.format_exc())
|
|
535
544
|
sys.exit(-1)
|
|
536
545
|
|
|
537
546
|
logging.debug(f'Done {APP_NAME} processing.')
|
|
@@ -711,7 +720,9 @@ def proc(args, obs_blueprints):
|
|
|
711
720
|
raise RuntimeError(msg)
|
|
712
721
|
|
|
713
722
|
subject = net.Subject.from_cmd_line_args(args)
|
|
714
|
-
client = data_util.StorageClientWrapper(
|
|
723
|
+
client = data_util.StorageClientWrapper(
|
|
724
|
+
subject, resource_id=args.resource_id, host=args.host,
|
|
725
|
+
insecure=args.insecure)
|
|
715
726
|
validate_wcs = True
|
|
716
727
|
if args.no_validate:
|
|
717
728
|
validate_wcs = False
|
|
@@ -831,13 +842,9 @@ def gen_proc(args, blueprints, **kwargs):
|
|
|
831
842
|
connected = False
|
|
832
843
|
else:
|
|
833
844
|
subject = net.Subject.from_cmd_line_args(args)
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
else:
|
|
838
|
-
# if the resource_id is defined, assume that the caller intends to use the Storage Inventory system, as
|
|
839
|
-
# it's the CADC storage client that depends on a resource_id
|
|
840
|
-
client = data_util.StorageClientWrapper(subject, resource_id=args.resource_id)
|
|
845
|
+
client = data_util.StorageClientWrapper(
|
|
846
|
+
subject, resource_id=args.resource_id, host=args.host,
|
|
847
|
+
insecure=args.insecure)
|
|
841
848
|
|
|
842
849
|
for ii, cardinality in enumerate(args.lineage):
|
|
843
850
|
product_id, uri = _extract_ids(cardinality)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# ****************** CANADIAN ASTRONOMY DATA CENTRE *******************
|
|
3
3
|
# ************* CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES **************
|
|
4
4
|
#
|
|
5
|
-
# (c)
|
|
5
|
+
# (c) 2025. (c) 2025.
|
|
6
6
|
# Government of Canada Gouvernement du Canada
|
|
7
7
|
# National Research Council Conseil national de recherches
|
|
8
8
|
# Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6
|
|
@@ -95,7 +95,8 @@ class StorageClientWrapper:
|
|
|
95
95
|
Wrap the metrics collection with StorageInventoryClient.
|
|
96
96
|
"""
|
|
97
97
|
|
|
98
|
-
def __init__(self, subject, resource_id='ivo://cadc.nrc.ca/uvic/minoc', metrics=None
|
|
98
|
+
def __init__(self, subject, resource_id='ivo://cadc.nrc.ca/uvic/minoc', metrics=None,
|
|
99
|
+
host=None, insecure=False):
|
|
99
100
|
"""
|
|
100
101
|
:param subject: net.Subject instance for authentication and authorization
|
|
101
102
|
:param resource_id: str identifies the StorageInventoryClient endpoint. Defaults to the installation closest to
|
|
@@ -103,8 +104,11 @@ class StorageClientWrapper:
|
|
|
103
104
|
:param metrics: caom2pipe.manaage_composable.Metrics instance. If set, will track execution times, by action,
|
|
104
105
|
from the beginning of the method invocation to the end of the method invocation, success or failure.
|
|
105
106
|
Defaults to None, because fits2caom2 is a stand-alone application.
|
|
107
|
+
:param host: Host server for the storage inventory service
|
|
108
|
+
:param insecure: skip SSL server certificate verification (for testing only)
|
|
106
109
|
"""
|
|
107
|
-
self._cadc_client = StorageInventoryClient(
|
|
110
|
+
self._cadc_client = StorageInventoryClient(
|
|
111
|
+
subject=subject, resource_id=resource_id, host=host, insecure=insecure)
|
|
108
112
|
self._metrics = metrics
|
|
109
113
|
self._logger = logging.getLogger(self.__class__.__name__)
|
|
110
114
|
|
|
@@ -188,24 +192,19 @@ class StorageClientWrapper:
|
|
|
188
192
|
self._logger.debug(f'Begin put for {uri} in {working_directory}')
|
|
189
193
|
start = self._current()
|
|
190
194
|
cwd = getcwd()
|
|
191
|
-
|
|
195
|
+
_, f_name = StorageClientWrapper._decompose(uri)
|
|
192
196
|
fqn = path.join(working_directory, f_name)
|
|
193
197
|
chdir(working_directory)
|
|
194
198
|
try:
|
|
195
199
|
local_meta = get_local_file_info(f_name)
|
|
196
200
|
encoding = get_file_encoding(f_name)
|
|
197
|
-
replace = True
|
|
198
|
-
cadc_meta = self.info(uri)
|
|
199
|
-
if cadc_meta is None:
|
|
200
|
-
replace = False
|
|
201
201
|
self._logger.debug(
|
|
202
|
-
f'uri {uri} src {fqn}
|
|
202
|
+
f'uri {uri} src {fqn} file_type {local_meta.file_type} encoding {encoding} '
|
|
203
203
|
f'md5_checksum {local_meta.md5sum}'
|
|
204
204
|
)
|
|
205
205
|
self._cadc_client.cadcput(
|
|
206
206
|
uri,
|
|
207
207
|
src=f_name,
|
|
208
|
-
replace=replace,
|
|
209
208
|
file_type=local_meta.file_type,
|
|
210
209
|
file_encoding=encoding,
|
|
211
210
|
md5_checksum=local_meta.md5sum,
|
|
@@ -279,15 +278,22 @@ def _clean_headers(fits_header):
|
|
|
279
278
|
|
|
280
279
|
|
|
281
280
|
def get_local_headers_from_fits(fqn):
|
|
282
|
-
"""Create a list of fits.Header instances from a
|
|
283
|
-
|
|
284
|
-
|
|
281
|
+
"""Create a list of fits.Header instances from a FITS file or plain-text
|
|
282
|
+
header file.
|
|
283
|
+
|
|
284
|
+
When the file is not a FITS binary (e.g. a ``*.fits.header`` text file),
|
|
285
|
+
headers are parsed from text instead.
|
|
286
|
+
|
|
287
|
+
:param fqn: fully-qualified name of the file on disk
|
|
288
|
+
:return: list of fits.Header instances
|
|
285
289
|
"""
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
290
|
+
try:
|
|
291
|
+
with fits.open(fqn, memmap=True, lazy_load_hdus=True) as hdulist:
|
|
292
|
+
hdulist.verify('fix')
|
|
293
|
+
return [h.header for h in hdulist]
|
|
294
|
+
except OSError:
|
|
295
|
+
with open(fqn, encoding='utf-8', errors='replace') as f:
|
|
296
|
+
return make_headers_from_string(f.read())
|
|
291
297
|
|
|
292
298
|
|
|
293
299
|
def get_local_file_headers(fqn):
|
|
@@ -457,10 +457,12 @@ def update_blueprint(obs_blueprint, artifact_uri=None, config=None, defaults=Non
|
|
|
457
457
|
return None
|
|
458
458
|
|
|
459
459
|
|
|
460
|
-
def
|
|
460
|
+
def build_fits2caom2_parser():
|
|
461
|
+
"""
|
|
462
|
+
Build the ArgumentParser for fits2caom2 (without parsing argv).
|
|
463
|
+
"""
|
|
461
464
|
parser = caom2blueprint.get_arg_parser()
|
|
462
465
|
|
|
463
|
-
# add legacy fits2caom2 arguments
|
|
464
466
|
parser.add_argument(
|
|
465
467
|
'--config',
|
|
466
468
|
required=False,
|
|
@@ -469,6 +471,11 @@ def main_app():
|
|
|
469
471
|
|
|
470
472
|
parser.add_argument('--default', help='file with default values for keywords')
|
|
471
473
|
parser.add_argument('--override', help='file with override values for keywords')
|
|
474
|
+
return parser
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def main_app():
|
|
478
|
+
parser = build_fits2caom2_parser()
|
|
472
479
|
|
|
473
480
|
if len(sys.argv) < 2:
|
|
474
481
|
# correct error message when running python3
|
|
@@ -507,9 +514,10 @@ def main_app():
|
|
|
507
514
|
try:
|
|
508
515
|
caom2blueprint.proc(args, obs_blueprint)
|
|
509
516
|
except Exception as e:
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
logging.
|
|
517
|
+
from cadcutils.util.cli_errors import format_user_error
|
|
518
|
+
logging.error(format_user_error(e))
|
|
519
|
+
if logging.getLogger().isEnabledFor(logging.DEBUG):
|
|
520
|
+
logging.debug(traceback.format_exc())
|
|
513
521
|
sys.exit(-1)
|
|
514
522
|
|
|
515
523
|
logging.info("DONE")
|
|
@@ -284,7 +284,7 @@ class BlueprintParser:
|
|
|
284
284
|
self.logger.debug(f'Could not find \'{lookup}\' in caom2blueprint ' f'configuration.')
|
|
285
285
|
|
|
286
286
|
# if there's something useful as a value in the keywords, extract it
|
|
287
|
-
if keywords:
|
|
287
|
+
if keywords is not None and any(keywords):
|
|
288
288
|
if ObsBlueprint.needs_lookup(keywords):
|
|
289
289
|
# if there's a default value use it
|
|
290
290
|
if keywords[1]:
|
|
@@ -351,17 +351,18 @@ class BlueprintParser:
|
|
|
351
351
|
self.logger.debug(tb)
|
|
352
352
|
self.logger.error(e)
|
|
353
353
|
return result
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
354
|
+
if execute:
|
|
355
|
+
try:
|
|
356
|
+
result = execute(parameter)
|
|
357
|
+
self.logger.debug(f'Key {key} calculated value of {result} using {value} type {type(result)}')
|
|
358
|
+
except Exception as e:
|
|
359
|
+
msg = f'Failed to execute {execute.__name__} for {key} in {self.uri}'
|
|
360
|
+
self.logger.error(msg)
|
|
361
|
+
self.logger.debug(f'Input parameter was {parameter}, value was {value}')
|
|
362
|
+
self._errors.append(msg)
|
|
363
|
+
tb = traceback.format_exc()
|
|
364
|
+
self.logger.debug(tb)
|
|
365
|
+
self.logger.error(e)
|
|
365
366
|
return result
|
|
366
367
|
|
|
367
368
|
def _execute_external_instance(self, value, key, extension):
|
|
@@ -423,6 +424,7 @@ class BlueprintParser:
|
|
|
423
424
|
# CFHT 2003/03/29,01:34:54
|
|
424
425
|
# CFHT 2003/03/29
|
|
425
426
|
# DDO 12/02/95
|
|
427
|
+
# TAOSII 2024-01-26T14:52:49Z
|
|
426
428
|
for dt_format in [
|
|
427
429
|
'%Y-%m-%dT%H:%M:%S',
|
|
428
430
|
'%Y-%m-%dT%H:%M:%S.%f',
|
|
@@ -435,6 +437,7 @@ class BlueprintParser:
|
|
|
435
437
|
'%d/%m/%y',
|
|
436
438
|
'%d/%m/%y %H:%M:%S',
|
|
437
439
|
'%d-%m-%Y',
|
|
440
|
+
'%Y-%m-%dT%H:%M:%SZ',
|
|
438
441
|
]:
|
|
439
442
|
try:
|
|
440
443
|
result = datetime.strptime(from_value, dt_format)
|
|
@@ -545,13 +548,17 @@ class BlueprintParser:
|
|
|
545
548
|
if name:
|
|
546
549
|
prov = caom2.Provenance(name, p_version, project, producer, run_id, reference, last_executed)
|
|
547
550
|
ContentParser._add_keywords(keywords, current, prov)
|
|
548
|
-
if inputs:
|
|
551
|
+
if inputs is not None and any(inputs):
|
|
549
552
|
if isinstance(inputs, caom2.TypedSet):
|
|
550
553
|
for i in inputs:
|
|
551
554
|
prov.inputs.add(i)
|
|
552
555
|
else:
|
|
553
|
-
|
|
554
|
-
|
|
556
|
+
if isinstance(inputs, str):
|
|
557
|
+
for i in inputs.split():
|
|
558
|
+
prov.inputs.add(caom2.PlaneURI(str(i)))
|
|
559
|
+
else:
|
|
560
|
+
for i in inputs:
|
|
561
|
+
prov.inputs.add(caom2.PlaneURI(str(i)))
|
|
555
562
|
else:
|
|
556
563
|
if current is not None and len(current.inputs) > 0:
|
|
557
564
|
# preserve the original value
|
|
@@ -572,10 +579,13 @@ class BlueprintParser:
|
|
|
572
579
|
|
|
573
580
|
|
|
574
581
|
class ContentParser(BlueprintParser):
|
|
575
|
-
def __init__(self, obs_blueprint=None, uri=None):
|
|
582
|
+
def __init__(self, obs_blueprint=None, uri=None, extension_start_index=0, extension_end_index=None):
|
|
576
583
|
super().__init__(obs_blueprint, uri)
|
|
584
|
+
# for those cases where the extensions of interest are not all the extensions in the original file
|
|
585
|
+
self._extension_start_index = extension_start_index
|
|
586
|
+
self._extension_end_index = extension_end_index if extension_end_index else self._get_num_parts()
|
|
577
587
|
self._wcs_parsers = {}
|
|
578
|
-
self.
|
|
588
|
+
self._set_wcs_parsers(obs_blueprint)
|
|
579
589
|
|
|
580
590
|
def _get_chunk_naxis(self, chunk, index):
|
|
581
591
|
chunk.naxis = self._get_from_list('Chunk.naxis', index, self.blueprint.get_configed_axes_count())
|
|
@@ -585,6 +595,9 @@ class ContentParser(BlueprintParser):
|
|
|
585
595
|
"""
|
|
586
596
|
return len(self._blueprint._extensions) + 1
|
|
587
597
|
|
|
598
|
+
def _set_wcs_parsers(self, obs_blueprint):
|
|
599
|
+
self._wcs_parsers[0] = WcsParser(obs_blueprint, extension=self._extension_start_index)
|
|
600
|
+
|
|
588
601
|
def augment_artifact(self, artifact):
|
|
589
602
|
"""
|
|
590
603
|
Augments a given CAOM2 artifact with available content information
|
|
@@ -592,23 +605,26 @@ class ContentParser(BlueprintParser):
|
|
|
592
605
|
:param index: int Part name
|
|
593
606
|
"""
|
|
594
607
|
super().augment_artifact(artifact)
|
|
595
|
-
|
|
596
608
|
self.logger.debug(f'Begin content artifact augmentation for {artifact.uri}')
|
|
597
609
|
|
|
598
610
|
if self.blueprint.get_configed_axes_count() == 0:
|
|
599
611
|
raise TypeError(f'No WCS Data. End content artifact augmentation for ' f'{artifact.uri}.')
|
|
600
612
|
|
|
601
|
-
for index in range(
|
|
613
|
+
for index in range(self._extension_start_index, self._extension_end_index):
|
|
602
614
|
if self.add_parts(artifact, index):
|
|
603
615
|
part = artifact.parts[str(index)]
|
|
604
616
|
part.product_type = self._get_from_list('Part.productType', index)
|
|
605
|
-
part.meta_producer = self._get_from_list(
|
|
617
|
+
part.meta_producer = self._get_from_list(
|
|
618
|
+
'Part.metaProducer', index=self._extension_start_index, current=part.meta_producer
|
|
619
|
+
)
|
|
606
620
|
|
|
607
621
|
# each Part has one Chunk, if it's not an empty part as determined just previously
|
|
608
622
|
if not part.chunks:
|
|
609
623
|
part.chunks.append(caom2.Chunk())
|
|
610
624
|
chunk = part.chunks[0]
|
|
611
|
-
chunk.meta_producer = self._get_from_list(
|
|
625
|
+
chunk.meta_producer = self._get_from_list(
|
|
626
|
+
'Chunk.metaProducer', index=self._extension_start_index, current=chunk.meta_producer
|
|
627
|
+
)
|
|
612
628
|
|
|
613
629
|
self._get_chunk_naxis(chunk, index)
|
|
614
630
|
|
|
@@ -847,7 +863,8 @@ class ContentParser(BlueprintParser):
|
|
|
847
863
|
return members
|
|
848
864
|
|
|
849
865
|
def _get_axis_wcs(self, label, wcs, index):
|
|
850
|
-
"""Helper function to construct a CoordAxis1D instance, with all
|
|
866
|
+
"""Helper function to construct a CoordAxis1D instance, with all
|
|
867
|
+
it's members, from the blueprint.
|
|
851
868
|
|
|
852
869
|
:param label: axis name - must be one of 'custom', 'energy', 'time', or 'polarization', as it's used for the
|
|
853
870
|
blueprint lookup.
|
|
@@ -1460,7 +1477,7 @@ class FitsParser(ContentParser):
|
|
|
1460
1477
|
|
|
1461
1478
|
"""
|
|
1462
1479
|
|
|
1463
|
-
def __init__(self, src, obs_blueprint=None, uri=None):
|
|
1480
|
+
def __init__(self, src, obs_blueprint=None, uri=None, extension_start_index=0, extension_end_index=None):
|
|
1464
1481
|
"""
|
|
1465
1482
|
Ctor
|
|
1466
1483
|
:param src: List of headers (dictionary of FITS keywords:value) with one header for each extension or a FITS
|
|
@@ -1487,6 +1504,8 @@ class FitsParser(ContentParser):
|
|
|
1487
1504
|
self._errors = []
|
|
1488
1505
|
# for command-line parameter to module execution
|
|
1489
1506
|
self.uri = uri
|
|
1507
|
+
self._extension_start_index = extension_start_index
|
|
1508
|
+
self._extension_end_index = extension_end_index if extension_end_index is not None else self._get_num_parts()
|
|
1490
1509
|
self.apply_blueprint()
|
|
1491
1510
|
|
|
1492
1511
|
def _get_num_parts(self):
|
|
@@ -1845,22 +1864,19 @@ class Hdf5Parser(ContentParser):
|
|
|
1845
1864
|
- use the astropy.wcs instance and other blueprint metadata to fill the CAOM2 record.
|
|
1846
1865
|
"""
|
|
1847
1866
|
|
|
1848
|
-
def __init__(self, obs_blueprint, uri, h5_file,
|
|
1867
|
+
def __init__(self, obs_blueprint, uri, h5_file, extension_names=None, extension_start_index=0,
|
|
1868
|
+
extension_end_index=None):
|
|
1849
1869
|
"""
|
|
1850
1870
|
:param obs_blueprint: Hdf5ObsBlueprint instance
|
|
1851
1871
|
:param uri: which artifact augmentation is based on
|
|
1852
1872
|
:param h5_file: h5py file handle
|
|
1853
|
-
:param
|
|
1873
|
+
:param extension_names: list of str where Chunk metadata starts. There is one Part/Chunk per list entry
|
|
1854
1874
|
"""
|
|
1855
1875
|
self._file = h5_file
|
|
1856
|
-
#
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
super().__init__(obs_blueprint, uri)
|
|
1861
|
-
# used to set the astropy wcs info, resulting in a validated wcs that can be used to construct a valid CAOM2
|
|
1862
|
-
# record
|
|
1863
|
-
self._wcs_parsers = {}
|
|
1876
|
+
# the length of the array is the number of Parts in an HDF5 file,
|
|
1877
|
+
# and the values are HDF5 lookup path names.
|
|
1878
|
+
self._extension_names = extension_names
|
|
1879
|
+
super().__init__(obs_blueprint, uri, extension_start_index, extension_end_index)
|
|
1864
1880
|
|
|
1865
1881
|
def _get_num_parts(self):
|
|
1866
1882
|
"""return the number of Parts to create for a CAOM record
|
|
@@ -1871,15 +1887,29 @@ class Hdf5Parser(ContentParser):
|
|
|
1871
1887
|
result = 1
|
|
1872
1888
|
return result
|
|
1873
1889
|
|
|
1890
|
+
def _set_wcs_parsers(self, obs_blueprint):
|
|
1891
|
+
# used to set the astropy wcs info, resulting in a validated wcs that can be used to construct a valid CAOM2
|
|
1892
|
+
# record
|
|
1893
|
+
# This method call is over-writing the default behaviour in the ContentParser class. The default behaviour
|
|
1894
|
+
# uses the obs_blueprint. This method is called in the ContentParser constructor.
|
|
1895
|
+
self._wcs_parsers = {}
|
|
1896
|
+
|
|
1874
1897
|
def apply_blueprint_from_file(self):
|
|
1875
1898
|
"""
|
|
1876
1899
|
Retrieve metadata from file, cache in the blueprint.
|
|
1877
1900
|
"""
|
|
1878
1901
|
self.logger.debug('Begin apply_blueprint_from_file')
|
|
1879
|
-
# h5py is an extra in this package since most collections do not
|
|
1902
|
+
# h5py is an extra in this package since most collections do not
|
|
1903
|
+
# require it
|
|
1880
1904
|
import h5py
|
|
1881
1905
|
|
|
1882
|
-
individual, multi, attributes = self._extract_path_names_from_blueprint()
|
|
1906
|
+
individual, multi, attributes, candidate_extensions = self._extract_path_names_from_blueprint()
|
|
1907
|
+
if self._extension_names is None and len(candidate_extensions) > 0:
|
|
1908
|
+
self._find_extension_names(candidate_extensions)
|
|
1909
|
+
for index, _ in enumerate(self._extension_names):
|
|
1910
|
+
self._blueprint._extensions[index] = {}
|
|
1911
|
+
else:
|
|
1912
|
+
self._blueprint._extensions[0] = {}
|
|
1883
1913
|
filtered_individual = [ii for ii in individual.keys() if '(' in ii]
|
|
1884
1914
|
|
|
1885
1915
|
def _extract_from_item(name, object):
|
|
@@ -1890,16 +1920,8 @@ class Hdf5Parser(ContentParser):
|
|
|
1890
1920
|
:param name: fully-qualified HDF5 path name
|
|
1891
1921
|
:param object: what the HDF5 path name points to
|
|
1892
1922
|
"""
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
# store the names and locations of the Part/Chunk metadata
|
|
1896
|
-
temp = f'{name}/{path_name}'
|
|
1897
|
-
self.logger.debug(f'Adding extension {temp}')
|
|
1898
|
-
self._extension_names.append(temp)
|
|
1899
|
-
self._blueprint._extensions[ii] = {}
|
|
1900
|
-
|
|
1901
|
-
# If it's the Part/Chunk metadata, capture it to extensions. Syntax of the keys described in
|
|
1902
|
-
# Hdf5ObsBlueprint class.
|
|
1923
|
+
# If it's the Part/Chunk metadata, capture it to extensions.
|
|
1924
|
+
# Syntax of the keys described in Hdf5ObsBlueprint class.
|
|
1903
1925
|
for part_index, part_name in enumerate(self._extension_names):
|
|
1904
1926
|
if name.startswith(part_name) and isinstance(object, h5py.Dataset) and object.dtype.names is not None:
|
|
1905
1927
|
for d_name in object.dtype.names:
|
|
@@ -1973,20 +1995,54 @@ class Hdf5Parser(ContentParser):
|
|
|
1973
1995
|
are _CAOM2_ELEMENT strings.
|
|
1974
1996
|
attributes - a dictionary of lists, keys reference expected content from the h5py.File().attrs data
|
|
1975
1997
|
structure and its keys.
|
|
1998
|
+
extensions - a list of prefixes for identifying extensions
|
|
1976
1999
|
"""
|
|
1977
2000
|
individual = defaultdict(list)
|
|
1978
2001
|
multi = defaultdict(list)
|
|
1979
2002
|
attributes = defaultdict(list)
|
|
2003
|
+
extensions = []
|
|
1980
2004
|
for key, value in self._blueprint._plan.items():
|
|
1981
2005
|
if ObsBlueprint.needs_lookup(value):
|
|
1982
2006
|
for ii in value[0]:
|
|
1983
2007
|
if ii.startswith('//'):
|
|
1984
2008
|
individual[ii].append(key)
|
|
1985
2009
|
elif ii.startswith('/'):
|
|
1986
|
-
|
|
2010
|
+
if '{}' in ii:
|
|
2011
|
+
bits = ii.split('{}')
|
|
2012
|
+
extensions.append(bits[0])
|
|
2013
|
+
multi[bits[1]].append(key)
|
|
2014
|
+
else:
|
|
2015
|
+
multi[ii].append(key)
|
|
1987
2016
|
else:
|
|
1988
2017
|
attributes[ii].append(key)
|
|
1989
|
-
|
|
2018
|
+
|
|
2019
|
+
return individual, multi, attributes, list(set(extensions))
|
|
2020
|
+
|
|
2021
|
+
def _find_extension_names(self, candidates):
|
|
2022
|
+
""" if the HDF5 file has a structure where-by more than one Chunk (the equivalent of a FITS HDU extension)
|
|
2023
|
+
is defined, try to guess that structure
|
|
2024
|
+
"""
|
|
2025
|
+
candidate_extension_names = []
|
|
2026
|
+
|
|
2027
|
+
def _extract_extension_prefixes(name, object):
|
|
2028
|
+
"""
|
|
2029
|
+
Function signature dictated by h5py visititems implementation. Executed for each dataset/group in an
|
|
2030
|
+
HDF5 file.
|
|
2031
|
+
|
|
2032
|
+
:param name: fully-qualified HDF5 path name
|
|
2033
|
+
:param object: what the HDF5 path name points to
|
|
2034
|
+
"""
|
|
2035
|
+
for part_name in candidates:
|
|
2036
|
+
y = part_name.replace('/', '', 1)
|
|
2037
|
+
if name.startswith(y):
|
|
2038
|
+
x = name.split(y)[1].split('/')
|
|
2039
|
+
temp = f'{y}{x[0]}'
|
|
2040
|
+
candidate_extension_names.append(temp)
|
|
2041
|
+
self._extension_names = list(sorted(set(candidate_extension_names)))
|
|
2042
|
+
|
|
2043
|
+
self._file.visititems(_extract_extension_prefixes)
|
|
2044
|
+
msg = '\n'.join(ii for ii in self._extension_names)
|
|
2045
|
+
self.logger.info(f'Found extension_names:\n{msg}')
|
|
1990
2046
|
|
|
1991
2047
|
def apply_blueprint(self):
|
|
1992
2048
|
self.logger.debug('Begin apply_blueprint')
|
|
@@ -2015,6 +2071,7 @@ class Hdf5Parser(ContentParser):
|
|
|
2015
2071
|
else:
|
|
2016
2072
|
exts[extension][key] = self._execute_external_instance(value, key, extension)
|
|
2017
2073
|
|
|
2074
|
+
# apply overrides
|
|
2018
2075
|
# blueprint already contains all the overrides, only need to make sure the overrides get applied to all the
|
|
2019
2076
|
# extensions
|
|
2020
2077
|
for extension in exts:
|
|
@@ -2032,6 +2089,7 @@ class Hdf5Parser(ContentParser):
|
|
|
2032
2089
|
exts[extension][key] = value
|
|
2033
2090
|
self.logger.debug(f'{key}: set to {value} in extension {extension}')
|
|
2034
2091
|
|
|
2092
|
+
# apply defaults
|
|
2035
2093
|
# if no values have been set by file lookups, function execution, or applying overrides, apply defaults,
|
|
2036
2094
|
# including to all extensions
|
|
2037
2095
|
for key, value in plan.items():
|
|
@@ -2054,7 +2112,7 @@ class Hdf5Parser(ContentParser):
|
|
|
2054
2112
|
return
|
|
2055
2113
|
|
|
2056
2114
|
def augment_artifact(self, artifact):
|
|
2057
|
-
for ii in range(
|
|
2115
|
+
for ii in range(self._extension_start_index, self._extension_end_index):
|
|
2058
2116
|
# one WCS parser per Part/Chunk
|
|
2059
2117
|
self._wcs_parsers[ii] = Hdf5WcsParser(self._blueprint, ii)
|
|
2060
2118
|
super().augment_artifact(artifact)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
usage: fits2caom2 [-h]
|
|
1
|
+
usage: fits2caom2 [-h]
|
|
2
|
+
[--cert CERT | -n | --netrc-file NETRC_FILE | -u USER | --token TOKEN]
|
|
2
3
|
[--resource-id RESOURCE_ID] [-q | -v] [-V] [--dumpconfig]
|
|
3
4
|
[--not_connected] [--no_validate] [-o OUT_OBS_XML]
|
|
4
5
|
(-i IN_OBS_XML | --observation collection observationID)
|
|
@@ -41,6 +42,7 @@ optional arguments:
|
|
|
41
42
|
--resource-id RESOURCE_ID
|
|
42
43
|
resource identifier (default
|
|
43
44
|
ivo://cadc.nrc.ca/global/raven)
|
|
45
|
+
--token TOKEN authentication token to use.
|
|
44
46
|
-u, --user USER name of user to authenticate. Note: application
|
|
45
47
|
prompts for the corresponding password!
|
|
46
48
|
-v, --verbose verbose messages
|