caom2utils 1.7.2__tar.gz → 1.7.3__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 (58) hide show
  1. {caom2utils-1.7.2/caom2utils.egg-info → caom2utils-1.7.3}/PKG-INFO +4 -3
  2. {caom2utils-1.7.2 → caom2utils-1.7.3}/caom2utils/blueprints.py +5 -1
  3. {caom2utils-1.7.2 → caom2utils-1.7.3}/caom2utils/data_util.py +3 -8
  4. {caom2utils-1.7.2 → caom2utils-1.7.3}/caom2utils/parsers.py +104 -47
  5. {caom2utils-1.7.2 → caom2utils-1.7.3}/caom2utils/tests/test_data_util.py +0 -1
  6. {caom2utils-1.7.2 → caom2utils-1.7.3}/caom2utils/tests/test_fits2caom2.py +17 -0
  7. caom2utils-1.7.3/caom2utils/version.py +1 -0
  8. {caom2utils-1.7.2 → caom2utils-1.7.3}/caom2utils/wcs_parsers.py +3 -2
  9. {caom2utils-1.7.2 → caom2utils-1.7.3/caom2utils.egg-info}/PKG-INFO +4 -3
  10. {caom2utils-1.7.2 → caom2utils-1.7.3}/caom2utils.egg-info/requires.txt +1 -1
  11. {caom2utils-1.7.2 → caom2utils-1.7.3}/setup.cfg +2 -2
  12. caom2utils-1.7.2/caom2utils/version.py +0 -1
  13. {caom2utils-1.7.2 → caom2utils-1.7.3}/LICENSE +0 -0
  14. {caom2utils-1.7.2 → caom2utils-1.7.3}/MANIFEST.in +0 -0
  15. {caom2utils-1.7.2 → caom2utils-1.7.3}/README.rst +0 -0
  16. {caom2utils-1.7.2 → caom2utils-1.7.3}/caom2utils/__init__.py +0 -0
  17. {caom2utils-1.7.2 → caom2utils-1.7.3}/caom2utils/caom2blueprint.py +0 -0
  18. {caom2utils-1.7.2 → caom2utils-1.7.3}/caom2utils/caomvalidator.py +0 -0
  19. {caom2utils-1.7.2 → caom2utils-1.7.3}/caom2utils/legacy.py +0 -0
  20. {caom2utils-1.7.2 → caom2utils-1.7.3}/caom2utils/polygonvalidator.py +0 -0
  21. {caom2utils-1.7.2 → caom2utils-1.7.3}/caom2utils/tests/__init__.py +0 -0
  22. {caom2utils-1.7.2 → caom2utils-1.7.3}/caom2utils/tests/conftest.py +0 -0
  23. {caom2utils-1.7.2 → caom2utils-1.7.3}/caom2utils/tests/data/4axes.fits +0 -0
  24. {caom2utils-1.7.2 → caom2utils-1.7.3}/caom2utils/tests/data/4axes.override +0 -0
  25. {caom2utils-1.7.2 → caom2utils-1.7.3}/caom2utils/tests/data/4axes_obs.fits +0 -0
  26. {caom2utils-1.7.2 → caom2utils-1.7.3}/caom2utils/tests/data/SampleComposite-CAOM-2.3.xml +0 -0
  27. {caom2utils-1.7.2 → caom2utils-1.7.3}/caom2utils/tests/data/bad_product_id.txt +0 -0
  28. {caom2utils-1.7.2 → caom2utils-1.7.3}/caom2utils/tests/data/bad_product_id.xml +0 -0
  29. {caom2utils-1.7.2 → caom2utils-1.7.3}/caom2utils/tests/data/edge_case.blueprint +0 -0
  30. {caom2utils-1.7.2 → caom2utils-1.7.3}/caom2utils/tests/data/fits2caom2.config +0 -0
  31. {caom2utils-1.7.2 → caom2utils-1.7.3}/caom2utils/tests/data/help.txt +0 -0
  32. {caom2utils-1.7.2 → caom2utils-1.7.3}/caom2utils/tests/data/java.config +0 -0
  33. {caom2utils-1.7.2 → caom2utils-1.7.3}/caom2utils/tests/data/missing_observation_help.txt +0 -0
  34. {caom2utils-1.7.2 → caom2utils-1.7.3}/caom2utils/tests/data/missing_positional_argument_help.txt +0 -0
  35. {caom2utils-1.7.2 → caom2utils-1.7.3}/caom2utils/tests/data/missing_product_id.txt +0 -0
  36. {caom2utils-1.7.2 → caom2utils-1.7.3}/caom2utils/tests/data/nonconformant.py +0 -0
  37. {caom2utils-1.7.2 → caom2utils-1.7.3}/caom2utils/tests/data/test.override +0 -0
  38. {caom2utils-1.7.2 → caom2utils-1.7.3}/caom2utils/tests/data/test_plugin.py +0 -0
  39. {caom2utils-1.7.2 → caom2utils-1.7.3}/caom2utils/tests/data/test_plugin_class.py +0 -0
  40. {caom2utils-1.7.2 → caom2utils-1.7.3}/caom2utils/tests/data/text.override +0 -0
  41. {caom2utils-1.7.2 → caom2utils-1.7.3}/caom2utils/tests/data/time_axes.fits +0 -0
  42. {caom2utils-1.7.2 → caom2utils-1.7.3}/caom2utils/tests/data/too_few_arguments_help.txt +0 -0
  43. {caom2utils-1.7.2 → caom2utils-1.7.3}/caom2utils/tests/test_caomvalidator.py +0 -0
  44. {caom2utils-1.7.2 → caom2utils-1.7.3}/caom2utils/tests/test_collections.py +0 -0
  45. {caom2utils-1.7.2 → caom2utils-1.7.3}/caom2utils/tests/test_convert_from_java.py +0 -0
  46. {caom2utils-1.7.2 → caom2utils-1.7.3}/caom2utils/tests/test_custom_axis_util.py +0 -0
  47. {caom2utils-1.7.2 → caom2utils-1.7.3}/caom2utils/tests/test_obs_blueprint.py +0 -0
  48. {caom2utils-1.7.2 → caom2utils-1.7.3}/caom2utils/tests/test_polygonvalidator.py +0 -0
  49. {caom2utils-1.7.2 → caom2utils-1.7.3}/caom2utils/tests/test_si_uris.py +0 -0
  50. {caom2utils-1.7.2 → caom2utils-1.7.3}/caom2utils/tests/test_wcsvalidator.py +0 -0
  51. {caom2utils-1.7.2 → caom2utils-1.7.3}/caom2utils/wcs_util.py +0 -0
  52. {caom2utils-1.7.2 → caom2utils-1.7.3}/caom2utils/wcsvalidator.py +0 -0
  53. {caom2utils-1.7.2 → caom2utils-1.7.3}/caom2utils.egg-info/SOURCES.txt +0 -0
  54. {caom2utils-1.7.2 → caom2utils-1.7.3}/caom2utils.egg-info/dependency_links.txt +0 -0
  55. {caom2utils-1.7.2 → caom2utils-1.7.3}/caom2utils.egg-info/entry_points.txt +0 -0
  56. {caom2utils-1.7.2 → caom2utils-1.7.3}/caom2utils.egg-info/not-zip-safe +0 -0
  57. {caom2utils-1.7.2 → caom2utils-1.7.3}/caom2utils.egg-info/top_level.txt +0 -0
  58. {caom2utils-1.7.2 → caom2utils-1.7.3}/setup.py +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.3
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
@@ -12,7 +12,7 @@ Classifier: Programming Language :: Python
12
12
  Classifier: Programming Language :: Python :: 3
