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.
Files changed (59) hide show
  1. {caom2utils-1.7.2/caom2utils.egg-info → caom2utils-1.7.4}/PKG-INFO +11 -5
  2. {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/blueprints.py +6 -4
  3. {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/caom2blueprint.py +19 -12
  4. {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/data_util.py +24 -18
  5. {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/legacy.py +13 -5
  6. {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/parsers.py +106 -48
  7. {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/data/help.txt +3 -1
  8. caom2utils-1.7.4/caom2utils/tests/test_cli_parsers.py +146 -0
  9. {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/test_collections.py +4 -1
  10. {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/test_data_util.py +26 -6
  11. {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/test_fits2caom2.py +37 -46
  12. caom2utils-1.7.4/caom2utils/version.py +1 -0
  13. {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/wcs_parsers.py +3 -2
  14. {caom2utils-1.7.2 → caom2utils-1.7.4/caom2utils.egg-info}/PKG-INFO +11 -5
  15. {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils.egg-info/SOURCES.txt +2 -4
  16. {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils.egg-info/requires.txt +2 -2
  17. {caom2utils-1.7.2 → caom2utils-1.7.4}/setup.cfg +6 -6
  18. {caom2utils-1.7.2 → caom2utils-1.7.4}/setup.py +6 -1
  19. caom2utils-1.7.2/caom2utils/tests/data/missing_observation_help.txt +0 -8
  20. caom2utils-1.7.2/caom2utils/tests/data/missing_positional_argument_help.txt +0 -10
  21. caom2utils-1.7.2/caom2utils/tests/data/too_few_arguments_help.txt +0 -0
  22. caom2utils-1.7.2/caom2utils/version.py +0 -1
  23. {caom2utils-1.7.2 → caom2utils-1.7.4}/LICENSE +0 -0
  24. {caom2utils-1.7.2 → caom2utils-1.7.4}/MANIFEST.in +0 -0
  25. {caom2utils-1.7.2 → caom2utils-1.7.4}/README.rst +0 -0
  26. {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/__init__.py +0 -0
  27. {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/caomvalidator.py +0 -0
  28. {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/polygonvalidator.py +0 -0
  29. {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/__init__.py +0 -0
  30. {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/conftest.py +0 -0
  31. {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/data/4axes.fits +0 -0
  32. {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/data/4axes.override +0 -0
  33. {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/data/4axes_obs.fits +0 -0
  34. {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/data/SampleComposite-CAOM-2.3.xml +0 -0
  35. {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/data/bad_product_id.txt +0 -0
  36. {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/data/bad_product_id.xml +0 -0
  37. {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/data/edge_case.blueprint +0 -0
  38. {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/data/fits2caom2.config +0 -0
  39. {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/data/java.config +0 -0
  40. {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/data/missing_product_id.txt +0 -0
  41. {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/data/nonconformant.py +0 -0
  42. {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/data/test.override +0 -0
  43. {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/data/test_plugin.py +0 -0
  44. {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/data/test_plugin_class.py +0 -0
  45. {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/data/text.override +0 -0
  46. {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/data/time_axes.fits +0 -0
  47. {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/test_caomvalidator.py +0 -0
  48. {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/test_convert_from_java.py +0 -0
  49. {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/test_custom_axis_util.py +0 -0
  50. {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/test_obs_blueprint.py +0 -0
  51. {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/test_polygonvalidator.py +0 -0
  52. {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/test_si_uris.py +0 -0
  53. {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/tests/test_wcsvalidator.py +0 -0
  54. {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/wcs_util.py +0 -0
  55. {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils/wcsvalidator.py +0 -0
  56. {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils.egg-info/dependency_links.txt +0 -0
  57. {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils.egg-info/entry_points.txt +0 -0
  58. {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils.egg-info/not-zip-safe +0 -0
  59. {caom2utils-1.7.2 → caom2utils-1.7.4}/caom2utils.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: caom2utils
3
- Version: 1.7.2
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
- Requires-Python: >=3.8, <4
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.0
16
- Requires-Dist: caom2>=2.6
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
- ('No TTYPE attributes in extension {} associated ' 'with keyword {}').format(
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 "find_roots_here" parameter for Hdf5Parser
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 caom2gen():
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
- tb = traceback.format_exc()
534
- logging.error(tb)
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(subject, resource_id=args.resource_id)
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
- if args.resource_id is None:
835
- # if the resource_id is Undefined, using CadcDataClient
836
- client = data_util.StorageClientWrapper(subject, using_storage_inventory=False)
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) 2021. (c) 2021.
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(subject=subject, resource_id=resource_id)
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
- archive, f_name = StorageClientWrapper._decompose(uri)
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} replace {replace} file_type {local_meta.file_type} encoding {encoding} '
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 fits file.
283
- :param fqn str fully-qualified name of the FITS file on disk
284
- :return list of fits.Header instances
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
- hdulist = fits.open(fqn, memmap=True, lazy_load_hdus=True)
287
- hdulist.verify('fix')
288
- hdulist.close()
289
- headers = [h.header for h in hdulist]
290
- return headers
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 main_app():
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
- logging.error(e)
511
- tb = traceback.format_exc()
512
- logging.debug(tb)
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
- try:
355
- result = execute(parameter)
356
- self.logger.debug(f'Key {key} calculated value of {result} using {value} type {type(result)}')
357
- except Exception as e:
358
- msg = f'Failed to execute {execute.__name__} for {key} in {self.uri}'
359
- self.logger.error(msg)
360
- self.logger.debug(f'Input parameter was {parameter}, value was {value}')
361
- self._errors.append(msg)
362
- tb = traceback.format_exc()
363
- self.logger.debug(tb)
364
- self.logger.error(e)
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
- for i in inputs.split():
554
- prov.inputs.add(caom2.PlaneURI(str(i)))
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._wcs_parsers[0] = WcsParser(obs_blueprint, extension=0)
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(0, self._get_num_parts()):
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('Part.metaProducer', index=0, current=part.meta_producer)
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('Chunk.metaProducer', index=0, current=chunk.meta_producer)
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 its members, from the blueprint.
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, find_roots_here='sitedata'):
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 find_roots_here: str location where Chunk metadata starts
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
- # where N Chunk metadata starts
1857
- self._find_roots_here = find_roots_here
1858
- # the length of the array is the number of Parts in an HDF5 file, and the values are HDF5 lookup path names.
1859
- self._extension_names = []
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 require it
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
- if name == self._find_roots_here:
1894
- for ii, path_name in enumerate(object.keys()):
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
- multi[ii].append(key)
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
- return individual, multi, attributes
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(0, self._get_num_parts()):
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] [--cert CERT | -n | --netrc-file NETRC_FILE | -u USER]
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