13
13
  Requires-Python: >=3.8, <4
14
14
  License-File: LICENSE
15
- Requires-Dist: cadcdata>=2.0
15
+ Requires-Dist: cadcdata>=2.5.2
16
16
  Requires-Dist: caom2>=2.6
17
17
  Requires-Dist: astropy>=2.0
18
18
  Requires-Dist: spherical-geometry>=1.2.11
@@ -30,6 +30,7 @@ Dynamic: classifier
30
30
  Dynamic: description
31
31
  Dynamic: home-page
32
32
  Dynamic: license
33
+ Dynamic: license-file
33
34
  Dynamic: requires-python
34
35
  Dynamic: summary
35
36
 
@@ -1187,10 +1187,14 @@ class Hdf5ObsBlueprint(ObsBlueprint):
1187
1187
  # lookup value starting with // means rooted at base of the hdf5 file
1188
1188
  ob.add_attribute('Observation.target.name', '//header/object/obj_id')
1189
1189
 
1190
- # lookup value starting with / means rooted at the base of the "find_roots_here" parameter for Hdf5Parser
1190
+ # lookup value starting with / means rooted at the base of one of the extension_names parameter for Hdf5Parser
1191
1191
  # (integer) means return only the value with the index of "integer" from a list
1192
1192
  ob.add_attribute('Chunk.position.axis.function.refCoord.coord1.pix', '/header/wcs/crpix(0)')
1193
1193
 
1194
+ # lookup values starting with / and with "{}" in the path will cause the blueprint application to attempt to
1195
+ # guess the extension names from the file content
1196
+ ob.add_attribute('Chunk.position.axis.function.refCoord.coord1.pix', '/sitedata/site{}/header/wcs/crpix(0)')
1197
+
1194
1198
  # (integer:integer) means return only the value with the index of "integer" from a list, followed by "integer"
1195
1199
  # from the list in the list
1196
1200
  ob.add_attribute('Chunk.position.axis.function.cd11', '/header/wcs/cd(0:0)')
@@ -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
@@ -188,24 +188,19 @@ class StorageClientWrapper:
188
188
  self._logger.debug(f'Begin put for {uri} in {working_directory}')
189
189
  start = self._current()
190
190
  cwd = getcwd()
191
- archive, f_name = StorageClientWrapper._decompose(uri)
191
+ _, f_name = StorageClientWrapper._decompose(uri)
192
192
  fqn = path.join(working_directory, f_name)
193
193
  chdir(working_directory)
194
194
  try:
195
195
  local_meta = get_local_file_info(f_name)
196
196
  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
197
  self._logger.debug(
202
- f'uri {uri} src {fqn} replace {replace} file_type {local_meta.file_type} encoding {encoding} '
198
+ f'uri {uri} src {fqn} file_type {local_meta.file_type} encoding {encoding} '
203
199
  f'md5_checksum {local_meta.md5sum}'
204
200
  )
205
201
  self._cadc_client.cadcput(
206
202
  uri,
207
203
  src=f_name,
208
- replace=replace,
209
204
  file_type=local_meta.file_type,
210
205
  file_encoding=encoding,
211
206
  md5_checksum=local_meta.md5sum,
@@ -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,6 +1887,13 @@ 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.
@@ -1879,7 +1902,13 @@ class Hdf5Parser(ContentParser):
1879
1902
  # h5py is an extra in this package since most collections do not require it
1880
1903
  import h5py
1881
1904
 
1882
- individual, multi, attributes = self._extract_path_names_from_blueprint()
1905
+ individual, multi, attributes, candidate_extensions = self._extract_path_names_from_blueprint()
1906
+ if self._extension_names is None and len(candidate_extensions) > 0:
1907
+ self._find_extension_names(candidate_extensions)
1908
+ for index, _ in enumerate(self._extension_names):
1909
+ self._blueprint._extensions[index] = {}
1910
+ else:
1911
+ self._blueprint._extensions[0] = {}
1883
1912
  filtered_individual = [ii for ii in individual.keys() if '(' in ii]
1884
1913
 
1885
1914
  def _extract_from_item(name, object):
@@ -1890,16 +1919,8 @@ class Hdf5Parser(ContentParser):
1890
1919
  :param name: fully-qualified HDF5 path name
1891
1920
  :param object: what the HDF5 path name points to
1892
1921
  """
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.
1922
+ # If it's the Part/Chunk metadata, capture it to extensions.
1923
+ # Syntax of the keys described in Hdf5ObsBlueprint class.
1903
1924
  for part_index, part_name in enumerate(self._extension_names):
1904
1925
  if name.startswith(part_name) and isinstance(object, h5py.Dataset) and object.dtype.names is not None:
1905
1926
  for d_name in object.dtype.names:
@@ -1973,20 +1994,54 @@ class Hdf5Parser(ContentParser):
1973
1994
  are _CAOM2_ELEMENT strings.
1974
1995
  attributes - a dictionary of lists, keys reference expected content from the h5py.File().attrs data
1975
1996
  structure and its keys.
1997
+ extensions - a list of prefixes for identifying extensions
1976
1998
  """
1977
1999
  individual = defaultdict(list)
1978
2000
  multi = defaultdict(list)
1979
2001
  attributes = defaultdict(list)
2002
+ extensions = []
1980
2003
  for key, value in self._blueprint._plan.items():
1981
2004
  if ObsBlueprint.needs_lookup(value):
1982
2005
  for ii in value[0]:
1983
2006
  if ii.startswith('//'):
1984
2007
  individual[ii].append(key)
1985
2008
  elif ii.startswith('/'):
1986
- multi[ii].append(key)
2009
+ if '{}' in ii:
2010
+ bits = ii.split('{}')
2011
+ extensions.append(bits[0])
2012
+ multi[bits[1]].append(key)
2013
+ else:
2014
+ multi[ii].append(key)
1987
2015
  else:
1988
2016
  attributes[ii].append(key)
1989
- return individual, multi, attributes
2017
+
2018
+ return individual, multi, attributes, list(set(extensions))
2019
+
2020
+ def _find_extension_names(self, candidates):
2021
+ """ if the HDF5 file has a structure where-by more than one Chunk (the equivalent of a FITS HDU extension)
2022
+ is defined, try to guess that structure
2023
+ """
2024
+ candidate_extension_names = []
2025
+
2026
+ def _extract_extension_prefixes(name, object):
2027
+ """
2028
+ Function signature dictated by h5py visititems implementation. Executed for each dataset/group in an
2029
+ HDF5 file.
2030
+
2031
+ :param name: fully-qualified HDF5 path name
2032
+ :param object: what the HDF5 path name points to
2033
+ """
2034
+ for part_name in candidates:
2035
+ y = part_name.replace('/', '', 1)
2036
+ if name.startswith(y):
2037
+ x = name.split(y)[1].split('/')
2038
+ temp = f'{y}{x[0]}'
2039
+ candidate_extension_names.append(temp)
2040
+ self._extension_names = list(sorted(set(candidate_extension_names)))
2041
+
2042
+ self._file.visititems(_extract_extension_prefixes)
2043
+ msg = '\n'.join(ii for ii in self._extension_names)
2044
+ self.logger.info(f'Found extension_names:\n{msg}')
1990
2045
 
1991
2046
  def apply_blueprint(self):
1992
2047
  self.logger.debug('Begin apply_blueprint')
@@ -2015,6 +2070,7 @@ class Hdf5Parser(ContentParser):
2015
2070
  else:
2016
2071
  exts[extension][key] = self._execute_external_instance(value, key, extension)
2017
2072
 
2073
+ # apply overrides
2018
2074
  # blueprint already contains all the overrides, only need to make sure the overrides get applied to all the
2019
2075
  # extensions
2020
2076
  for extension in exts:
@@ -2032,6 +2088,7 @@ class Hdf5Parser(ContentParser):
2032
2088
  exts[extension][key] = value
2033
2089
  self.logger.debug(f'{key}: set to {value} in extension {extension}')
2034
2090
 
2091
+ # apply defaults
2035
2092
  # if no values have been set by file lookups, function execution, or applying overrides, apply defaults,
2036
2093
  # including to all extensions
2037
2094
  for key, value in plan.items():
@@ -2054,7 +2111,7 @@ class Hdf5Parser(ContentParser):
2054
2111
  return
2055
2112
 
2056
2113
  def augment_artifact(self, artifact):
2057
- for ii in range(0, self._get_num_parts()):
2114
+ for ii in range(self._extension_start_index, self._extension_end_index):
2058
2115
  # one WCS parser per Part/Chunk
2059
2116
  self._wcs_parsers[ii] = Hdf5WcsParser(self._blueprint, ii)
2060
2117
  super().augment_artifact(artifact)
@@ -257,7 +257,6 @@ def _check_put_result(client_mock):
257
257
  client_mock.assert_called_with(
258
258
  'cadc:TEST/test_file.fits',
259
259
  src='test_file.fits',
260
- replace=True,
261
260
  file_type='application/fits',
262
261
  file_encoding=None,
263
262
  md5_checksum='3c66ee2cb6e0c2cfb5cd6824d353dc11',
@@ -1412,6 +1412,23 @@ def test_generic_parser1():
1412
1412
  assert test_parser._blueprint._plan[test_key] == test_value, 'original value over-ridden'
1413
1413
 
1414
1414
 
1415
+ def test_generic_parser_imported_module_error_handling():
1416
+ # this test exercises the error handling code for executing functions defined by blueprints
1417
+ test_key = 'Plane.metaRelease'
1418
+ test_key_2 = 'Plane.dataRelease'
1419
+ test_value = '2013-10-10'
1420
+ test_blueprint = ObsBlueprint()
1421
+ test_blueprint.set(test_key, '2013-10-10')
1422
+ # pick __sizeof__ as an attribute that will fail to execute for any module
1423
+ test_blueprint.set(test_key_2, '__sizeof__()')
1424
+ test_parser = BlueprintParser()
1425
+ assert test_parser._blueprint._plan[test_key] == (['RELEASE', 'REL_DATE'], None), 'default value changed'
1426
+ test_parser.blueprint = test_blueprint
1427
+ assert test_parser._blueprint._plan[test_key] == test_value, 'original value over-ridden'
1428
+ test_result = test_parser._execute_external('__sizeof__(uri)', test_key_2, 0)
1429
+ assert test_result == '', 'wrong result'
1430
+
1431
+
1415
1432
  def test_get_external_headers():
1416
1433
  test_uri = 'http://localhost/obs23/collection/obsid-1'
1417
1434
  with patch('requests.Session.get') as session_get_mock:
@@ -0,0 +1 @@
1
+ version = '1.7.3'
@@ -644,8 +644,9 @@ class WcsParser:
644
644
  else:
645
645
  result = self._wcs.array_shape[for_axis - 1]
646
646
  if isinstance(result, tuple):
647
- # the blueprint is incompletely configured
648
- raise ValueError(f'Could not find axis length for axis {for_axis}')
647
+ raise ValueError(
648
+ f'Could not find axis length for axis {for_axis}. The blueprint is incompletely configured.'
649
+ )
649
650
  return _to_int(result)
650
651
 
651
652
  def _get_cd(self, x_index, y_index):
@@ -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.3
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
@@ -12,7 +12,7 @@ Classifier: Programming Language :: Python
12
12
  Classifier: Programming Language :: Python :: 3
13
13
  Requires-Python: >=3.8, <4
14
14
  License-File: LICENSE
15
- Requires-Dist: cadcdata>=2.0
15
+ Requires-Dist: cadcdata>=2.5.2
16
16
  Requires-Dist: caom2>=2.6
17
17
  Requires-Dist: astropy>=2.0
18
18
  Requires-Dist: spherical-geometry>=1.2.11
@@ -30,6 +30,7 @@ Dynamic: classifier
30
30
  Dynamic: description
31
31
  Dynamic: home-page
32
32
  Dynamic: license
33
+ Dynamic: license-file
33
34
  Dynamic: requires-python
34
35
  Dynamic: summary
35
36
 
@@ -1,4 +1,4 @@
1
- cadcdata>=2.0
1
+ cadcdata>=2.5.2
2
2
  caom2>=2.6
3
3
  astropy>=2.0
4
4
  spherical-geometry>=1.2.11
@@ -32,11 +32,11 @@ license = AGPLv3
32
32
  url = https://www.cadc-ccda.hia-iha.nrc-cnrc.gc.ca/caom2
33
33
  edit_on_github = False
34
34
  github_project = opencadc/caom2tools
35
- version = 1.7.2
35
+ version = 1.7.3
36
36
 
37
37
  [options]
38
38
  install_requires =
39
- cadcdata>=2.0
39
+ cadcdata>=2.5.2
40
40
  caom2>=2.6
41
41
  astropy>=2.0
42
42
  spherical-geometry>=1.2.11
@@ -1 +0,0 @@
1
- version = '1.7.2'
File without changes
File without changes
File without changes
File without changes