From e7eb31e5dd9ca2a342ae44b58096545c2b246c40 Mon Sep 17 00:00:00 2001 From: Adrian Damian Date: Mon, 27 Jan 2025 16:38:16 -0800 Subject: [PATCH 1/6] CAOM2.5 Observation successfully parsed. --- caom2/caom2/__init__.py | 1 + caom2/caom2/artifact.py | 71 +- caom2/caom2/caom_util.py | 35 +- caom2/caom2/checksum.py | 10 +- caom2/caom2/chunk.py | 63 +- caom2/caom2/common.py | 28 +- caom2/caom2/dali.py | 164 + caom2/caom2/data/CAOM-2.4.xsd | 12 +- caom2/caom2/data/CAOM-2.5.xsd | 816 +++ caom2/caom2/obs_reader_writer.py | 576 +- caom2/caom2/observation.py | 112 +- caom2/caom2/part.py | 7 +- caom2/caom2/plane.py | 290 +- caom2/caom2/shape.py | 136 +- caom2/caom2/tests/caom_test_instances.py | 91 +- .../data/CompleteCompositeCircle-CAOM-2.2.xml | 105 - .../CompleteCompositePolygon-CAOM-2.2.xml | 102 - .../data/CompleteSimpleCircle-CAOM-2.2.xml | 102 - .../data/CompleteSimplePolygon-CAOM-2.2.xml | 102 - .../data/MinimalCompositeCircle-CAOM-2.2.xml | 29 - .../data/MinimalCompositePolygon-CAOM-2.2.xml | 29 - .../data/MinimalSimpleCircle-CAOM-2.2.xml | 26 - .../data/MinimalSimplePolygon-CAOM-2.2.xml | 26 - .../tests/data/sample-derived-caom25.xml | 5469 +++++++++++++++++ caom2/caom2/tests/test_artifact.py | 18 +- caom2/caom2/tests/test_caom_util.py | 70 +- caom2/caom2/tests/test_checksum.py | 12 +- caom2/caom2/tests/test_chunk.py | 54 +- caom2/caom2/tests/test_common.py | 9 +- caom2/caom2/tests/test_dali.py | 110 + caom2/caom2/tests/test_diffs.py | 40 +- caom2/caom2/tests/test_obs_reader_writer.py | 136 +- caom2/caom2/tests/test_observation.py | 56 +- caom2/caom2/tests/test_part.py | 4 +- caom2/caom2/tests/test_plane.py | 75 +- caom2/caom2/tests/test_shape.py | 165 +- 36 files changed, 7666 insertions(+), 1485 deletions(-) create mode 100644 caom2/caom2/dali.py create mode 100644 caom2/caom2/data/CAOM-2.5.xsd delete mode 100644 caom2/caom2/tests/data/CompleteCompositeCircle-CAOM-2.2.xml delete mode 100644 caom2/caom2/tests/data/CompleteCompositePolygon-CAOM-2.2.xml delete mode 100644 caom2/caom2/tests/data/CompleteSimpleCircle-CAOM-2.2.xml delete mode 100644 caom2/caom2/tests/data/CompleteSimplePolygon-CAOM-2.2.xml delete mode 100644 caom2/caom2/tests/data/MinimalCompositeCircle-CAOM-2.2.xml delete mode 100644 caom2/caom2/tests/data/MinimalCompositePolygon-CAOM-2.2.xml delete mode 100644 caom2/caom2/tests/data/MinimalSimpleCircle-CAOM-2.2.xml delete mode 100644 caom2/caom2/tests/data/MinimalSimplePolygon-CAOM-2.2.xml create mode 100644 caom2/caom2/tests/data/sample-derived-caom25.xml create mode 100644 caom2/caom2/tests/test_dali.py diff --git a/caom2/caom2/__init__.py b/caom2/caom2/__init__.py index 5d459811..440cff43 100644 --- a/caom2/caom2/__init__.py +++ b/caom2/caom2/__init__.py @@ -86,6 +86,7 @@ from .checksum import * # noqa from .chunk import * # noqa from .common import * # noqa +from .dali import * # noqa from .diff import * # noqa from .obs_reader_writer import * # noqa from .observation import * # noqa diff --git a/caom2/caom2/artifact.py b/caom2/caom2/artifact.py index b09ea22e..e86bb903 100644 --- a/caom2/caom2/artifact.py +++ b/caom2/caom2/artifact.py @@ -2,7 +2,7 @@ # ****************** CANADIAN ASTRONOMY DATA CENTRE ******************* # ************* CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** # -# (c) 2022. (c) 2022. +# (c) 2025. (c) 2025. # Government of Canada Gouvernement du Canada # National Research Council Conseil national de recherches # Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 @@ -74,10 +74,10 @@ from urllib.parse import urlparse from . import caom_util -from .chunk import ProductType -from .common import AbstractCaomEntity +from .common import AbstractCaomEntity, CaomObject, compute_bucket from .common import ChecksumURI, OrderedEnum from .part import Part +from .chunk import DataLinkSemantics from datetime import datetime @@ -95,8 +95,30 @@ class ReleaseType(OrderedEnum): META = "meta" +class ArtifactDescription(CaomObject): + """ Short description with a URI reference for details""" + + def __init__(self, uri, description): + super().__init__() + try: + urlparse(uri) + except ValueError: + raise TypeError('Expected any IVOA URI for ArtifactDescription.uri, ' + 'received {}'.format(uri)) + self._uri = uri + self._description = description + + @property + def uri(self): + return self._uri + + @property + def description(self): + return self._description + + class Artifact(AbstractCaomEntity): - """Contains the meta data assocaited with a file. + """Contains the metadata associated with a file. - location of the file (uri) - the http content-type @@ -104,8 +126,8 @@ class Artifact(AbstractCaomEntity): As well as a pointer (parts) to content of the file. - eg: Artifact('ad:CFHT/1234567o') - where 'ad:CFHT/1234567o' is a uri that refernce the file... + eg: Artifact('cadc:CFHT/1234567o') + where 'cadc:CFHT/1234567o' is an uri that reference the file... """ @@ -118,17 +140,23 @@ def __init__(self, content_checksum=None, content_release=None, content_read_groups=None, - parts=None + parts=None, + description_id=None ): """ - Initialize a Artifact instance. + Initialize an Artifact instance. Arguments: uri of the artifact. eg: vos://cadc.nrc.ca!vospace/APASS/apass_north/proc/100605/n100605.fz ad:CFHT/123456p """ super(Artifact, self).__init__() - self.uri = uri + try: + urlparse(uri) + except ValueError: + raise TypeError('Expected URI for Artifact.uri, received {}'.format(uri)) + self._uri = uri + self._uri_bucket = compute_bucket(uri) self.product_type = product_type self.release_type = release_type self.content_type = content_type @@ -139,6 +167,7 @@ def __init__(self, if parts is None: parts = caom_util.TypedOrderedDict(Part, ) self.parts = parts + self.description_id = description_id def _key(self): return self.uri @@ -160,15 +189,9 @@ def uri(self): """ return self._uri - @uri.setter - def uri(self, value): - caom_util.type_check(value, str, 'uri') - uri = urlparse(value) - if not uri.scheme: - raise ValueError('URI without scheme: {}'.format(value)) - uri_str = uri.geturl() - caom_util.value_check(value, None, None, 'uri', override=uri_str) - self._uri = uri_str + @property + def uri_bucket(self): + return self._uri_bucket @property def product_type(self): @@ -183,7 +206,7 @@ def product_type(self): @product_type.setter def product_type(self, value): - caom_util.type_check(value, ProductType, "product_type", False) + caom_util.type_check(value, DataLinkSemantics, "product_type", False) self._product_type = value @property @@ -306,3 +329,13 @@ def parts(self, value): caom_util.type_check(value, caom_util.TypedOrderedDict, 'parts', override=False) self._parts = value + + @property + def description_id(self): + return self._description_id + + @description_id.setter + def description_id(self, value): + if value is not None: + caom_util.type_check(value, str, 'description_id') + self._description_id = value diff --git a/caom2/caom2/caom_util.py b/caom2/caom2/caom_util.py index 6595cf76..94ca60fc 100644 --- a/caom2/caom2/caom_util.py +++ b/caom2/caom2/caom_util.py @@ -2,7 +2,7 @@ # ****************** CANADIAN ASTRONOMY DATA CENTRE ******************* # ************* CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** # -# (c) 2016. (c) 2016. +# (c) 2025. (c) 2025. # Government of Canada Gouvernement du Canada # National Research Council Conseil national de recherches # Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 @@ -74,7 +74,6 @@ the first point of use could be implemented. This helps the data engineer get the correct meta data more quickly. """ - import sys import collections from datetime import datetime @@ -82,9 +81,11 @@ from urllib.parse import urlsplit from builtins import int, str as newstr +from . import dali + __all__ = ['TypedList', 'TypedSet', 'TypedOrderedDict', 'ClassProperty', - 'URISet'] + 'URISet', 'validate_uri'] # TODO both these are very bad, implement more sensibly IVOA_DATE_FORMAT = "%Y-%m-%dT%H:%M:%S.%f" @@ -235,8 +236,8 @@ def __repr__(self): def check(self, v): if not isinstance(v, self._oktypes): - raise TypeError("Wrong type in list. OK Types: {0}". - format(self._oktypes)) + raise TypeError("Wrong type ({0}) in list. OK Types: {1}". + format(type(v), self._oktypes)) def __len__(self): return len(self.list) @@ -328,6 +329,22 @@ def __contains__(self, item): return False +def validate_uri(uri, scheme=None): + """ + Validates a URI. If a scheme is provided, the URI must have that scheme. + :param uri: + :param scheme: + :raise TypeError: when uri not valid + """ + if uri: + tmp = urlsplit(uri) + if scheme and scheme != tmp.scheme: + raise TypeError("Invalid URI scheme: {}".format(uri)) + if tmp.geturl() == uri: + return + raise TypeError("Invalid URI: " + uri) + + class URISet(TypedSet): """ Class that customizes a TypedSet to check for URIs @@ -350,13 +367,7 @@ def check(self, v): :param v: value to check :return: """ - if v: - tmp = urlsplit(v) - if self.scheme and tmp.scheme != self.scheme: - raise TypeError("Invalid URI scheme: {}".format(v)) - if tmp.geturl() == v: - return - raise TypeError("Invalid URI: " + v) + validate_uri(v, self.scheme) class TypedOrderedDict(collections.OrderedDict): diff --git a/caom2/caom2/checksum.py b/caom2/caom2/checksum.py index c858bb82..78d96eef 100644 --- a/caom2/caom2/checksum.py +++ b/caom2/caom2/checksum.py @@ -81,6 +81,9 @@ from caom2.common import CaomObject, AbstractCaomEntity, ObservationURI from caom2.common import ChecksumURI from caom2.observation import Observation +from .obs_reader_writer import CAOM25_NAMESPACE, CAOM24_NAMESPACE, \ + CAOM23_NAMESPACE + with warnings.catch_warnings(): warnings.simplefilter('ignore') from aenum import Enum @@ -399,8 +402,13 @@ def checksum_diff(): mistmatches += _print_diff(plane[0], plane[1]) mistmatches += _print_diff(orig, actual) + ns = CAOM25_NAMESPACE + if reader.version == 24: + ns = CAOM24_NAMESPACE + if reader.version == 23: + ns = CAOM23_NAMESPACE if args.output: - writer = obs_reader_writer.ObservationWriter(validate=True) + writer = obs_reader_writer.ObservationWriter(validate=True, namespace=ns) writer.write(actual, args.output) print("Total: {} mistmatches".format(mistmatches)) diff --git a/caom2/caom2/chunk.py b/caom2/caom2/chunk.py index 43c67360..c2f9e152 100644 --- a/caom2/caom2/chunk.py +++ b/caom2/caom2/chunk.py @@ -2,7 +2,7 @@ # ****************** CANADIAN ASTRONOMY DATA CENTRE ******************* # ************* CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** # -# (c) 2024. (c) 2024. +# (c) 2025. (c) 2025. # Government of Canada Gouvernement du Canada # National Research Council Conseil national de recherches # Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 @@ -76,14 +76,19 @@ from caom2.caom_util import int_32 from . import caom_util from . import wcs -from .common import AbstractCaomEntity -from .common import CaomObject, OrderedEnum +from .common import AbstractCaomEntity, OrderedEnum, VocabularyTerm, \ + _DATA_LINK_VOCAB_NS, _CAOM_PRODUCT_TYPE_NS +from .common import CaomObject -class ProductType(OrderedEnum): +__all__ = ['Chunk', 'ObservableAxis', 'SpatialWCS', 'DataLinkSemantics', + 'SpectralWCS', 'TemporalWCS', 'PolarizationWCS', 'CustomWCS'] + + +class DataLinkSemantics(OrderedEnum): """ Subset of IVOA DataLink terms at: - https://www.ivoa.net/rdf/datalink/core/2022-01-27/datalink.html + https://www.ivoa.net/rdf/datalink/core/ THIS = "this" AUXILIARY = "auxiliary" @@ -102,23 +107,22 @@ class ProductType(OrderedEnum): WEIGHT = 'weight' """ - - THIS = "this" - - AUXILIARY = "auxiliary" - BIAS = 'bias' - CALIBRATION = 'calibration' - CODERIVED = 'coderived' - DARK = 'dark' - DOCUMENTATION = 'documentation' - ERROR = 'error' - FLAT = 'flat' - NOISE = 'noise' - PREVIEW = 'preview' - PREVIEW_IMAGE = 'preview-image' - PREVIEW_PLOT = 'preview-plot' - THUMBNAIL = 'thumbnail' - WEIGHT = 'weight' + THIS = VocabularyTerm(_DATA_LINK_VOCAB_NS, "this", True).get_value() + + AUXILIARY = VocabularyTerm(_DATA_LINK_VOCAB_NS, 'auxiliary', True).get_value() + BIAS = VocabularyTerm(_DATA_LINK_VOCAB_NS, 'bias', True).get_value() + CALIBRATION = VocabularyTerm(_DATA_LINK_VOCAB_NS, 'calibration', True).get_value() + CODERIVED = VocabularyTerm(_DATA_LINK_VOCAB_NS, 'coderived', True).get_value() + DARK = VocabularyTerm(_DATA_LINK_VOCAB_NS, 'dark', True).get_value() + DOCUMENTATION = VocabularyTerm(_DATA_LINK_VOCAB_NS, 'documentation', True).get_value() + ERROR = VocabularyTerm(_DATA_LINK_VOCAB_NS, 'error', True).get_value() + FLAT = VocabularyTerm(_DATA_LINK_VOCAB_NS, 'flat', True).get_value() + NOISE = VocabularyTerm(_DATA_LINK_VOCAB_NS, 'noise', True).get_value() + PREVIEW = VocabularyTerm(_DATA_LINK_VOCAB_NS, 'preview', True).get_value() + PREVIEW_IMAGE = VocabularyTerm(_DATA_LINK_VOCAB_NS, 'preview-image', True).get_value() + PREVIEW_PLOT = VocabularyTerm(_DATA_LINK_VOCAB_NS, 'preview-plot', True).get_value() + THUMBNAIL = VocabularyTerm(_DATA_LINK_VOCAB_NS, 'thumbnail', True).get_value() + WEIGHT = VocabularyTerm(_DATA_LINK_VOCAB_NS, 'weight', True).get_value() # DataLink terms explicitly not included # counterpart @@ -130,16 +134,13 @@ class ProductType(OrderedEnum): # progenitor # CAOM specific terms public - SCIENCE = 'science' # this + SCIENCE = VocabularyTerm(_CAOM_PRODUCT_TYPE_NS, 'science', True).get_value() # this # deprecated # INFO = 'info' # CATALOG = 'catalog' -__all__ = ['ProductType', 'Chunk', 'ObservableAxis', 'SpatialWCS', - 'SpectralWCS', 'TemporalWCS', 'PolarizationWCS', 'CustomWCS'] - class Chunk(AbstractCaomEntity): """A caom2.Chunk object. A chunk is a peice of file part. @@ -199,10 +200,10 @@ def __init__(self, product_type=None, def product_type(self): """A word that describes the content of the chunk. - eg. Chunk.product_type = ProductType.SCIENCE + eg. Chunk.product_type = DataLinkSemantics.SCIENCE Allowed values: - """ + str(list(ProductType)) + """ + """ + str(list(DataLinkSemantics)) + """ """ @@ -210,10 +211,10 @@ def product_type(self): @product_type.setter def product_type(self, value): - if isinstance(value, str) and value in ProductType.names(): + if isinstance(value, str) and value in DataLinkSemantics.names(): # be helpful - value = ProductType('value') - caom_util.type_check(value, ProductType, 'product_type') + value = DataLinkSemantics('value') + caom_util.type_check(value, DataLinkSemantics, 'product_type') self._product_type = value @property diff --git a/caom2/caom2/common.py b/caom2/caom2/common.py index e4174efe..979bbbf5 100644 --- a/caom2/caom2/common.py +++ b/caom2/caom2/common.py @@ -2,7 +2,7 @@ # ****************** CANADIAN ASTRONOMY DATA CENTRE ******************* # ************* CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** # -# (c) 2022. (c) 2022. +# (c) 2025. (c) 2025. # Government of Canada Gouvernement du Canada # National Research Council Conseil national de recherches # Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 @@ -65,7 +65,7 @@ # # *********************************************************************** # - +import hashlib import inspect import uuid from datetime import datetime @@ -82,13 +82,18 @@ __all__ = ['CaomObject', 'AbstractCaomEntity', 'ObservationURI', 'ChecksumURI', - 'VocabularyTerm'] - -_OBSCORE_VOCAB_NS = "http://www.ivoa.net/std/ObsCore" -_CAOM_VOCAB_NS = "http://www.opencadc.org/caom2/DataProductType" + 'VocabularyTerm', 'compute_bucket'] logger = logging.getLogger('caom2') +_DATA_LINK_VOCAB_NS = 'https://www.ivoa.net/rdf/datalink/core' +# CAOM2 vocabularies +_CAOM_DATA_PRODUCT_TYPE_NS = "http://www.opencadc.org/caom2/DataProductType" +_CAOM_PRODUCT_TYPE_NS = "http://www.opencadc.org/caom2/ProductType" +_CAOM_QUALITY_NS = "http://www.opencadc.org/caom2/Quality" +_CAOM_STATUS_NS = "http://www.opencadc.org/caom2/Status" +_CAOM_TARGET_TYPE_NS = "http://www.opencadc.org/caom2/TargetType" + def get_current_ivoa_time(): """Generate a datetime with 3 digit microsecond precision. @@ -101,6 +106,17 @@ def get_current_ivoa_time(): now.second, int(str(now.microsecond)[:-3] + '000')) +def compute_bucket(uri): + """ + Compute a bucket name from a URI as the first 3 characters of the MD5 hash + :param uri: uri to compute bucket for + :return: bucket name + """ + md5 = hashlib.sha1() + md5.update(uri.encode('utf-8')) + return md5.hexdigest()[:3] + + class OrderedEnum(Enum): """ Enums are in the order of their definition. diff --git a/caom2/caom2/dali.py b/caom2/caom2/dali.py new file mode 100644 index 00000000..667df815 --- /dev/null +++ b/caom2/caom2/dali.py @@ -0,0 +1,164 @@ +# *********************************************************************** +# ****************** CANADIAN ASTRONOMY DATA CENTRE ******************* +# ************* CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** +# +# (c) 2025. (c) 2025. +# Government of Canada Gouvernement du Canada +# National Research Council Conseil national de recherches +# Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 +# All rights reserved Tous droits réservés +# +# NRC disclaims any warranties, Le CNRC dénie toute garantie +# expressed, implied, or énoncée, implicite ou légale, +# statutory, of any kind with de quelque nature que ce +# respect to the software, soit, concernant le logiciel, +# including without limitation y compris sans restriction +# any warranty of merchantability toute garantie de valeur +# or fitness for a particular marchande ou de pertinence +# purpose. NRC shall not be pour un usage particulier. +# liable in any event for any Le CNRC ne pourra en aucun cas +# damages, whether direct or être tenu responsable de tout +# indirect, special or general, dommage, direct ou indirect, +# consequential or incidental, particulier ou général, +# arising from the use of the accessoire ou fortuit, résultant +# software. Neither the name de l'utilisation du logiciel. Ni +# of the National Research le nom du Conseil National de +# Council of Canada nor the Recherches du Canada ni les noms +# names of its contributors may de ses participants ne peuvent +# be used to endorse or promote être utilisés pour approuver ou +# products derived from this promouvoir les produits dérivés +# software without specific prior de ce logiciel sans autorisation +# written permission. préalable et particulière +# par écrit. +# +# This file is part of the Ce fichier fait partie du projet +# OpenCADC project. OpenCADC. +# +# OpenCADC is free software: OpenCADC est un logiciel libre ; +# you can redistribute it and/or vous pouvez le redistribuer ou le +# modify it under the terms of modifier suivant les termes de +# the GNU Affero General Public la “GNU Affero General Public +# License as published by the License” telle que publiée +# Free Software Foundation, par la Free Software Foundation +# either version 3 of the : soit la version 3 de cette +# License, or (at your option) licence, soit (à votre gré) +# any later version. toute version ultérieure. +# +# OpenCADC is distributed in the OpenCADC est distribué +# hope that it will be useful, dans l’espoir qu’il vous +# but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE +# without even the implied GARANTIE : sans même la garantie +# warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ +# or FITNESS FOR A PARTICULAR ni d’ADÉQUATION À UN OBJECTIF +# PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence +# General Public License for Générale Publique GNU Affero +# more details. pour plus de détails. +# +# You should have received Vous devriez avoir reçu une +# a copy of the GNU Affero copie de la Licence Générale +# General Public License along Publique GNU Affero avec +# with OpenCADC. If not, see OpenCADC ; si ce n’est +# . pas le cas, consultez : +# . +# +# $Revision: 4 $ +# +# *********************************************************************** +# + +from . import common +from . import caom_util + +__all__ = ['Interval'] + + +class Interval(common.CaomObject): + def __init__(self, lower, upper): + + super().__init__() + self.lower = lower + self.upper = upper + + def get_width(self): + return self._upper - self._lower + + @classmethod + def intersection(cls, i1, i2): + if i1.lower > i2.upper or i1.upper < i2.lower: + return None + + lb = max(i1.lower, i2.lower) + ub = min(i1.upper, i2.upper) + return cls(lb, ub) + + # Properties + + @property + def lower(self): + """ + type: float + """ + return self._lower + + @lower.setter + def lower(self, value): + caom_util.type_check(value, float, 'lower', override=False) + has_upper = True + try: + self._upper + except AttributeError: + has_upper = False + if has_upper and self._upper < value: + raise ValueError("Interval: attempt to set upper < lower " + "for {}, {}".format(self._upper, value)) + self._lower = value + + @property + def upper(self): + """ + type: float + """ + return self._upper + + @upper.setter + def upper(self, value): + caom_util.type_check(value, float, 'upper', override=False) + has_lower = True + try: + self._lower + except AttributeError: + has_lower = False + if has_lower and value < self._lower: + raise ValueError("Interval: attempt to set upper < lower " + "for {}, {}".format(value, self._lower)) + self._upper = value + + # def validate(self): + # """ + # Performs a validation of the current object. + # + # An AssertionError is thrown if the object does not represent an + # Interval + # """ + # if self._samples is not None: + # + # if len(self._samples) == 0: + # raise ValueError( + # 'invalid interval (samples cannot be empty)') + # + # prev = None + # for sample in self._samples: + # if sample.lower < self._lower: + # raise ValueError( + # 'invalid interval: sample extends below lower bound: ' + # '{} vs {}'.format(sample, self._lower)) + # if sample.upper > self._upper: + # raise ValueError( + # 'invalid interval: sample extends above upper bound: ' + # '{} vs {}'.format(sample, self._upper)) + # if prev is not None: + # if sample.lower <= prev.upper: + # raise ValueError( + # 'invalid interval: sample overlaps previous ' + # 'sample:\n{}\nvs\n{}'.format(sample, prev)) + # prev = sample diff --git a/caom2/caom2/data/CAOM-2.4.xsd b/caom2/caom2/data/CAOM-2.4.xsd index 84028aa8..35ba8113 100644 --- a/caom2/caom2/data/CAOM-2.4.xsd +++ b/caom2/caom2/data/CAOM-2.4.xsd @@ -446,7 +446,7 @@ - + @@ -481,7 +481,7 @@ - + @@ -500,7 +500,7 @@ - + @@ -541,7 +541,7 @@ - + @@ -764,6 +764,7 @@ + + + + + + + + + + + + + + + The metaRelease date is expected to be in IVOA date format: + yyyy-MM-dd'T'HH:mm:ss.SSS + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The metaRelease date is expected to be in IVOA date format: + yyyy-MM-dd'T'HH:mm:ss.SSS + + + + + + + + The dataRelease date is expected to be in IVOA date format: + yyyy-MM-dd'T'HH:mm:ss.SSS + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The lastExecuted date is expected to be in IVOA date format: + yyyy-MM-dd'T'HH:mm:ss.SSS + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The contentRelease date is expected to be in IVOA date format: + yyyy-MM-dd'T'HH:mm:ss.SSS + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/caom2/caom2/obs_reader_writer.py b/caom2/caom2/obs_reader_writer.py index ffb38aaa..cd600963 100644 --- a/caom2/caom2/obs_reader_writer.py +++ b/caom2/caom2/obs_reader_writer.py @@ -3,7 +3,7 @@ # ****************** CANADIAN ASTRONOMY DATA CENTRE ******************* # ************* CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** # -# (c) 2022. (c) 2022. +# (c) 2025. (c) 2025. # Government of Canada Gouvernement du Canada # National Research Council Conseil national de recherches # Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 @@ -77,9 +77,11 @@ from lxml import etree +import caom2 from . import artifact from . import caom_util from . import chunk +from . import dali from . import observation from . import part from . import plane @@ -93,20 +95,21 @@ CAOM22_SCHEMA_FILE = 'CAOM-2.2.xsd' CAOM23_SCHEMA_FILE = 'CAOM-2.3.xsd' CAOM24_SCHEMA_FILE = 'CAOM-2.4.xsd' +CAOM25_SCHEMA_FILE = 'CAOM-2.5.xsd' -CAOM22_NAMESPACE = 'vos://cadc.nrc.ca!vospace/CADC/xml/CAOM/v2.2' CAOM23_NAMESPACE = 'http://www.opencadc.org/caom2/xml/v2.3' CAOM24_NAMESPACE = 'http://www.opencadc.org/caom2/xml/v2.4' +CAOM25_NAMESPACE = 'http://www.opencadc.org/caom2/xml/v2.5' CAOM_VERSION = { - CAOM22_NAMESPACE: 22, CAOM23_NAMESPACE: 23, - CAOM24_NAMESPACE: 24 + CAOM24_NAMESPACE: 24, + CAOM25_NAMESPACE: 25 } -CAOM22 = "{%s}" % CAOM22_NAMESPACE CAOM23 = "{%s}" % CAOM23_NAMESPACE CAOM24 = "{%s}" % CAOM24_NAMESPACE +CAOM25 = "{%s}" % CAOM25_NAMESPACE XSI_NAMESPACE = "http://www.w3.org/2001/XMLSchema-instance" XSI = "{%s}" % XSI_NAMESPACE @@ -119,6 +122,45 @@ logger = logging.getLogger(__name__) +def _to_samples(vertices): + samples = [] + last_closed_point = None + points = [] + for vertex in vertices: + if vertex.type == shape.SegmentType.MOVE: + points.append(shape.Point(vertex.cval1, vertex.cval2)) + elif vertex.type == shape.SegmentType.CLOSE: + last_closed_point = shape.Point(vertex.cval1, vertex.cval2) + points.append(last_closed_point) + samples.append(shape.Polygon(points)) + points = [] # continue with a new polygon + else: + if not points: + # no move so start from the last closed point + points.append(last_closed_point) + points.append(shape.Point(vertex.cval1, vertex.cval2)) + return caom2.MultiShape(samples) + + +def _to_vertices(samples): + vertices = [] + for sample_shape in samples.shapes: + if isinstance(sample_shape, shape.Polygon): + vertices.append(shape.Vertex(sample_shape.points[0].cval1, + sample_shape.points[0].cval2, + shape.SegmentType.MOVE)) + for point in sample_shape.points[1:-1]: + vertices.append(shape.Vertex(point.cval1, point.cval2, + shape.SegmentType.LINE)) + vertices.append(shape.Vertex(sample_shape.points[-1].cval1, + sample_shape.points[-1].cval2, + shape.SegmentType.CLOSE)) + else: + raise ValueError("Only polygons can be converted to Vertices/MultiPolygon") + + return vertices + + class ObservationReader(object): """ObservationReader """ @@ -131,27 +173,23 @@ def __init__(self, validate=False): validate : If True enable schema validation, False otherwise """ self._validate = validate - if self._validate: - # caom20_schema_path = pkg_resources.resource_filename( - # DATA_PKG, CAOM20_SCHEMA_FILE) - caom22_schema_path = os.path.join(THIS_DIR + '/' + DATA_PKG, - CAOM22_SCHEMA_FILE) - + caom23_schema_path = os.path.join(THIS_DIR + '/' + DATA_PKG, + CAOM23_SCHEMA_FILE) parser = etree.XMLParser(remove_blank_text=True) - xsd = etree.parse(caom22_schema_path, parser) - - caom23_schema = etree.Element( - '{http://www.w3.org/2001/XMLSchema}import', - namespace=CAOM23_NAMESPACE, - schemaLocation=CAOM23_SCHEMA_FILE) - xsd.getroot().insert(1, caom23_schema) + xsd = etree.parse(caom23_schema_path, parser) caom24_schema = etree.Element( '{http://www.w3.org/2001/XMLSchema}import', namespace=CAOM24_NAMESPACE, schemaLocation=CAOM24_SCHEMA_FILE) - xsd.getroot().insert(2, caom24_schema) + xsd.getroot().insert(1, caom24_schema) + + caom25_schema = etree.Element( + '{http://www.w3.org/2001/XMLSchema}import', + namespace=CAOM25_NAMESPACE, + schemaLocation=CAOM25_SCHEMA_FILE) + xsd.getroot().insert(2, caom25_schema) self._xmlschema = etree.XMLSchema(xsd) self.version = None @@ -247,18 +285,12 @@ def _add_keywords(self, keywords_list, element, ns, required): :param ns: name space :param required: keywords sub-element required or not """ - if self.version < 23: - keywords = self._get_child_text("keywords", element, ns, required) - if keywords is not None: - for keyword in keywords.split(): - keywords_list.add(keyword) - else: - keywords_element = self._get_child_element("keywords", element, ns, - required) - if keywords_element is not None: - for keyword in keywords_element.iterchildren( - tag=("{" + ns + "}keyword")): - keywords_list.add(keyword.text) + keywords_element = self._get_child_element("keywords", element, ns, + required) + if keywords_element is not None: + for keyword in keywords_element.iterchildren( + tag=("{" + ns + "}keyword")): + keywords_list.add(keyword.text) def _get_algorithm(self, element_tag, parent, ns, required): """Build an Algorithm object from an XML representation @@ -343,6 +375,7 @@ def _get_proposal(self, element_tag, parent, ns, required): proposal.project = self._get_child_text("project", el, ns, False) proposal.title = self._get_child_text("title", el, ns, False) self._add_keywords(proposal.keywords, el, ns, False) + proposal.reference = self._get_child_text("reference", el, ns, False) return proposal def _get_target(self, element_tag, parent, ns, required): @@ -450,6 +483,7 @@ def _get_telescope(self, element_tag, parent, ns, required): telescope.geo_location_z = ( self._get_child_text_as_float("geoLocationZ", el, ns, False)) self._add_keywords(telescope.keywords, el, ns, False) + telescope.tracking_mode = self._get_child_text("trackingMode", el, ns, False) return telescope def _get_instrument(self, element_tag, parent, ns, required): @@ -538,8 +572,12 @@ def _add_inputs(self, inputs, parent, ns): """ el = self._get_child_element("inputs", parent, ns, False) if el is not None: - for uri_element in el.iterchildren("{" + ns + "}planeURI"): - inputs.add(plane.PlaneURI(str(uri_element.text))) + if self.version < 25: + for uri_element in el.iterchildren("{" + ns + "}planeURI"): + inputs.add(plane.PlaneURI(str(uri_element.text))) + else: + for uri_element in el.iterchildren("{" + ns + "}input"): + inputs.add(plane.PlaneURI(str(uri_element.text))) if not inputs: error = "No planeURI element found in members" @@ -1255,8 +1293,8 @@ def _get_position(self, element_tag, parent, ns, required): el = self._get_child_element(element_tag, parent, ns, required) if el is None: return None - pos = plane.Position() - pos.bounds = self._get_shape("bounds", el, ns, False) + bounds, samples = self._get_shape("bounds", el, ns, False) + pos = plane.Position(bounds=bounds, samples=samples) pos.dimension = self._get_dimension2d("dimension", el, ns, False) pos.resolution = self._get_child_text_as_float("resolution", el, ns, False) @@ -1264,8 +1302,10 @@ def _get_position(self, element_tag, parent, ns, required): ns, False) pos.sample_size = self._get_child_text_as_float("sampleSize", el, ns, False) - pos.time_dependent = self._get_child_text_as_boolean("timeDependent", - el, ns, False) + if self.version < 25: + pos.time_dependent = self._get_child_text_as_boolean("timeDependent", + el, ns, False) + pos.calibration = self._get_child_text("calibration", el, ns, False) return pos def _get_energy(self, element_tag, parent, ns, required): @@ -1284,14 +1324,21 @@ def _get_energy(self, element_tag, parent, ns, required): el = self._get_child_element(element_tag, parent, ns, required) if el is None: return None - energy = plane.Energy() - energy.bounds = self._get_interval("bounds", el, ns, False) + + bounds = self._get_interval("bounds", el, ns, True) + if self.version < 25: + samples = self._get_samples(self._get_child_element("bounds", el, ns, True), ns, True) + else: + samples = self._get_samples(el, ns, True) + energy = plane.Energy(bounds, samples) energy.dimension = \ self._get_child_text_as_int("dimension", el, ns, False) energy.resolving_power = self._get_child_text_as_float( "resolvingPower", el, ns, False) energy.resolving_power_bounds = self._get_interval( "resolvingPowerBounds", el, ns, False) + energy.resolution = self._get_child_text_as_float("resolution", el, ns, False) + energy.resolution_bounds = self._get_interval("resolutionBounds", el, ns, False) energy.sample_size = \ self._get_child_text_as_float("sampleSize", el, ns, False) energy.bandpass_name = \ @@ -1306,6 +1353,7 @@ def _get_energy(self, element_tag, parent, ns, required): transition = \ self._get_child_text("transition", _transition_el, ns, True) energy.transition = wcs.EnergyTransition(species, transition) + energy.calibration = self._get_child_text("calibration", el, ns, False) return energy @@ -1325,8 +1373,12 @@ def _get_time(self, element_tag, parent, ns, required): el = self._get_child_element(element_tag, parent, ns, required) if el is None: return None - time = plane.Time() - time.bounds = self._get_interval("bounds", el, ns, False) + bounds = self._get_interval("bounds", el, ns, False) + if self.version < 25: + samples = self._get_samples(self._get_child_element("bounds", el, ns, True), ns, True) + else: + samples = self._get_samples(el, ns, True) + time = plane.Time(bounds, samples) time.dimension = \ self._get_child_text_as_int("dimension", el, ns, False) time.resolution = \ @@ -1337,6 +1389,8 @@ def _get_time(self, element_tag, parent, ns, required): self._get_child_text_as_float("sampleSize", el, ns, False) time.exposure = \ self._get_child_text_as_float("exposure", el, ns, False) + time.exposure_bounds = self._get_interval("exposureBounds", el, ns, False) + time.calibration = self._get_child_text("calibration", el, ns, False) return time @@ -1357,8 +1411,12 @@ def _get_custom(self, element_tag, parent, ns, required): if el is None: return None ctype = self._get_child_text("ctype", el, ns, False) - custom = plane.CustomAxis(ctype) - custom.bounds = self._get_interval("bounds", el, ns, False) + bounds = self._get_interval("bounds", el, ns, False) + if self.version < 25: + samples = self._get_samples(self._get_child_element("bounds", el, ns, True), ns, True) + else: + samples = self._get_samples(el, ns, True) + custom = plane.CustomAxis(ctype, bounds, samples=samples) custom.dimension = \ self._get_child_text_as_int("dimension", el, ns, False) return custom @@ -1400,31 +1458,51 @@ def _get_shape(self, element_tag, parent, ns, required): return None shape_type = shape_element.get(XSI + "type") if "caom2:Polygon" == shape_type: - if self.version < 23: - raise TypeError( - ("Polygon element not supported for " - "CAOM releases prior to 2.3")) - points_element = self._get_child_element("points", shape_element, - ns, True) - points = list() - for point in points_element.iterchildren( - tag=("{" + ns + "}point")): - points.append(self._get_point(point, ns, True)) - samples_element = self._get_child_element("samples", shape_element, - ns, True) - vertices = list() - self._add_vertices(vertices, samples_element, ns) - return shape.Polygon(points=points, - samples=shape.MultiPolygon(vertices=vertices)) + polygon = self._get_polygon(ns, shape_element) + samples = None + if self.version < 25: + samples_element = self._get_child_element("samples", shape_element, + ns, True) + vertices = list() + self._add_vertices(vertices, samples_element, ns) + samples = _to_samples(vertices) + else: + samples = self._get_child_element("samples", parent, + ns, True) + sample_list = [] + for _shape in samples.iterchildren("{" + ns + "}shape"): + sample_type = _shape.get(XSI + "type") + if "caom2:Polygon" == sample_type: + sample_list.append(self._get_polygon(ns, _shape)) + elif "caom2:Circle" == sample_type: + sample_list.append(self._get_circle(ns, _shape)) + else: + raise TypeError("Unsupported sample type " + sample_type) + samples = shape.MultiShape(sample_list) + return polygon, samples elif "caom2:Circle" == shape_type: - center = self._get_child_element("center", shape_element, ns, True) - center_point = self._get_point(center, ns, True) - radius = self._get_child_text_as_float( - "radius", shape_element, ns, True) - return shape.Circle(center=center_point, radius=radius) + circle = self._get_circle(ns, shape_element) + return circle, shape.MultiShape([circle]) else: raise TypeError("Unsupported shape type " + shape_type) + def _get_circle(self, ns, shape_element): + center = self._get_child_element("center", shape_element, ns, True) + center_point = self._get_point(center, ns, True) + radius = self._get_child_text_as_float( + "radius", shape_element, ns, True) + circle = shape.Circle(center=center_point, radius=radius) + return circle + + def _get_polygon(self, ns, shape_element): + points_element = self._get_child_element("points", shape_element, + ns, True) + points = list() + for point in points_element.iterchildren( + tag=("{" + ns + "}point")): + points.append(self._get_point(point, ns, True)) + return shape.Polygon(points) + def _add_energy_bands(self, energy_bands, parent, ns): """Create EnergyBand objects from an XML representation of ObservationURI elements found in energy_band element, and add them to @@ -1472,20 +1550,21 @@ def _get_interval(self, element_tag, parent, ns, required): return None _lower = self._get_child_text_as_float("lower", _interval_el, ns, True) _upper = self._get_child_text_as_float("upper", _interval_el, ns, True) - _samples_el = self._get_child_element("samples", _interval_el, ns, - required) - _interval = shape.Interval(_lower, _upper) - if _samples_el is not None: - _samples = list() - for _sample_el in _samples_el.iterchildren("{" + ns + "}sample"): - _si_lower = self._get_child_text_as_float("lower", _sample_el, - ns, required) - _si_upper = self._get_child_text_as_float("upper", _sample_el, - ns, required) - _sub_interval = shape.SubInterval(_si_lower, _si_upper) - _samples.append(_sub_interval) - _interval.samples = _samples - return _interval + return dali.Interval(_lower, _upper) + + def _get_samples(self, parent, ns, required): + _samples_el = self._get_child_element("samples", parent, ns, required) + if _samples_el is None: + return None + _samples = [] + for _sample_el in _samples_el.iterchildren("{" + ns + "}sample"): + _si_lower = self._get_child_text_as_float("lower", _sample_el, + ns, required) + _si_upper = self._get_child_text_as_float("upper", _sample_el, + ns, required) + _sub_interval = dali.Interval(_si_lower, _si_upper) + _samples.append(_sub_interval) + return _samples def _add_chunks(self, chunks, parent, ns): """Build Chunk objects from an XML representation of Chunk elements @@ -1508,7 +1587,7 @@ def _add_chunks(self, chunks, parent, ns): False) if product_type: _chunk.product_type = \ - chunk.ProductType(product_type) + chunk.DataLinkSemantics(product_type) _chunk.naxis = \ self._get_child_text_as_int("naxis", chunk_element, ns, False) @@ -1574,7 +1653,7 @@ def _add_parts(self, parts, parent, ns): False) if product_type: _part.product_type = \ - chunk.ProductType(product_type) + chunk.DataLinkSemantics(product_type) self._add_chunks(_part.chunks, part_element, ns) self._set_entity_attributes(part_element, ns, _part) parts[_part.name] = _part @@ -1600,12 +1679,12 @@ def _add_artifacts(self, artifacts, parent, ns): artifact_element, ns, False) if product_type is None: - product_type = chunk.ProductType.SCIENCE + product_type = chunk.DataLinkSemantics.SCIENCE print( "Using default Artifact.productType value {0}".format( - str(chunk.ProductType.SCIENCE))) + str(chunk.DataLinkSemantics.SCIENCE))) else: - product_type = chunk.ProductType(product_type) + product_type = chunk.DataLinkSemantics(product_type) release_type = self._get_child_text("releaseType", artifact_element, ns, @@ -1619,6 +1698,15 @@ def _add_artifacts(self, artifacts, parent, ns): release_type = artifact.ReleaseType(release_type) _artifact = artifact.Artifact(uri, product_type, release_type) + if self.version >= 25: + sub = self._get_child_text("uriBucket", artifact_element, ns, True) + if sub != _artifact.uri_bucket: + raise ObservationParsingException( + "Parsed artifact URI bucket {} does not match calculated artifact URI bucket {}". + format(sub, _artifact.uri_bucket)) + _artifact.description_id = self._get_child_text("descriptionID", + artifact_element, + ns, False) cr = self._get_child_text("contentRelease", artifact_element, ns, False) _artifact.content_release = caom_util.str2ivoa(cr) @@ -1641,12 +1729,12 @@ def _add_artifacts(self, artifacts, parent, ns): self._set_entity_attributes(artifact_element, ns, _artifact) artifacts[_artifact.uri] = _artifact - def _add_planes(self, planes, parent, ns): + def _add_planes(self, obs, parent, ns): """Create Planes object from XML representation of Plane elements and add them to the set of Planes. Arguments: - planes : Set of planes from the parent Observation object + obs : Observation object containing the Planes parent : element containing the Plane elements ns : namespace of the document raise : ObservationParsingException @@ -1656,11 +1744,14 @@ def _add_planes(self, planes, parent, ns): return None else: for plane_element in el.iterchildren("{" + ns + "}plane"): - _plane = plane.Plane( - self._get_child_text("productID", plane_element, ns, True)) - _plane.meta_release = caom_util.str2ivoa( - self._get_child_text("metaRelease", plane_element, ns, - False)) + if self.version < 25: + _uri = plane.PlaneURI.get_plane_uri( + obs.uri, + self._get_child_text("productID", + plane_element, ns, True)) + else: + _uri = plane.PlaneURI(self._get_child_text("uri", plane_element, ns, True)) + _plane = plane.Plane(_uri.uri) _plane.data_release = caom_util.str2ivoa( self._get_child_text("dataRelease", plane_element, ns, False)) @@ -1674,17 +1765,7 @@ def _add_planes(self, planes, parent, ns): self._get_child_text("dataProductType", plane_element, ns, False) if data_product_type: - if (data_product_type == 'catalog') and \ - (self.version < 23): - # TODO backawards compatibility. To be removed when 2.2 - # and older version no longer supported - _plane.data_product_type = \ - plane.DataProductType( - '{}#{}'.format(plane._CAOM_VOCAB_NS, - data_product_type)) - else: - _plane.data_product_type = \ - plane.DataProductType(data_product_type) + _plane.data_product_type = plane.DataProductType(data_product_type) _plane.creator_id = \ self._get_child_text("creatorID", plane_element, ns, False) calibration_level = \ @@ -1713,7 +1794,7 @@ def _add_planes(self, planes, parent, ns): self._get_custom("custom", plane_element, ns, False) self._add_artifacts(_plane.artifacts, plane_element, ns) self._set_entity_attributes(plane_element, ns, _plane) - planes[_plane.product_id] = _plane + obs.planes[_plane.uri] = _plane def read(self, source): """Build an Observation object from an XML document located in source. @@ -1734,19 +1815,28 @@ def read(self, source): self.version = CAOM_VERSION[ns] collection = str( self._get_child_element("collection", root, ns, True).text) - observation_id = \ - str(self._get_child_element("observationID", root, ns, True).text) + if self.version < 25: + observation_id = \ + str(self._get_child_text("observationID", root, ns, True)) + uri = "caom:" + collection + "/" + observation_id + else: + uri = self._get_child_text("uri", root, ns, True) # Instantiate Algorithm algorithm = self._get_algorithm("algorithm", root, ns, True) # Instantiate Observation if root.get("{http://www.w3.org/2001/XMLSchema-instance}type") \ == "caom2:SimpleObservation": - obs = observation.SimpleObservation(collection, observation_id) + obs = observation.SimpleObservation(collection, uri) obs.algorithm = algorithm else: obs = \ - observation.DerivedObservation(collection, observation_id, - algorithm) + observation.DerivedObservation(collection, uri, algorithm) + if self.version >= 25: + sub = self._get_child_text("uriBucket", root, ns, True) + if sub != obs.uri_bucket: + raise ObservationParsingException( + "Parsed obs URI bucket {} does not match calculated obs URI bucket {}". + format(sub, obs.uri_bucket)) # Instantiate children of Observation obs.sequence_number = \ self._get_child_text_as_int("sequenceNumber", root, ns, False) @@ -1773,7 +1863,7 @@ def read(self, source): self._get_environment("environment", root, ns, False) obs.requirements = \ self._get_requirements("requirements", root, ns, False) - self._add_planes(obs.planes, root, ns) + self._add_planes(obs, root, ns) if isinstance(obs, observation.DerivedObservation): self._add_members(obs.members, root, ns) @@ -1799,28 +1889,28 @@ def __init__(self, validate=False, write_empty_collections=False, if namespace_prefix is None or not namespace_prefix: raise RuntimeError('null or empty namespace_prefix not allowed') - if namespace is None or namespace == CAOM24_NAMESPACE: + if namespace is None or namespace == CAOM25_NAMESPACE: + self._output_version = 25 + self._caom2_namespace = CAOM25 + self._namespace = CAOM25_NAMESPACE + elif namespace == CAOM24_NAMESPACE: self._output_version = 24 self._caom2_namespace = CAOM24 self._namespace = CAOM24_NAMESPACE - elif namespace is None or namespace == CAOM23_NAMESPACE: + elif namespace == CAOM23_NAMESPACE: self._output_version = 23 self._caom2_namespace = CAOM23 self._namespace = CAOM23_NAMESPACE - elif namespace == CAOM22_NAMESPACE: - self._output_version = 22 - self._caom2_namespace = CAOM22 - self._namespace = CAOM22_NAMESPACE else: raise RuntimeError('invalid namespace {}'.format(namespace)) if self._validate: - if self._output_version == 24: + if self._output_version == 25: + schema_file = CAOM25_SCHEMA_FILE + elif self._output_version == 24: schema_file = CAOM24_SCHEMA_FILE elif self._output_version == 23: schema_file = CAOM23_SCHEMA_FILE - else: - schema_file = CAOM22_SCHEMA_FILE schema_path = os.path.join(THIS_DIR + '/' + DATA_PKG, schema_file) # schema_path = pkg_resources.resource_filename( @@ -1851,7 +1941,12 @@ def write(self, obs, out): self._add_entity_attributes(obs, obs_element) self._add_element("collection", obs.collection, obs_element) - self._add_element("observationID", obs.observation_id, obs_element) + if self._output_version < 25: + observation_id = obs.uri.uri.split('/')[-1] + self._add_element("observationID", observation_id, obs_element) + else: + self._add_element("uri", obs.uri.uri, obs_element) + self._add_element('uriBucket', obs.uri_bucket, obs_element) self._add_datetime_element("metaRelease", obs.meta_release, obs_element) if self._output_version < 24 and obs.meta_read_groups: @@ -1901,17 +1996,16 @@ def _add_entity_attributes(self, entity, element): "lastModified", caom_util.date2ivoa(entity._last_modified), element) - if self._output_version >= 23: - if entity._max_last_modified is not None: - self._add_attribute( - "maxLastModified", - caom_util.date2ivoa(entity._max_last_modified), element) - if entity._meta_checksum is not None: - self._add_attribute( - "metaChecksum", entity._meta_checksum.uri, element) - if entity._acc_meta_checksum is not None: - self._add_attribute( - "accMetaChecksum", entity._acc_meta_checksum.uri, element) + if entity._max_last_modified is not None: + self._add_attribute( + "maxLastModified", + caom_util.date2ivoa(entity._max_last_modified), element) + if entity._meta_checksum is not None: + self._add_attribute( + "metaChecksum", entity._meta_checksum.uri, element) + if entity._acc_meta_checksum is not None: + self._add_attribute( + "accMetaChecksum", entity._acc_meta_checksum.uri, element) if self._output_version >= 24: if entity._meta_producer is not None: @@ -1934,6 +2028,7 @@ def _add_proposal_element(self, proposal, parent): self._add_element("pi", proposal.pi_name, element) self._add_element("project", proposal.project, element) self._add_element("title", proposal.title, element) + self._add_element("reference", proposal.reference, element) self._add_keywords_element(proposal.keywords, element) def _add_target_element(self, target, parent): @@ -1984,6 +2079,7 @@ def _add_telescope_element(self, telescope, parent): self._add_element("geoLocationX", telescope.geo_location_x, element) self._add_element("geoLocationY", telescope.geo_location_y, element) self._add_element("geoLocationZ", telescope.geo_location_z, element) + self._add_element("trackingMode", telescope.tracking_mode, element) self._add_keywords_element(telescope.keywords, element) def _add_instrument_element(self, instrument, parent): @@ -2038,13 +2134,14 @@ def _add_planes_element(self, planes, parent): for _plane in planes.values(): plane_element = self._get_caom_element("plane", element) self._add_entity_attributes(_plane, plane_element) - self._add_element("productID", _plane.product_id, plane_element) - if _plane.creator_id is not None: - if self._output_version >= 23: - self._add_element("creatorID", _plane.creator_id, - plane_element) - else: - raise AttributeError('creatorID only available in CAOM2.3') + if self._output_version < 25: + _comp = _plane.uri.uri.split('/') + if len(_comp) != 3: + raise ValueError("Attempt to write CAOM2.4 but can't deduce " + "Plane.productID in Plane.uri=" + _plane.uri) + self._add_element("productID", _comp[-1], plane_element) + else: + self._add_element("uri", _plane.uri.uri, plane_element) self._add_datetime_element("metaRelease", _plane.meta_release, plane_element) if self._output_version < 24 and _plane.meta_read_groups: @@ -2062,19 +2159,9 @@ def _add_planes_element(self, planes, parent): self._add_groups_element("dataReadGroups", _plane.data_read_groups, plane_element) if _plane.data_product_type is not None: - if self._output_version < 23: - dpt = urlparse(_plane.data_product_type.value) - if dpt.fragment != '': - dpt = dpt.fragment - else: - dpt = _plane.data_product_type.value - self._add_element("dataProductType", - dpt, - plane_element) - else: - self._add_element("dataProductType", - _plane.data_product_type.value, - plane_element) + self._add_element("dataProductType", + _plane.data_product_type.value, + plane_element) if _plane.calibration_level is not None: self._add_element("calibrationLevel", _plane.calibration_level.value, @@ -2102,7 +2189,7 @@ def _add_position_element(self, position, parent): return element = self._get_caom_element("position", parent) - self._add_shape_element("bounds", position.bounds, element) + self._add_bounds_and_samples(position, element) self._add_dimension2d_element("dimension", position.dimension, element) self._add_element("resolution", position.resolution, element) if self._output_version < 24: @@ -2114,14 +2201,33 @@ def _add_position_element(self, position, parent): self._add_interval_element("resolutionBounds", position.resolution_bounds, element) self._add_element("sampleSize", position.sample_size, element) - self._add_boolean_element("timeDependent", position.time_dependent, - element) + if position.time_dependent is not None: + if self._output_version < 25: + self._add_boolean_element("timeDependent", position.time_dependent, + element) + else: + raise AttributeError( + "Attempt to write CAOM2.5 element that contains " + "deprecated Position.timeDependent attribute") + if position.calibration: + if self._output_version < 25: + raise AttributeError( + "Attempt to write CAOM2.5 element (position.calibration) as " + "{} Observation".format(self._output_version)) + else: + self._add_element("calibration", position.calibration, element) def _add_energy_element(self, energy, parent): if energy is None: return element = self._get_caom_element("energy", parent) - self._add_interval_element("bounds", energy.bounds, element) + bounds_elem = self._add_interval_element("bounds", energy.bounds, element) + if self._output_version < 25: + # samples element is within bounds element + self._add_samples_element(energy.samples, bounds_elem) + else: + self._add_samples_element(energy.samples, element) + self._add_element("dimension", energy.dimension, element) self._add_element("resolvingPower", energy.resolving_power, element) if energy.resolving_power_bounds is not None: @@ -2134,6 +2240,22 @@ def _add_energy_element(self, energy, parent): else: self._add_interval_element("resolvingPowerBounds", energy.resolving_power_bounds, element) + if energy.resolution is not None: + if self._output_version < 25: + raise AttributeError( + "Attempt to write CAOM2.5 element (energy.resolution) as " + "{} Observation".format(self._output_version)) + else: + self._add_element("resolution", energy.resolution, element) + if energy.resolution_bounds is not None: + if self._output_version < 25: + raise AttributeError( + "Attempt to write CAOM2.5 element (energy.resolution_bounds) as " + "{} Observation".format(self._output_version)) + else: + self._add_interval_element("resolutionBounds", + energy.resolution_bounds, element) + self._add_element("sampleSize", energy.sample_size, element) self._add_element("bandpassName", energy.bandpass_name, element) if energy.energy_bands: @@ -2157,12 +2279,24 @@ def _add_energy_element(self, energy, parent): self._add_element("species", energy.transition.species, transition) self._add_element("transition", energy.transition.transition, transition) + if energy.calibration: + if self._output_version < 25: + raise AttributeError( + "Attempt to write CAOM2.5 element (energy.calibration) as " + "{} Observation".format(self._output_version)) + else: + self._add_element("calibration", energy.calibration, element) def _add_time_element(self, time, parent): if time is None: return element = self._get_caom_element("time", parent) - self._add_interval_element("bounds", time.bounds, element) + bounds_elem = self._add_interval_element("bounds", time.bounds, element) + if self._output_version < 25: + # samples element is within bounds element + self._add_samples_element(time.samples, bounds_elem) + else: + self._add_samples_element(time.samples, element) self._add_element("dimension", time.dimension, element) self._add_element("resolution", time.resolution, element) if self._output_version < 24: @@ -2175,13 +2309,34 @@ def _add_time_element(self, time, parent): time.resolution_bounds, element) self._add_element("sampleSize", time.sample_size, element) self._add_element("exposure", time.exposure, element) + if time.exposure_bounds is not None: + if self._output_version < 25: + if len(time.exposure_bounds) > 1: + raise AttributeError( + "Attempt to write CAOM2.5 element " + "(time.exposure_bounds) as {} Observation".format(self._output_version)) + else: + self.add_interval_element("exposureBounds", + time.exposure_bounds, element) + if time.calibration: + if self._output_version < 25: + raise AttributeError( + "Attempt to write CAOM2.5 element (time.calibration) as {} " + "Observation".format(self._output_version)) + else: + self._add_element("calibration", time.calibration, element) def _add_custom_element(self, custom, parent): if custom is None: return element = self._get_caom_element("custom", parent) self._add_element("ctype", custom.ctype, element) - self._add_interval_element("bounds", custom.bounds, element) + bounds_elem = self._add_interval_element("bounds", custom.bounds, element) + if self._output_version < 25: + # samples element is within bounds element + self._add_samples_element(custom.samples, bounds_elem) + else: + self._add_samples_element(custom.samples, element) self._add_element("dimension", custom.dimension, element) def _add_polarization_element(self, polarization, parent): @@ -2194,36 +2349,54 @@ def _add_polarization_element(self, polarization, parent): self._add_element("state", _state.value, _pstates_el) self._add_element("dimension", polarization.dimension, element) - def _add_shape_element(self, name, the_shape, parent): - if the_shape is None: + def _add_bounds_and_samples(self, position, parent): + if position is None: return - if self._output_version < 23: - raise TypeError( - 'Polygon shape not supported in CAOM2 previous to v2.3') - if isinstance(the_shape, shape.Polygon): + shape_element = self._add_shape_element("bounds", parent, position.bounds) + if self._output_version < 25: + if isinstance(position.bounds,shape.Circle): + # samples not supported for circles so need to make sure that + # samples is the same with bounds + if len(position.samples.shapes) > 1 or position.samples.shapes[0] != position.bounds: + raise AttributeError( + "Cannot write a CAOM25 circle bounds position as CAOM{} " + "Observation".format(self._output_version)) + else: + pass # samples is not defined with circles + else: + samples_element = self._get_caom_element("samples", shape_element) + vertices_element = self._get_caom_element("vertices", + samples_element) + vertices = _to_vertices(position.samples) + for vertex in vertices: + vertex_element = self._get_caom_element("vertex", + vertices_element) + self._add_element("cval1", vertex.cval1, vertex_element) + self._add_element("cval2", vertex.cval2, vertex_element) + self._add_element("type", vertex.type.value, vertex_element) + else: + samples_element = self._get_caom_element("samples", parent=parent) + for samples_shape in position.samples.shapes: + self._add_shape_element("shape", samples_element, samples_shape) + + def _add_shape_element(self, name, parent, elem_shape): + if isinstance(elem_shape, shape.Polygon): shape_element = self._get_caom_element(name, parent) shape_element.set(XSI + "type", "caom2:Polygon") points_element = self._get_caom_element("points", shape_element) - for point in the_shape.points: + for point in elem_shape.points: self._add_point_element("point", point, points_element) - samples_element = self._get_caom_element("samples", shape_element) - vertices_element = self._get_caom_element("vertices", - samples_element) - for vertex in the_shape.samples.vertices: - vertex_element = self._get_caom_element("vertex", - vertices_element) - self._add_element("cval1", vertex.cval1, vertex_element) - self._add_element("cval2", vertex.cval2, vertex_element) - self._add_element("type", vertex.type.value, vertex_element) - elif isinstance(the_shape, shape.Circle): + elif isinstance(elem_shape, shape.Circle): + shape_element = self._get_caom_element(name, parent) shape_element.set(XSI + "type", "caom2:Circle") - self._add_point_element("center", the_shape.center, shape_element) - self._add_element("radius", the_shape.radius, shape_element) + self._add_point_element("center", elem_shape.center, + shape_element) + self._add_element("radius", elem_shape.radius, shape_element) else: - raise TypeError("Unsupported shape type " - + the_shape.__class__.__name__) + raise TypeError("Unsupported shape type " + elem_shape.__class__.__name__) + return shape_element def _add_interval_element(self, name, interval, parent): if interval is None: @@ -2232,14 +2405,18 @@ def _add_interval_element(self, name, interval, parent): _interval_element = self._get_caom_element(name, parent) self._add_element("lower", interval.lower, _interval_element) self._add_element("upper", interval.upper, _interval_element) - if interval.samples: - _samples_element = self._get_caom_element("samples", - _interval_element) - for _sample in interval.samples: - _sample_element = self._get_caom_element("sample", - _samples_element) - self._add_element("lower", _sample.lower, _sample_element) - self._add_element("upper", _sample.upper, _sample_element) + return _interval_element + + def _add_samples_element(self, samples, parent): + if not samples: + raise AttributeError("non empty samples attribute is required") + + _samples_element = self._get_caom_element("samples", parent) + for _sample in samples: + _sample_element = self._get_caom_element("sample", + _samples_element) + self._add_element("lower", _sample.lower, _sample_element) + self._add_element("upper", _sample.upper, _sample_element) def _add_provenance_element(self, provenance, parent): if provenance is None: @@ -2297,6 +2474,8 @@ def _add_artifacts_element(self, artifacts, parent): artifact_element = self._get_caom_element("artifact", element) self._add_entity_attributes(_artifact, artifact_element) self._add_element("uri", _artifact.uri, artifact_element) + if self._output_version >= 25: + self._add_element("uriBucket", _artifact.uri_bucket, artifact_element) self._add_element("productType", _artifact.product_type.value, artifact_element) self._add_element("releaseType", _artifact.release_type.value, @@ -2319,11 +2498,18 @@ def _add_artifacts_element(self, artifacts, parent): artifact_element) self._add_element("contentLength", _artifact.content_length, artifact_element) - if self._output_version > 22: - if _artifact.content_checksum: - self._add_element("contentChecksum", - _artifact.content_checksum.uri, - artifact_element) + if _artifact.content_checksum: + self._add_element("contentChecksum", + _artifact.content_checksum.uri, + artifact_element) + if _artifact.description_id is not None: + if self._output_version < 25: + raise AttributeError( + "Attempt to write CAOM2.5 element " + "(artifact.description_id) as CAOM{} Observation".format(self._output_version)) + else: + self._add_element("descriptionID", + _artifact.description_id, artifact_element) self._add_parts_element(_artifact.parts, artifact_element) def _add_parts_element(self, parts, parent): @@ -2695,11 +2881,8 @@ def _add_keywords_element(self, collection, parent): (len(collection) == 0 and not self._write_empty_collections): return element = self._get_caom_element("keywords", parent) - if self._output_version < 23: - element.text = ' '.join(collection) - else: - for keyword in collection: - self._get_caom_element("keyword", element).text = keyword + for keyword in collection: + self._get_caom_element("keyword", element).text = keyword def _add_coord_range_1d_list_element(self, name, values, parent): if values is None: @@ -2714,7 +2897,10 @@ def _add_inputs_element(self, name, collection, parent): return element = self._get_caom_element(name, parent) for plane_uri in collection: - self._add_element("planeURI", plane_uri.uri, element) + if self._output_version < 25: + self._add_element("planeURI", plane_uri.uri, element) + else: + self._add_element("input", plane_uri.uri, element) def _get_caom_element(self, tag, parent): return etree.SubElement(parent, self._caom2_namespace + tag) diff --git a/caom2/caom2/observation.py b/caom2/caom2/observation.py index 7d5d8245..615aaff9 100644 --- a/caom2/caom2/observation.py +++ b/caom2/caom2/observation.py @@ -2,7 +2,7 @@ # ****************** CANADIAN ASTRONOMY DATA CENTRE ******************* # ************* CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** # -# (c) 2022. (c) 2022. +# (c) 2025. (c) 2025. # Government of Canada Gouvernement du Canada # National Research Council Conseil national de recherches # Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 @@ -75,13 +75,14 @@ from deprecated import deprecated from . import caom_util -from .caom_util import int_32 +from .caom_util import int_32, validate_uri from .common import AbstractCaomEntity, CaomObject, ObservationURI, \ - VocabularyTerm, OrderedEnum -from .common import _CAOM_VOCAB_NS + VocabularyTerm, OrderedEnum, compute_bucket +from .common import _CAOM_DATA_PRODUCT_TYPE_NS from .plane import Plane from .shape import Point -from urllib.parse import urlsplit +from urllib.parse import urlsplit, urlparse + with warnings.catch_warnings(): warnings.simplefilter('ignore') from aenum import Enum @@ -106,7 +107,7 @@ class Status(Enum): """ FAIL: "fail" """ - FAIL = VocabularyTerm(_CAOM_VOCAB_NS, "fail", True).get_value() + FAIL = VocabularyTerm(_CAOM_DATA_PRODUCT_TYPE_NS, "fail", True).get_value() class TargetType(Enum): @@ -114,8 +115,16 @@ class TargetType(Enum): FIELD: "field", OBJECT: "object" """ - FIELD = VocabularyTerm(_CAOM_VOCAB_NS, "field", True).get_value() - OBJECT = VocabularyTerm(_CAOM_VOCAB_NS, "object", True).get_value() + FIELD = VocabularyTerm(_CAOM_DATA_PRODUCT_TYPE_NS, "field", True).get_value() + OBJECT = VocabularyTerm(_CAOM_DATA_PRODUCT_TYPE_NS, "object", True).get_value() + + +class Tracking(Enum): + """ + FIELD: "field", + OBJECT: "object" + """ + FIELD = VocabularyTerm(_CAOM_DATA_PRODUCT_TYPE_NS, "sidereal", True).get_value() class Observation(AbstractCaomEntity): @@ -168,7 +177,7 @@ class Observation(AbstractCaomEntity): def __init__(self, collection, - observation_id, + uri, algorithm, sequence_number=None, intent=None, @@ -190,8 +199,7 @@ def __init__(self, Arguments: collection : where the observation is from (eg. 'HST') - observation_id : a unique identifier within that collection - (eg. '111') + uri : a unique logical identifier for the observation (eg. 'caom:HST/123456abc') algorithm : the algorithm used to create the observation. For a telescope observation this is always 'exposure' @@ -199,14 +207,13 @@ def __init__(self, super(Observation, self).__init__() self.collection = collection - self.observation_id = observation_id + validate_uri(uri) + self._uri = ObservationURI(uri) + self._uri_bucket = compute_bucket(uri) if not algorithm: raise AttributeError('Algorithm required') self.algorithm = algorithm - self._uri = ObservationURI.get_observation_uri(collection, - observation_id) - self.sequence_number = sequence_number self.intent = intent self.type = type @@ -237,27 +244,17 @@ def collection(self, value): self._collection = value @property - def observation_id(self): - """A string that uniquely identifies this obseravtion within the given - collection. + def uri(self): + """A unique identifier for the observation. - type: unicode string - """ - return self._observation_id - - @observation_id.setter - def observation_id(self, value): - caom_util.type_check(value, str, 'observation_id', override=False) - self._observation_id = value - - def get_uri(self): - """A URI for this observation referenced in the caom system. - - This attribute is auto geneqrated from the other metadata. type: unicode string """ return self._uri + @property + def uri_bucket(self): + return self._uri_bucket + @property def planes(self): """A typed ordered dictionary containing plane objects associated with @@ -541,7 +538,7 @@ class SimpleObservation(Observation): def __init__(self, collection, - observation_id, + uri, algorithm=_DEFAULT_ALGORITHM_NAME, sequence_number=None, intent=None, @@ -560,10 +557,10 @@ def __init__(self, collection - A name that describes a collection of data, nominally the name of a telescope - observation_id - A UNIQUE identifier with in that collection + uri - A unique identifier for the observation """ super(SimpleObservation, self).__init__(collection, - observation_id, + uri, algorithm, sequence_number, intent, @@ -610,7 +607,7 @@ class DerivedObservation(Observation): def __init__(self, collection, - observation_id, + uri, algorithm, sequence_number=None, intent=None, @@ -626,7 +623,7 @@ def __init__(self, target_position=None): super(DerivedObservation, self).__init__( collection=collection, - observation_id=observation_id, + uri=uri, algorithm=algorithm, sequence_number=sequence_number, intent=intent, @@ -670,7 +667,7 @@ class CompositeObservation(DerivedObservation): def __init__(self, collection, - observation_id, + uri, algorithm, sequence_number=None, intent=None, @@ -685,7 +682,7 @@ def __init__(self, target_position=None): super(CompositeObservation, self).__init__( collection=collection, - observation_id=observation_id, + uri=uri, algorithm=algorithm, sequence_number=sequence_number, intent=intent, @@ -884,7 +881,8 @@ def __init__(self, id, pi_name=None, project=None, - title=None): + title=None, + reference=None): """ Initializes a Proposal instance @@ -896,8 +894,8 @@ def __init__(self, self.pi_name = pi_name self.project = project self.title = title - self.keywords = set() + self.reference = reference # Properties @@ -972,6 +970,19 @@ def title(self, value): caom_util.type_check(value, str, 'title') self._title = value + @property + def reference(self): + return self._reference + + @reference.setter + def reference(self, value): + caom_util.type_check(value, str, 'reference') + if value is not None: + tmp = urlsplit(value) + if tmp.geturl() != value: + raise ValueError("Invalid URI: " + value) + self._reference = value + class Requirements(CaomObject): """ Requirements """ @@ -1189,7 +1200,8 @@ def __init__(self, name, geo_location_x=None, geo_location_y=None, geo_location_z=None, - keywords=None + keywords=None, + tracking_mode=None ): """ Initializes a Telescope instance @@ -1207,9 +1219,27 @@ def __init__(self, name, if keywords is None: keywords = set() self.keywords = keywords + self.tracking_mode = tracking_mode # Properties + @property + def tracking_mode(self): + """A keyword indicating how the telescope moves during data aquisition. + must be from the list + """ + str(list(Tracking)) + """ + type: Tracking + + """ + return self._tracking_mode + + @tracking_mode.setter + def tracking_mode(self, value): + if isinstance(value, str): + value = Tracking(value) + caom_util.type_check(value, Tracking, "tracking_mode") + self._tracking_mode = value + @property def name(self): """a name for this facility. diff --git a/caom2/caom2/part.py b/caom2/caom2/part.py index 49979083..9bc90fe0 100644 --- a/caom2/caom2/part.py +++ b/caom2/caom2/part.py @@ -2,7 +2,7 @@ # ****************** CANADIAN ASTRONOMY DATA CENTRE ******************* # ************* CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** # -# (c) 2022. (c) 2022. +# (c) 2025. (c) 2025. # Government of Canada Gouvernement du Canada # National Research Council Conseil national de recherches # Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 @@ -72,8 +72,7 @@ from builtins import str from . import caom_util -from .chunk import Chunk -from .chunk import ProductType +from .chunk import Chunk, DataLinkSemantics from .common import AbstractCaomEntity __all__ = ['Part'] @@ -117,7 +116,7 @@ def product_type(self): @product_type.setter def product_type(self, value): - caom_util.type_check(value, ProductType, "product_type") + caom_util.type_check(value, DataLinkSemantics, "product_type") self._product_type = value @property diff --git a/caom2/caom2/plane.py b/caom2/caom2/plane.py index c47af0e2..1b7d1ade 100644 --- a/caom2/caom2/plane.py +++ b/caom2/caom2/plane.py @@ -2,7 +2,7 @@ # ****************** CANADIAN ASTRONOMY DATA CENTRE ******************* # ************* CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** # -# (c) 2022. (c) 2022. +# (c) 2025. (c) 2025. # Government of Canada Gouvernement du Canada # National Research Council Conseil national de recherches # Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 @@ -71,19 +71,18 @@ from datetime import datetime from builtins import str, int -from urllib.parse import SplitResult, urlsplit +from urllib.parse import SplitResult, urlsplit, urlparse from deprecated import deprecated -from caom2.caom_util import int_32 +from caom2.caom_util import int_32, validate_uri from . import caom_util from . import shape from . import wcs from .artifact import Artifact from .common import AbstractCaomEntity, CaomObject, ObservationURI,\ VocabularyTerm, OrderedEnum -from .common import _CAOM_VOCAB_NS, _OBSCORE_VOCAB_NS +from .common import _CAOM_DATA_PRODUCT_TYPE_NS import warnings -from enum import Enum with warnings.catch_warnings(): warnings.simplefilter('ignore') from aenum import Enum, extend_enum @@ -110,23 +109,41 @@ class CalibrationLevel(Enum): ANALYSIS_PRODUCT = int_32(4) +class Ucd(OrderedEnum): + """ UCD - enum of UCDs""" + UCD_VOCAB = "https://ivoa.net/documents/UCD1+/20230125/ucd-list.txt" + + # TODO no values yet + + +class CalibrationStatus(OrderedEnum): + """ CalibrationStatus - enum of calibration status""" + CALIB_STATUS_VOCAB = "http://www.opencadc.org/caom2/CalibrationStatus" + + ABSOLUTE = VocabularyTerm(CALIB_STATUS_VOCAB, "absolute", True).get_value() + NORMALIZED = VocabularyTerm(CALIB_STATUS_VOCAB, "normalized", True).get_value() + RELATIVE = VocabularyTerm(CALIB_STATUS_VOCAB, "relative", True).get_value() + + class DataProductType(OrderedEnum): """ DataproductType - enum of data product types""" + DATA_PRODUCT_TYPE_VOCAB = "http://www.ivoa.net/rdf/product-type" - IMAGE = VocabularyTerm(_OBSCORE_VOCAB_NS, "image", True).get_value() - CUBE = VocabularyTerm(_OBSCORE_VOCAB_NS, "cube", True).get_value() - EVENTLIST = VocabularyTerm(_OBSCORE_VOCAB_NS, "eventlist", + IMAGE = VocabularyTerm(DATA_PRODUCT_TYPE_VOCAB, "image", True).get_value() + CUBE = VocabularyTerm(DATA_PRODUCT_TYPE_VOCAB, "cube", True).get_value() + EVENTLIST = VocabularyTerm(DATA_PRODUCT_TYPE_VOCAB, "eventlist", True).get_value() - SPECTRUM = VocabularyTerm(_OBSCORE_VOCAB_NS, "spectrum", True).get_value() - TIMESERIES = VocabularyTerm(_OBSCORE_VOCAB_NS, "timeseries", + SPECTRUM = VocabularyTerm(DATA_PRODUCT_TYPE_VOCAB, "spectrum", True).get_value() + TIMESERIES = VocabularyTerm(DATA_PRODUCT_TYPE_VOCAB, "timeseries", True).get_value() - VISIBILITY = VocabularyTerm(_OBSCORE_VOCAB_NS, "visibility", + VISIBILITY = VocabularyTerm(DATA_PRODUCT_TYPE_VOCAB, "visibility", True).get_value() - MEASUREMENTS = VocabularyTerm(_OBSCORE_VOCAB_NS, "measurements", + MEASUREMENTS = VocabularyTerm(DATA_PRODUCT_TYPE_VOCAB, "measurements", True).get_value() - CATALOG = VocabularyTerm(_CAOM_VOCAB_NS, "catalog").get_value() - EVENT = VocabularyTerm(_CAOM_VOCAB_NS, "event", True).get_value() - SED = VocabularyTerm(_CAOM_VOCAB_NS, "sed", True).get_value() + EVENT = VocabularyTerm(DATA_PRODUCT_TYPE_VOCAB, "event-list", True).get_value() + SED = VocabularyTerm(DATA_PRODUCT_TYPE_VOCAB, "sed", True).get_value() + + CATALOG = VocabularyTerm(_CAOM_DATA_PRODUCT_TYPE_NS, "catalog").get_value() @staticmethod def extend(namespace, name): @@ -205,14 +222,15 @@ class Quality(Enum): """ JUNK: junk """ - JUNK = VocabularyTerm(_CAOM_VOCAB_NS, "junk", True).get_value() + JUNK = VocabularyTerm(_CAOM_DATA_PRODUCT_TYPE_NS, "junk", True).get_value() class Observable(): """ Observable class""" - def __init__(self, ucd): + def __init__(self, ucd, calibration=None): self.ucd = ucd + self.calibration = calibration @property def ucd(self): @@ -220,15 +238,27 @@ def ucd(self): @ucd.setter def ucd(self, value): - caom_util.type_check(value, str, 'ucd', override=False) + caom_util.type_check(value, Ucd, 'ucd', override=False) self._ucd = value + @property + def calibration(self): + return self._calibration + + @calibration.setter + def calibration(self, value): + if value is not None: + caom_util.type_check(value, CalibrationStatus, 'calibration', override=False) + self._calibration = CalibrationStatus(value) + else: + self._calibration = None + class Plane(AbstractCaomEntity): """ Plane class """ - def __init__(self, product_id, - creator_id=None, + def __init__(self, uri, + creator_id=None, # deprecated since 2.5 artifacts=None, meta_release=None, data_release=None, @@ -244,13 +274,14 @@ def __init__(self, product_id, Initialize a Plane instance Arguments: - product_id : product ID + uri : product URI """ super(Plane, self).__init__() - self.product_id = product_id + validate_uri(uri) + self._uri = PlaneURI(uri) + self.creator_id = creator_id if artifacts is None: artifacts = caom_util.TypedOrderedDict(Artifact, ) - self.creator_id = creator_id self.artifacts = artifacts self.meta_release = meta_release @@ -273,28 +304,23 @@ def __init__(self, product_id, self.observable = observable def _key(self): - return self.product_id + return self.uri def __hash__(self): return hash(self._key()) # Properties @property - def product_id(self): - """A string that identifies the data product, within a given + def uri(self): + """A URI that identifies the data product, within a given observation, that is stored in this plane. eg: '1234567p' type: unicode string """ - return self._product_id - - @product_id.setter - def product_id(self, value): - caom_util.type_check(value, str, 'product_id', override=False) - self._product_id = value + return self._uri - @property + @ property def creator_id(self): """A URI that identifies the creator of this plane. @@ -303,11 +329,12 @@ def creator_id(self): """ return self._creator_id - @creator_id.setter + @ creator_id.setter def creator_id(self, value): caom_util.type_check(value, str, 'creator_id') if value is not None: tmp = urlsplit(value) + if tmp.geturl() != value: raise ValueError("Invalid URI: " + value) self._creator_id = value @@ -591,9 +618,9 @@ def compute_polarization(self): "has not been implemented in this module") +#TODO not sure this is needed anymore class PlaneURI(CaomObject): """ Plane URI """ - def __init__(self, uri): """ Initializes an Plane instance @@ -640,7 +667,7 @@ def get_plane_uri(cls, observation_uri, product_id): caom_util.type_check(observation_uri, ObservationURI, "observation_uri", override=False) - caom_util.type_check(product_id, str, "observation_uri", + caom_util.type_check(product_id, str, "product_id", override=False) caom_util.validate_path_component(cls, "product_id", product_id) @@ -677,15 +704,6 @@ def uri(self, value): ObservationURI.get_observation_uri(collection, observation_id) self._uri = value - def get_product_id(self): - """return the product_id associated with this plane""" - return self._product_id - - def get_observation_uri(self): - """Return the uri that can be used to find the caom2 observation object that - this plane belongs to""" - return self._observation_uri - class DataQuality(CaomObject): """ DataQuality """ @@ -939,12 +957,14 @@ def inputs(self): class Position(CaomObject): """ Position """ - def __init__(self, bounds=None, + def __init__(self, bounds, + samples, dimension=None, resolution=None, resolution_bounds=None, sample_size=None, - time_dependent=None + time_dependent=None, # deprecated since 2.5 + calibration=None ): """ Initialize a Position instance. @@ -952,12 +972,23 @@ def __init__(self, bounds=None, Arguments: None """ - self.bounds = bounds + if not bounds: + raise ValueError("No bounds provided") + caom_util.type_check(bounds, + (shape.Box, shape.Circle, shape.Polygon), + 'bounds', override=False) + self._bounds = bounds + if not samples: + # TODO - not sure whether to do this or create default samples from bounds + raise ValueError("No samples provided") + caom_util.type_check(samples, shape.MultiShape, 'samples') + self._samples = samples self.dimension = dimension self.resolution = resolution self.resolution_bounds = resolution_bounds self.sample_size = sample_size self.time_dependent = time_dependent + self.calibration = calibration # Properties @@ -966,13 +997,10 @@ def bounds(self): """ Bounds """ return self._bounds - @bounds.setter - def bounds(self, value): - if value is not None: - caom_util.type_check(value, - (shape.Box, shape.Circle, shape.Polygon), - 'bounds', override=False) - self._bounds = value + @property + def samples(self): + """ Samples """ + return self._samples @property def dimension(self): @@ -1030,14 +1058,26 @@ def time_dependent(self, value): caom_util.type_check(value, bool, 'time_dependent') self._time_dependent = value + @property + def calibration(self): + return self._calibration + + @calibration.setter + def calibration(self, value): + if value is not None: + caom_util.type_check(value, str, 'calibration', override=False) + self._calibration = CalibrationStatus(value) + else: + self._calibration = None + class Energy(CaomObject): """ Energy """ - def __init__(self, bounds=None, dimension=None, resolving_power=None, - resolving_power_bounds=None, energy_bands=None, - sample_size=None, bandpass_name=None, em_band=None, - transition=None, restwav=None): + def __init__(self, bounds, samples, dimension=None, resolving_power=None, + resolving_power_bounds=None, resolution=None, resolution_bounds=None, + energy_bands=None, sample_size=None, bandpass_name=None, em_band=None, + transition=None, restwav=None, calibration=None): """ Initialize an Energy instance. @@ -1045,9 +1085,12 @@ def __init__(self, bounds=None, dimension=None, resolving_power=None, None """ self.bounds = bounds + self.samples = samples self.dimension = dimension self.resolving_power = resolving_power self.resolving_power_bounds = resolving_power_bounds + self.resolution = resolution + self.resolution_bounds = resolution_bounds self.sample_size = sample_size self.bandpass_name = bandpass_name self.energy_bands = energy_bands @@ -1056,6 +1099,7 @@ def __init__(self, bounds=None, dimension=None, resolving_power=None, self.energy_bands.add(em_band) self.transition = transition self.restwav = restwav + self.calibration = calibration # Properties @@ -1070,6 +1114,25 @@ def bounds(self, value): caom_util.type_check(value, shape.Interval, 'bounds') self._bounds = value + @property + def samples(self): + return self._samples + + @samples.setter + def samples(self, value): + """ + value is a List of intervals + """ + if value is None: + raise AttributeError('samples in Energy cannot be None') + else: + caom_util.type_check(value, list,'samples') + if len(value) == 0: + raise ValueError('samples in Energy cannot be empty') + # TODO - could check that the intervals are within the bounds? + self._samples = value + + @property def dimension(self): """DIMENSION (NUMBER OF PIXELS) ALONG ENERGY AXIS.""" @@ -1104,6 +1167,26 @@ def resolving_power_bounds(self, value): 'resolving power bounds') self._resolving_power_bounds = value + @property + def resolution(self): + return self._resolution + + @resolution.setter + def resolution(self, value): + if value is not None: + caom_util.type_check(value, float, 'resolution') + self._resolution = value + + @property + def resolution_bounds(self): + return self._resolution_bounds + + @resolution_bounds.setter + def resolution_bounds(self, value): + if value is not None: + caom_util.type_check(value, shape.Interval, 'resolution bounds') + self._resolution_bounds = value + @property def sample_size(self): """ Sample size """ @@ -1177,6 +1260,18 @@ def restwav(self, value): caom_util.type_check(value, float, 'restwav') self._restwav = value + @property + def calibration(self): + return self._calibration + + @calibration.setter + def calibration(self, value): + if value is not None: + caom_util.type_check(value, str, 'calibration', override=False) + self._calibration = CalibrationStatus(value) + else: + self._calibration = None + class Polarization(CaomObject): """ Polarization """ @@ -1228,12 +1323,15 @@ class Time(CaomObject): """ Time """ def __init__(self, - bounds=None, + bounds, + samples, dimension=None, resolution=None, resolution_bounds=None, sample_size=None, - exposure=None): + exposure=None, + exposure_bounds=None, + calibration=None): """ Initialize a Time instance. @@ -1241,11 +1339,14 @@ def __init__(self, None """ self.bounds = bounds + self.samples = samples self.dimension = dimension self.resolution = resolution self.resolution_bounds = resolution_bounds self.sample_size = sample_size self.exposure = exposure + self.exposure_bounds = exposure_bounds + self.calibration = calibration # Properties @@ -1267,6 +1368,26 @@ def bounds(self, value): caom_util.type_check(value, shape.Interval, 'bounds') self._bounds = value + @property + def samples(self): + return self._samples + + @samples.setter + def samples(self, value): + """ + value is a List of intervals + """ + if value is None: + raise AttributeError('samples in Time cannot be None') + else: + caom_util.type_check(value, list, + 'samples') + if len(value) == 0: + raise ValueError('samples in Time cannot be empty') + # TODO - could check that the intervals are within the bounds? + self._samples = value + + @property def dimension(self): """Number of pixel in the time direction, normally 1. @@ -1335,6 +1456,29 @@ def exposure(self, value): caom_util.type_check(value, float, 'exposure') self._exposure = value + @property + def exposure_bounds(self): + """ Exposure bounds""" + return self._exposure_bounds + + @exposure_bounds.setter + def exposure_bounds(self, value): + if value is not None: + caom_util.type_check(value, shape.Interval, 'exposure bounds') + self._exposure_bounds = value + + @property + def calibration(self): + return self._calibration + + @calibration.setter + def calibration(self, value): + if value is not None: + caom_util.type_check(value, str, 'calibration', override=False) + self._calibration = CalibrationStatus(value) + else: + self._calibration = None + class CustomAxis(CaomObject): """ @@ -1344,7 +1488,8 @@ class CustomAxis(CaomObject): def __init__(self, ctype, - bounds=None, + bounds, + samples, dimension=None): """ Initialize a Custom Axis instance. @@ -1352,8 +1497,11 @@ def __init__(self, if ctype is None: raise AttributeError('ctype of CustomAxis cannot be None') self._ctype = ctype + if bounds is None: + raise AttributeError('bounds of CustomAxis cannot be None') self.bounds = bounds self.dimension = dimension + self.samples = samples # Properties @@ -1384,3 +1532,21 @@ def dimension(self): def dimension(self, value): caom_util.type_check(value, int, 'dimension') self._dimension = value + + @property + def samples(self): + return self._samples + + @samples.setter + def samples(self, value): + """ + value is a List of intervals + """ + if value is None: + raise AttributeError('samples in CustomAxis cannot be None') + else: + caom_util.type_check(value, list, 'samples') + if len(value) == 0: + raise ValueError('samples in CustomAxis cannot be empty') + # TODO - could check that the intervals are within the bounds? + self._samples = value diff --git a/caom2/caom2/shape.py b/caom2/caom2/shape.py index 2c4ee304..06a2853b 100644 --- a/caom2/caom2/shape.py +++ b/caom2/caom2/shape.py @@ -70,13 +70,14 @@ from caom2.caom_util import int_32 from . import caom_util from . import common +from . import dali import warnings with warnings.catch_warnings(): warnings.simplefilter('ignore') from aenum import Enum __all__ = ['SegmentType', 'Box', 'Circle', 'Interval', 'Point', - 'Polygon', 'Vertex', 'MultiPolygon'] + 'Polygon', 'Vertex', 'MultiShape'] class SegmentType(Enum): @@ -84,6 +85,8 @@ class SegmentType(Enum): CLOSE: 0 LINE: 1 MOVE: 2 + + Deprecated: starting CAOM2.5 Vertex and SegmentType are deprecated """ CLOSE = int_32(0) LINE = int_32(1) @@ -241,110 +244,7 @@ def upper(self, value): self._upper = value -class Interval(common.CaomObject): - def __init__(self, lower, upper, samples=None): - - self.lower = lower - self.upper = upper - self.samples = samples - self.validate() - - def get_width(self): - return self._upper - self._lower - - @classmethod - def intersection(cls, i1, i2): - if i1.lower > i2.upper or i1.upper < i2.lower: - return None - - lb = max(i1.lower, i2.lower) - ub = min(i1.upper, i2.upper) - return cls(lb, ub) - - # Properties - - @property - def lower(self): - """ - type: float - """ - return self._lower - - @lower.setter - def lower(self, value): - caom_util.type_check(value, float, 'lower', override=False) - has_upper = True - try: - self._upper - except AttributeError: - has_upper = False - if has_upper and self._upper < value: - raise ValueError("Interval: attempt to set upper < lower " - "for {}, {}".format(self._upper, value)) - self._lower = value - - @property - def upper(self): - """ - type: float - """ - return self._upper - - @upper.setter - def upper(self, value): - caom_util.type_check(value, float, 'upper', override=False) - has_lower = True - try: - self._lower - except AttributeError: - has_lower = False - if has_lower and value < self._lower: - raise ValueError("Interval: attempt to set upper < lower " - "for {}, {}".format(value, self._lower)) - self._upper = value - - @property - def samples(self): - """ - type: list - """ - return self._samples - - @samples.setter - def samples(self, value): - if value is not None: - caom_util.type_check(value, list, 'samples', override=False) - self._samples = value - - def validate(self): - """ - Performs a validation of the current object. - - An AssertionError is thrown if the object does not represent an - Interval - """ - if self._samples is not None: - - if len(self._samples) == 0: - raise ValueError( - 'invalid interval (samples cannot be empty)') - - prev = None - for sample in self._samples: - if sample.lower < self._lower: - raise ValueError( - 'invalid interval: sample extends below lower bound: ' - '{} vs {}'.format(sample, self._lower)) - if sample.upper > self._upper: - raise ValueError( - 'invalid interval: sample extends above upper bound: ' - '{} vs {}'.format(sample, self._upper)) - if prev is not None: - if sample.lower <= prev.upper: - raise ValueError( - 'invalid interval: sample overlaps previous ' - 'sample:\n{}\nvs\n{}'.format(sample, prev)) - prev = sample +Interval = dali.Interval # Moved to dali class Point(common.CaomObject): @@ -404,29 +304,35 @@ def samples(self): @samples.setter def samples(self, value): if value is not None: - caom_util.type_check(value, MultiPolygon, 'multipolygon', + caom_util.type_check(value, MultiShape, 'multipolygon', override=False) self._samples = value -class MultiPolygon(common.CaomObject): - def __init__(self, vertices=None): - if vertices is None: - self._vertices = [] - else: - self._vertices = vertices +class MultiShape(common.CaomObject): + def __init__(self, shapes): + """ + :param shapes: list of shapes + """ + super().__init__() + if not shapes: + raise ValueError("MultiShape: shapes must be non-empty") + self._shapes = shapes # Properties @property - def vertices(self): + def shapes(self): """ - type: list of Vertices + type: list of shapes """ - return self._vertices + return self._shapes class Vertex(Point): + """ + Deprecated: starting CAOM2.5 Vertex and SegmentType are deprecated + """ def __init__(self, cval1, cval2, type): super(Vertex, self).__init__(cval1, cval2) self.type = type diff --git a/caom2/caom2/tests/caom_test_instances.py b/caom2/caom2/tests/caom_test_instances.py index 61acf90f..f74c9ed3 100644 --- a/caom2/caom2/tests/caom_test_instances.py +++ b/caom2/caom2/tests/caom_test_instances.py @@ -2,7 +2,7 @@ # ****************** CANADIAN ASTRONOMY DATA CENTRE ******************* # ************* CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** # -# (c) 2022. (c) 2022. +# (c) 2025. (c) 2025. # Government of Canada Gouvernement du Canada # National Research Council Conseil national de recherches # Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 @@ -73,10 +73,11 @@ import uuid from builtins import int -from caom2 import artifact +from caom2 import artifact, MultiShape from caom2 import caom_util from caom2 import chunk from caom2 import common +from caom2 import dali from caom2 import observation from caom2 import part from caom2 import plane @@ -105,7 +106,7 @@ def get_64bit_uuid(id): class Caom2TestInstances(object): _collection = "collection" - _observation_id = "observationID" + _uri = "caom:collection/observationID" _product_id = "productId" _keywords = {"keyword1", "keyword2"} _ivoa_date = datetime(2012, 7, 11, 13, 26, 37, 0) @@ -151,7 +152,7 @@ def caom_version(self, v): def get_simple_observation(self, short_uuid=False): simple_observation = \ observation.SimpleObservation(Caom2TestInstances._collection, - Caom2TestInstances._observation_id) + Caom2TestInstances._uri) if self.complete: simple_observation.sequence_number = int(5) simple_observation.obs_type = "flat" @@ -187,7 +188,7 @@ def get_composite_observation(self, short_uuid=False): composite_observation = \ observation.CompositeObservation( Caom2TestInstances._collection, - Caom2TestInstances._observation_id, + Caom2TestInstances._uri, self.get_algorithm()) print("Creating test composite observation of version " + str( self.caom_version)) @@ -221,7 +222,7 @@ def get_derived_observation(self, short_uuid=False): derived_observation = \ observation.DerivedObservation( Caom2TestInstances._collection, - Caom2TestInstances._observation_id, + Caom2TestInstances._uri, self.get_algorithm()) print("Creating test composite observation of version " + str( self.caom_version)) @@ -324,8 +325,8 @@ def get_planes(self): shapes = ['polygon'] for s in shapes: - prod_id = "productID{}".format(s) - _plane = plane.Plane(prod_id) + plane_uri = "{}/{}".format(self._uri, s) + _plane = plane.Plane(plane_uri) if self.complete: _plane.meta_release = Caom2TestInstances._ivoa_date _plane.data_release = Caom2TestInstances._ivoa_date @@ -358,67 +359,50 @@ def get_planes(self): if self.depth > 2: for k, v in self.get_artifacts().items(): _plane.artifacts[k] = v - planes[prod_id] = _plane + planes[_plane.uri] = _plane return planes def get_poly_position(self): - position = plane.Position() - - if self.caom_version >= 23: - v0 = shape.Vertex(0.0, 0.0, shape.SegmentType.MOVE) - v1 = shape.Vertex(3.0, 4.0, shape.SegmentType.LINE) - v2 = shape.Vertex(2.0, 3.0, shape.SegmentType.LINE) - v3 = shape.Vertex(1.0, 2.0, shape.SegmentType.LINE) - v4 = shape.Vertex(0.0, 0.0, shape.SegmentType.CLOSE) - vl = [v0, v1, v2, v3, v4] - - samples = shape.MultiPolygon(vertices=vl) - - p1 = shape.Point(0.0, 0.0) - p2 = shape.Point(3.0, 4.0) - p3 = shape.Point(2.0, 3.0) - p4 = shape.Point(1.0, 2.0) - p = [p1, p2, p3, p4] - polygon = shape.Polygon(points=p, samples=samples) - - position.bounds = polygon + p1 = shape.Point(0.0, 0.0) + p2 = shape.Point(3.0, 4.0) + p3 = shape.Point(2.0, 3.0) + p4 = shape.Point(1.0, 2.0) + p = [p1, p2, p3, p4] + polygon = shape.Polygon(points=p) + position = plane.Position(polygon, MultiShape([polygon])) position.dimension = wcs.Dimension2D(10, 20) position.resolution = 0.5 position.sample_size = 1.1 position.time_dependent = False if self.caom_version >= 24: - position.resolution_bounds = shape.Interval(1.0, 2.0) + position.resolution_bounds = dali.Interval(1.0, 2.0) return position def get_circle_position(self): - position = plane.Position() - position.bounds = shape.Circle(shape.Point(1.1, 2.2), 3.0) + circle = shape.Circle(shape.Point(1.1, 2.2), 3.0) + position = plane.Position(circle, MultiShape([circle])) position.dimension = wcs.Dimension2D(10, 20) position.resolution = 0.5 position.sample_size = 1.1 position.time_dependent = False if self.caom_version >= 24: - position.resolution_bounds = shape.Interval(1.0, 2.0) + position.resolution_bounds = dali.Interval(1.0, 2.0) return position def get_energy(self): - energy = plane.Energy() - lower = 1.0 upper = 2.0 lower1 = 1.1 upper1 = 2.1 lower2 = 1.2 upper2 = 2.2 - samples = [shape.SubInterval(lower, lower1), - shape.SubInterval(lower2, upper), - shape.SubInterval(upper1, upper2)] - - interval = shape.Interval(lower, upper2, samples) + samples = [dali.Interval(lower, lower1), + dali.Interval(lower2, upper), + dali.Interval(upper1, upper2)] + energy = plane.Energy(dali.Interval(lower, upper2), samples) - energy.bounds = interval energy.dimension = 100 energy.resolving_power = 2.0 energy.sample_size = 1.1 @@ -431,23 +415,19 @@ def get_energy(self): return energy def get_time(self): - time = plane.Time() - lower = 1.0 upper = 2.0 lower1 = 1.1 upper1 = 2.1 lower2 = 1.2 upper2 = 2.2 - samples = [shape.SubInterval(lower, lower1), - shape.SubInterval(lower2, upper), - shape.SubInterval(upper1, upper2)] - - interval = shape.Interval(lower, upper2, samples) + samples = [dali.Interval(lower, lower1), + dali.Interval(lower2, upper), + dali.Interval(upper1, upper2)] + time = plane.Time(dali.Interval(lower, upper2), samples) - time.bounds = interval if self.caom_version >= 24: - time.resolution_bounds = shape.Interval(22.2, 33.3) + time.resolution_bounds = dali.Interval(22.2, 33.3) time.dimension = 1 time.resolution = 2.1 time.sample_size = 3.0 @@ -456,8 +436,9 @@ def get_time(self): return time def get_custom(self): - custom = plane.CustomAxis('MyAxis') - custom.bounds = shape.Interval(2.2, 3.3) + bounds = dali.Interval(2.2, 3.3) + samples = [bounds] + custom = plane.CustomAxis('MyAxis', bounds=bounds, samples=samples) custom.dimension = 1 def get_polarization(self): @@ -504,7 +485,7 @@ def get_quality(self): def get_artifacts(self): artifacts = collections.OrderedDict() _artifact = artifact.Artifact("ad:foo/bar1", - chunk.ProductType.SCIENCE, + chunk.DataLinkSemantics.SCIENCE, artifact.ReleaseType.META) if self.complete: _artifact.content_type = "application/fits" @@ -531,7 +512,7 @@ def get_parts(self): parts = collections.OrderedDict() _part = part.Part("x") if self.complete: - _part.product_type = chunk.ProductType.SCIENCE + _part.product_type = chunk.DataLinkSemantics.SCIENCE if self.depth > 4: for _chunk in self.get_chunks(): _part.chunks.append(_chunk) @@ -542,7 +523,7 @@ def get_chunks(self): chunks = caom_util.TypedList(chunk.Chunk, ) _chunk = chunk.Chunk() if self.complete: - _chunk.product_type = chunk.ProductType.SCIENCE + _chunk.product_type = chunk.DataLinkSemantics.SCIENCE _chunk.naxis = 5 _chunk.observable_axis = 1 _chunk.position_axis_1 = 1 diff --git a/caom2/caom2/tests/data/CompleteCompositeCircle-CAOM-2.2.xml b/caom2/caom2/tests/data/CompleteCompositeCircle-CAOM-2.2.xml deleted file mode 100644 index 2e0b3506..00000000 --- a/caom2/caom2/tests/data/CompleteCompositeCircle-CAOM-2.2.xml +++ /dev/null @@ -1,105 +0,0 @@ - - - collection - observationID - 2012-07-11T13:26:37.000 - 10 - - algorithmName - - filed - science - - proposalId - proposalPi - proposalProject - proposalTitle - keyword - - - targetName - object - false - 1.5 - false - keyword - - - coordsys - 3.0 - - 1.0 - 2.0 - - - - fail - - - telescopeName - 1.0 - 2.0 - 3.0 - keyword - - - 0.08 - 0.35 - 2.7 - 0.7 - 0.00045 - 20.0 - true - - - - productID - 2012-07-11T13:26:37.000 - 2012-07-11T13:26:37.000 - image - 3 - - name - version - producer - run_id - http://foo/bar - 2012-07-11T13:26:37.000 - keyword - - caom:foo/bar/plane2 - caom:foo/bar/plane1 - - - - 1.0 - 2.0 - 3.0 - 4.0 - 5.0 - - - junk - - - - ad:foo/bar1 - science - meta - application/fits - 12345 - - - x - science - - - - - - - - - caom:foo/bar - - diff --git a/caom2/caom2/tests/data/CompleteCompositePolygon-CAOM-2.2.xml b/caom2/caom2/tests/data/CompleteCompositePolygon-CAOM-2.2.xml deleted file mode 100644 index 8dfd6c7c..00000000 --- a/caom2/caom2/tests/data/CompleteCompositePolygon-CAOM-2.2.xml +++ /dev/null @@ -1,102 +0,0 @@ - - - collection - observationID - 2012-07-11T13:26:37.000 - 10 - - algorithmName - - filed - science - - proposalId - proposalPi - proposalProject - proposalTitle - keyword - - - targetName - object - false - 1.5 - false - keyword - - - coordsys - 3.0 - - 1.0 - 2.0 - - - - telescopeName - 1.0 - 2.0 - 3.0 - keyword - - - 0.08 - 0.35 - 2.7 - 0.7 - 0.00045 - 20.0 - true - - - - productID - 2012-07-11T13:26:37.000 - 2012-07-11T13:26:37.000 - image - 3 - - name - version - producer - run_id - http://foo/bar - 2012-07-11T13:26:37.000 - keyword - - caom:foo/bar/plane2 - caom:foo/bar/plane1 - - - - 1.0 - 2.0 - 3.0 - 4.0 - 5.0 - - - junk - - - - ad:foo/bar1 - science - meta - application/fits - 12345 - - - x - science - - - - - - - - - caom:foo/bar - - diff --git a/caom2/caom2/tests/data/CompleteSimpleCircle-CAOM-2.2.xml b/caom2/caom2/tests/data/CompleteSimpleCircle-CAOM-2.2.xml deleted file mode 100644 index dc4af87e..00000000 --- a/caom2/caom2/tests/data/CompleteSimpleCircle-CAOM-2.2.xml +++ /dev/null @@ -1,102 +0,0 @@ - - - collection - observationID - 2012-07-11T13:26:37.000 - 5 - - exposure - - flat - calibration - - proposalId - proposalPi - proposalProject - proposalTitle - keyword - - - targetName - object - false - 1.5 - false - keyword - - - coordsys - 3.0 - - 1.0 - 2.0 - - - - fail - - - telescopeName - 1.0 - 2.0 - 3.0 - keyword - - - 0.08 - 0.35 - 2.7 - 0.7 - 0.00045 - 20.0 - true - - - - productID - 2012-07-11T13:26:37.000 - 2012-07-11T13:26:37.000 - image - 3 - - name - version - producer - run_id - http://foo/bar - 2012-07-11T13:26:37.000 - keyword - - caom:foo/bar/plane2 - caom:foo/bar/plane1 - - - - 1.0 - 2.0 - 3.0 - 4.0 - 5.0 - - - junk - - - - ad:foo/bar1 - science - meta - application/fits - 12345 - - - x - science - - - - - - - - diff --git a/caom2/caom2/tests/data/CompleteSimplePolygon-CAOM-2.2.xml b/caom2/caom2/tests/data/CompleteSimplePolygon-CAOM-2.2.xml deleted file mode 100644 index 9a8308d5..00000000 --- a/caom2/caom2/tests/data/CompleteSimplePolygon-CAOM-2.2.xml +++ /dev/null @@ -1,102 +0,0 @@ - - - collection - observationID - 2012-07-11T13:26:37.000 - 5 - - exposure - - flat - calibration - - proposalId - proposalPi - proposalProject - proposalTitle - keyword - - - targetName - object - false - 1.5 - false - keyword - - - coordsys - 3.0 - - 1.0 - 2.0 - - - - fail - - - telescopeName - 1.0 - 2.0 - 3.0 - keyword - - - 0.08 - 0.35 - 2.7 - 0.7 - 0.00045 - 20.0 - true - - - - productID - 2012-07-11T13:26:37.000 - 2012-07-11T13:26:37.000 - catalog - 3 - - name - version - producer - run_id - http://foo/bar - 2012-07-11T13:26:37.000 - keyword - - caom:foo/bar/plane2 - caom:foo/bar/plane1 - - - - 1.0 - 2.0 - 3.0 - 4.0 - 5.0 - - - junk - - - - ad:foo/bar1 - science - meta - application/fits - 12345 - - - x - science - - - - - - - - diff --git a/caom2/caom2/tests/data/MinimalCompositeCircle-CAOM-2.2.xml b/caom2/caom2/tests/data/MinimalCompositeCircle-CAOM-2.2.xml deleted file mode 100644 index 03226c1f..00000000 --- a/caom2/caom2/tests/data/MinimalCompositeCircle-CAOM-2.2.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - collection - observationID - - algorithmName - - - - productID - - - ad:foo/bar1 - science - meta - - - x - - - - - - - - - caom:foo/bar - - diff --git a/caom2/caom2/tests/data/MinimalCompositePolygon-CAOM-2.2.xml b/caom2/caom2/tests/data/MinimalCompositePolygon-CAOM-2.2.xml deleted file mode 100644 index c46144e4..00000000 --- a/caom2/caom2/tests/data/MinimalCompositePolygon-CAOM-2.2.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - collection - observationID - - algorithmName - - - - productID - - - ad:foo/bar1 - science - meta - - - x - - - - - - - - - caom:foo/bar - - diff --git a/caom2/caom2/tests/data/MinimalSimpleCircle-CAOM-2.2.xml b/caom2/caom2/tests/data/MinimalSimpleCircle-CAOM-2.2.xml deleted file mode 100644 index 85f47d63..00000000 --- a/caom2/caom2/tests/data/MinimalSimpleCircle-CAOM-2.2.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - collection - observationID - - exposure - - - - productID - - - ad:foo/bar1 - science - meta - - - x - - - - - - - - diff --git a/caom2/caom2/tests/data/MinimalSimplePolygon-CAOM-2.2.xml b/caom2/caom2/tests/data/MinimalSimplePolygon-CAOM-2.2.xml deleted file mode 100644 index 742be676..00000000 --- a/caom2/caom2/tests/data/MinimalSimplePolygon-CAOM-2.2.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - collection - observationID - - exposure - - - - productID - - - ad:foo/bar1 - science - meta - - - x - - - - - - - - diff --git a/caom2/caom2/tests/data/sample-derived-caom25.xml b/caom2/caom2/tests/data/sample-derived-caom25.xml new file mode 100644 index 00000000..e075f587 --- /dev/null +++ b/caom2/caom2/tests/data/sample-derived-caom25.xml @@ -0,0 +1,5469 @@ + + + TEST + caom:TEST/observationID + 7c1 + 2025-01-10T23:55:32.467 + + ivo://cadc.nrc.ca/gms?A + ivo://cadc.nrc.ca/gms?B + + 123 + + algorithmName + + field + science + + proposalId + proposalPi + proposalProject + proposalTitle + mast:HST:proposal/123 + + keyword1 + keyword2 + + + + targetName + naif:1701 + object + false + 1.5 + false + + keyword1 + keyword2 + + + + FK5 + 2000.0 + + 1.0 + 2.0 + + + + fail + + + telescopeName + 1.0 + 2.0 + 3.0 + sidereal + + keyword1 + keyword2 + + + + instrumentName + + keyword1 + keyword2 + + + + 0.08 + 0.35 + 2.7 + 1.7 + 4.5E-4 + 666.0 + true + + + + caom:collection/observation/plane0 + 2025-01-10T23:55:32.467 + + ivo://cadc.nrc.ca/gms?A + ivo://cadc.nrc.ca/gms?B + + 2025-01-10T23:55:32.467 + + ivo://cadc.nrc.ca/gms?C + ivo://cadc.nrc.ca/gms?D + + image + 3 + + provenanceName + provenanceVersion + provenanceProject + provenanceProducer + provenanceRunID + http://foo/bar + 2025-01-10T23:55:32.467 + + keyword1 + keyword2 + + + caom:foo/bar/plane1 + caom:foo/bar/plane2 + + + + phot.count + relative + + + 100.0 + 33.3 + 0.2 + 1.0E-8 + 27.5 + 2.7 + + + junk + + + + + + 2.0 + 2.0 + + + 1.0 + 4.0 + + + 3.0 + 3.0 + + + + + + + + 2.0 + 2.0 + + + 1.0 + 4.0 + + + 3.0 + 3.0 + + + + + + + + 2.0 + 2.0 + + + 1.0 + 4.0 + + + 3.0 + 3.0 + + + + + 1024 + 2048 + + + 0.2 + 0.4 + + 0.05 + + 0.04 + 0.06 + + 0.025 + absolute + + + + 4.0E-4 + 9.0E-4 + + + + 4.0E-4 + 5.0E-4 + + + 8.0E-4 + 9.0E-4 + + + 2 + 2.0 + + 1.0 + 8.0 + + 1.0E-4 + + 1.0E-4 + 1.0E-4 + + 1.0E-4 + V + + Radio + Millimeter + Infrared + Optical + UV + EUV + X-ray + Gamma-ray + + 6.0E-7 + + H + alpha + + normalized + + + + 50000.25 + 50000.75 + + + + 50000.25 + 50000.4 + + + 50000.5 + 50000.75 + + + 2 + 0.5 + + 0.5 + 1.1 + + 0.15 + 600.0 + + 500.0 + 700.0 + + absolute + + + + I + Q + U + + 3 + + + FARADAY + + 10.0 + 20.0 + + + + 10.0 + 13.0 + + + 17.0 + 20.0 + + + 10 + + + + 20.0 + 120.0 + + 0.5 + 0.85 + + + + ad:foo/bar0 + 109 + this + data + 2025-01-10T23:55:32.467 + + ivo://cadc.nrc.ca/gms?C + ivo://cadc.nrc.ca/gms?D + + application/fits + 12345 + md5:d41d8cd98f00b204e9800998ecf8427e + desc:TEST/science-ready-data + + + x0 + this + + + 5 + 6 + 1 + 2 + 3 + 4 + 5 + 7 + + + + sliceCtype + sliceCunit + + 1 + + + + sliceCtype + sliceCunit + + 1 + + + + + + RA + deg + + + DEC + deg + + + 1.0 + 1.5 + + + 2.0 + 2.5 + + + + + 1.0 + 10.0 + + + 1.0 + 11.0 + + + + + 20.0 + 12.0 + + + 40.0 + 13.0 + + + + + + + 11.0 + 12.0 + + 0.5 + + + + + 20 + 20 + + + + 10.0 + 11.0 + + + 20.0 + 12.0 + + + 0.05 + 0.0 + 0.0 + 0.05 + + + ICRS + 2000.0 + 0.5 + + + + + WAVE + m + + + 1.0 + 1.5 + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + 4 + 10.0 + + 0.5 + 100.0 + + + + BARCENT + BARCENT + BARCENT + 1.0 + 2.0 + 3.0 + 4.0 + 5.0 + energy bandpassName + 6.0 + + H + 21cm + + + + + + TIME + d + + + 1.0 + 1.5 + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + 4 + 10.0 + + 0.5 + 100.0 + + + + UTC + TOPOCENTER + 1.0 + 2.0 + + + + + STOKES + + + 1.0 + 1.5 + + + + 0.5 + 1.0 + + + 3.5 + 4.0 + + + + + + + 0.5 + -4.0 + + + 3.5 + -1.0 + + + + + 3.5 + 1.0 + + + 7.5 + 4.0 + + + + + + 4 + 1.0 + + 0.5 + 1.0 + + + + + + + + RM + rad/m**2 + + + 1.0E-4 + 1.0E-6 + + + + 0.5 + 1.0 + + + 10.5 + 4.0 + + + + + + + 0.5 + 1.0 + + + 10.5 + 4.0 + + + + + + 10 + 1.0 + + 0.5 + 1.0 + + + + + + + 5 + 6 + 1 + 2 + 3 + 4 + 5 + 7 + + + + sliceCtype + sliceCunit + + 1 + + + + sliceCtype + sliceCunit + + 1 + + + + + + RA + deg + + + DEC + deg + + + 1.0 + 1.5 + + + 2.0 + 2.5 + + + + + 1.0 + 10.0 + + + 1.0 + 11.0 + + + + + 20.0 + 12.0 + + + 40.0 + 13.0 + + + + + + + 11.0 + 12.0 + + 0.5 + + + + + 20 + 20 + + + + 10.0 + 11.0 + + + 20.0 + 12.0 + + + 0.05 + 0.0 + 0.0 + 0.05 + + + ICRS + 2000.0 + 0.5 + + + + + WAVE + m + + + 1.0 + 1.5 + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + 4 + 10.0 + + 0.5 + 100.0 + + + + BARCENT + BARCENT + BARCENT + 1.0 + 2.0 + 3.0 + 4.0 + 5.0 + energy bandpassName + 6.0 + + H + 21cm + + + + + + TIME + d + + + 1.0 + 1.5 + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + 4 + 10.0 + + 0.5 + 100.0 + + + + UTC + TOPOCENTER + 1.0 + 2.0 + + + + + STOKES + + + 1.0 + 1.5 + + + + 0.5 + 1.0 + + + 3.5 + 4.0 + + + + + + + 0.5 + -4.0 + + + 3.5 + -1.0 + + + + + 3.5 + 1.0 + + + 7.5 + 4.0 + + + + + + 4 + 1.0 + + 0.5 + 1.0 + + + + + + + + RM + rad/m**2 + + + 1.0E-4 + 1.0E-6 + + + + 0.5 + 1.0 + + + 10.5 + 4.0 + + + + + + + 0.5 + 1.0 + + + 10.5 + 4.0 + + + + + + 10 + 1.0 + + 0.5 + 1.0 + + + + + + + + + x1 + this + + + 5 + 6 + 1 + 2 + 3 + 4 + 5 + 7 + + + + sliceCtype + sliceCunit + + 1 + + + + sliceCtype + sliceCunit + + 1 + + + + + + RA + deg + + + DEC + deg + + + 1.0 + 1.5 + + + 2.0 + 2.5 + + + + + 1.0 + 10.0 + + + 1.0 + 11.0 + + + + + 20.0 + 12.0 + + + 40.0 + 13.0 + + + + + + + 11.0 + 12.0 + + 0.5 + + + + + 20 + 20 + + + + 10.0 + 11.0 + + + 20.0 + 12.0 + + + 0.05 + 0.0 + 0.0 + 0.05 + + + ICRS + 2000.0 + 0.5 + + + + + WAVE + m + + + 1.0 + 1.5 + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + 4 + 10.0 + + 0.5 + 100.0 + + + + BARCENT + BARCENT + BARCENT + 1.0 + 2.0 + 3.0 + 4.0 + 5.0 + energy bandpassName + 6.0 + + H + 21cm + + + + + + TIME + d + + + 1.0 + 1.5 + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + 4 + 10.0 + + 0.5 + 100.0 + + + + UTC + TOPOCENTER + 1.0 + 2.0 + + + + + STOKES + + + 1.0 + 1.5 + + + + 0.5 + 1.0 + + + 3.5 + 4.0 + + + + + + + 0.5 + -4.0 + + + 3.5 + -1.0 + + + + + 3.5 + 1.0 + + + 7.5 + 4.0 + + + + + + 4 + 1.0 + + 0.5 + 1.0 + + + + + + + + RM + rad/m**2 + + + 1.0E-4 + 1.0E-6 + + + + 0.5 + 1.0 + + + 10.5 + 4.0 + + + + + + + 0.5 + 1.0 + + + 10.5 + 4.0 + + + + + + 10 + 1.0 + + 0.5 + 1.0 + + + + + + + 5 + 6 + 1 + 2 + 3 + 4 + 5 + 7 + + + + sliceCtype + sliceCunit + + 1 + + + + sliceCtype + sliceCunit + + 1 + + + + + + RA + deg + + + DEC + deg + + + 1.0 + 1.5 + + + 2.0 + 2.5 + + + + + 1.0 + 10.0 + + + 1.0 + 11.0 + + + + + 20.0 + 12.0 + + + 40.0 + 13.0 + + + + + + + 11.0 + 12.0 + + 0.5 + + + + + 20 + 20 + + + + 10.0 + 11.0 + + + 20.0 + 12.0 + + + 0.05 + 0.0 + 0.0 + 0.05 + + + ICRS + 2000.0 + 0.5 + + + + + WAVE + m + + + 1.0 + 1.5 + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + 4 + 10.0 + + 0.5 + 100.0 + + + + BARCENT + BARCENT + BARCENT + 1.0 + 2.0 + 3.0 + 4.0 + 5.0 + energy bandpassName + 6.0 + + H + 21cm + + + + + + TIME + d + + + 1.0 + 1.5 + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + 4 + 10.0 + + 0.5 + 100.0 + + + + UTC + TOPOCENTER + 1.0 + 2.0 + + + + + STOKES + + + 1.0 + 1.5 + + + + 0.5 + 1.0 + + + 3.5 + 4.0 + + + + + + + 0.5 + -4.0 + + + 3.5 + -1.0 + + + + + 3.5 + 1.0 + + + 7.5 + 4.0 + + + + + + 4 + 1.0 + + 0.5 + 1.0 + + + + + + + + RM + rad/m**2 + + + 1.0E-4 + 1.0E-6 + + + + 0.5 + 1.0 + + + 10.5 + 4.0 + + + + + + + 0.5 + 1.0 + + + 10.5 + 4.0 + + + + + + 10 + 1.0 + + 0.5 + 1.0 + + + + + + + + + + + ad:foo/bar1 + 6de + this + data + 2025-01-10T23:55:32.467 + + ivo://cadc.nrc.ca/gms?C + ivo://cadc.nrc.ca/gms?D + + application/fits + 12345 + md5:d41d8cd98f00b204e9800998ecf8427e + desc:TEST/science-ready-data + + + x0 + this + + + 5 + 6 + 1 + 2 + 3 + 4 + 5 + 7 + + + + sliceCtype + sliceCunit + + 1 + + + + sliceCtype + sliceCunit + + 1 + + + + + + RA + deg + + + DEC + deg + + + 1.0 + 1.5 + + + 2.0 + 2.5 + + + + + 1.0 + 10.0 + + + 1.0 + 11.0 + + + + + 20.0 + 12.0 + + + 40.0 + 13.0 + + + + + + + 11.0 + 12.0 + + 0.5 + + + + + 20 + 20 + + + + 10.0 + 11.0 + + + 20.0 + 12.0 + + + 0.05 + 0.0 + 0.0 + 0.05 + + + ICRS + 2000.0 + 0.5 + + + + + WAVE + m + + + 1.0 + 1.5 + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + 4 + 10.0 + + 0.5 + 100.0 + + + + BARCENT + BARCENT + BARCENT + 1.0 + 2.0 + 3.0 + 4.0 + 5.0 + energy bandpassName + 6.0 + + H + 21cm + + + + + + TIME + d + + + 1.0 + 1.5 + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + 4 + 10.0 + + 0.5 + 100.0 + + + + UTC + TOPOCENTER + 1.0 + 2.0 + + + + + STOKES + + + 1.0 + 1.5 + + + + 0.5 + 1.0 + + + 3.5 + 4.0 + + + + + + + 0.5 + -4.0 + + + 3.5 + -1.0 + + + + + 3.5 + 1.0 + + + 7.5 + 4.0 + + + + + + 4 + 1.0 + + 0.5 + 1.0 + + + + + + + + RM + rad/m**2 + + + 1.0E-4 + 1.0E-6 + + + + 0.5 + 1.0 + + + 10.5 + 4.0 + + + + + + + 0.5 + 1.0 + + + 10.5 + 4.0 + + + + + + 10 + 1.0 + + 0.5 + 1.0 + + + + + + + 5 + 6 + 1 + 2 + 3 + 4 + 5 + 7 + + + + sliceCtype + sliceCunit + + 1 + + + + sliceCtype + sliceCunit + + 1 + + + + + + RA + deg + + + DEC + deg + + + 1.0 + 1.5 + + + 2.0 + 2.5 + + + + + 1.0 + 10.0 + + + 1.0 + 11.0 + + + + + 20.0 + 12.0 + + + 40.0 + 13.0 + + + + + + + 11.0 + 12.0 + + 0.5 + + + + + 20 + 20 + + + + 10.0 + 11.0 + + + 20.0 + 12.0 + + + 0.05 + 0.0 + 0.0 + 0.05 + + + ICRS + 2000.0 + 0.5 + + + + + WAVE + m + + + 1.0 + 1.5 + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + 4 + 10.0 + + 0.5 + 100.0 + + + + BARCENT + BARCENT + BARCENT + 1.0 + 2.0 + 3.0 + 4.0 + 5.0 + energy bandpassName + 6.0 + + H + 21cm + + + + + + TIME + d + + + 1.0 + 1.5 + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + 4 + 10.0 + + 0.5 + 100.0 + + + + UTC + TOPOCENTER + 1.0 + 2.0 + + + + + STOKES + + + 1.0 + 1.5 + + + + 0.5 + 1.0 + + + 3.5 + 4.0 + + + + + + + 0.5 + -4.0 + + + 3.5 + -1.0 + + + + + 3.5 + 1.0 + + + 7.5 + 4.0 + + + + + + 4 + 1.0 + + 0.5 + 1.0 + + + + + + + + RM + rad/m**2 + + + 1.0E-4 + 1.0E-6 + + + + 0.5 + 1.0 + + + 10.5 + 4.0 + + + + + + + 0.5 + 1.0 + + + 10.5 + 4.0 + + + + + + 10 + 1.0 + + 0.5 + 1.0 + + + + + + + + + x1 + this + + + 5 + 6 + 1 + 2 + 3 + 4 + 5 + 7 + + + + sliceCtype + sliceCunit + + 1 + + + + sliceCtype + sliceCunit + + 1 + + + + + + RA + deg + + + DEC + deg + + + 1.0 + 1.5 + + + 2.0 + 2.5 + + + + + 1.0 + 10.0 + + + 1.0 + 11.0 + + + + + 20.0 + 12.0 + + + 40.0 + 13.0 + + + + + + + 11.0 + 12.0 + + 0.5 + + + + + 20 + 20 + + + + 10.0 + 11.0 + + + 20.0 + 12.0 + + + 0.05 + 0.0 + 0.0 + 0.05 + + + ICRS + 2000.0 + 0.5 + + + + + WAVE + m + + + 1.0 + 1.5 + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + 4 + 10.0 + + 0.5 + 100.0 + + + + BARCENT + BARCENT + BARCENT + 1.0 + 2.0 + 3.0 + 4.0 + 5.0 + energy bandpassName + 6.0 + + H + 21cm + + + + + + TIME + d + + + 1.0 + 1.5 + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + 4 + 10.0 + + 0.5 + 100.0 + + + + UTC + TOPOCENTER + 1.0 + 2.0 + + + + + STOKES + + + 1.0 + 1.5 + + + + 0.5 + 1.0 + + + 3.5 + 4.0 + + + + + + + 0.5 + -4.0 + + + 3.5 + -1.0 + + + + + 3.5 + 1.0 + + + 7.5 + 4.0 + + + + + + 4 + 1.0 + + 0.5 + 1.0 + + + + + + + + RM + rad/m**2 + + + 1.0E-4 + 1.0E-6 + + + + 0.5 + 1.0 + + + 10.5 + 4.0 + + + + + + + 0.5 + 1.0 + + + 10.5 + 4.0 + + + + + + 10 + 1.0 + + 0.5 + 1.0 + + + + + + + 5 + 6 + 1 + 2 + 3 + 4 + 5 + 7 + + + + sliceCtype + sliceCunit + + 1 + + + + sliceCtype + sliceCunit + + 1 + + + + + + RA + deg + + + DEC + deg + + + 1.0 + 1.5 + + + 2.0 + 2.5 + + + + + 1.0 + 10.0 + + + 1.0 + 11.0 + + + + + 20.0 + 12.0 + + + 40.0 + 13.0 + + + + + + + 11.0 + 12.0 + + 0.5 + + + + + 20 + 20 + + + + 10.0 + 11.0 + + + 20.0 + 12.0 + + + 0.05 + 0.0 + 0.0 + 0.05 + + + ICRS + 2000.0 + 0.5 + + + + + WAVE + m + + + 1.0 + 1.5 + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + 4 + 10.0 + + 0.5 + 100.0 + + + + BARCENT + BARCENT + BARCENT + 1.0 + 2.0 + 3.0 + 4.0 + 5.0 + energy bandpassName + 6.0 + + H + 21cm + + + + + + TIME + d + + + 1.0 + 1.5 + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + 4 + 10.0 + + 0.5 + 100.0 + + + + UTC + TOPOCENTER + 1.0 + 2.0 + + + + + STOKES + + + 1.0 + 1.5 + + + + 0.5 + 1.0 + + + 3.5 + 4.0 + + + + + + + 0.5 + -4.0 + + + 3.5 + -1.0 + + + + + 3.5 + 1.0 + + + 7.5 + 4.0 + + + + + + 4 + 1.0 + + 0.5 + 1.0 + + + + + + + + RM + rad/m**2 + + + 1.0E-4 + 1.0E-6 + + + + 0.5 + 1.0 + + + 10.5 + 4.0 + + + + + + + 0.5 + 1.0 + + + 10.5 + 4.0 + + + + + + 10 + 1.0 + + 0.5 + 1.0 + + + + + + + + + + + + + caom:collection/observation/plane1 + 2025-01-10T23:55:32.467 + + ivo://cadc.nrc.ca/gms?A + ivo://cadc.nrc.ca/gms?B + + 2025-01-10T23:55:32.467 + + ivo://cadc.nrc.ca/gms?C + ivo://cadc.nrc.ca/gms?D + + image + 3 + + provenanceName + provenanceVersion + provenanceProject + provenanceProducer + provenanceRunID + http://foo/bar + 2025-01-10T23:55:32.467 + + keyword1 + keyword2 + + + caom:foo/bar/plane1 + caom:foo/bar/plane2 + + + + phot.count + relative + + + 100.0 + 33.3 + 0.2 + 1.0E-8 + 27.5 + 2.7 + + + junk + + + + + 2.0 + 4.0 + + 1.0 + + + + + 2.0 + 4.0 + + 1.0 + + + + + 2.0 + 4.0 + + 0.6 + + + 1024 + 2048 + + + 0.2 + 0.4 + + 0.05 + + 0.04 + 0.06 + + 0.025 + absolute + + + + 4.0E-4 + 9.0E-4 + + + + 4.0E-4 + 5.0E-4 + + + 8.0E-4 + 9.0E-4 + + + 2 + 2.0 + + 1.0 + 8.0 + + 1.0E-4 + + 1.0E-4 + 1.0E-4 + + 1.0E-4 + V + + Radio + Millimeter + Infrared + Optical + UV + EUV + X-ray + Gamma-ray + + 6.0E-7 + + H + alpha + + normalized + + + + 50000.25 + 50000.75 + + + + 50000.25 + 50000.4 + + + 50000.5 + 50000.75 + + + 2 + 0.5 + + 0.5 + 1.1 + + 0.15 + 600.0 + + 500.0 + 700.0 + + absolute + + + + I + Q + U + + 3 + + + FARADAY + + 10.0 + 20.0 + + + + 10.0 + 13.0 + + + 17.0 + 20.0 + + + 10 + + + + 20.0 + 120.0 + + 0.5 + 0.85 + + + + ad:foo/bar0 + 109 + this + data + 2025-01-10T23:55:32.467 + + ivo://cadc.nrc.ca/gms?C + ivo://cadc.nrc.ca/gms?D + + application/fits + 12345 + md5:d41d8cd98f00b204e9800998ecf8427e + desc:TEST/science-ready-data + + + x0 + this + + + 5 + 6 + 1 + 2 + 3 + 4 + 5 + 7 + + + + sliceCtype + sliceCunit + + 1 + + + + sliceCtype + sliceCunit + + 1 + + + + + + RA + deg + + + DEC + deg + + + 1.0 + 1.5 + + + 2.0 + 2.5 + + + + + 1.0 + 10.0 + + + 1.0 + 11.0 + + + + + 20.0 + 12.0 + + + 40.0 + 13.0 + + + + + + + 11.0 + 12.0 + + 0.5 + + + + + 20 + 20 + + + + 10.0 + 11.0 + + + 20.0 + 12.0 + + + 0.05 + 0.0 + 0.0 + 0.05 + + + ICRS + 2000.0 + 0.5 + + + + + WAVE + m + + + 1.0 + 1.5 + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + 4 + 10.0 + + 0.5 + 100.0 + + + + BARCENT + BARCENT + BARCENT + 1.0 + 2.0 + 3.0 + 4.0 + 5.0 + energy bandpassName + 6.0 + + H + 21cm + + + + + + TIME + d + + + 1.0 + 1.5 + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + 4 + 10.0 + + 0.5 + 100.0 + + + + UTC + TOPOCENTER + 1.0 + 2.0 + + + + + STOKES + + + 1.0 + 1.5 + + + + 0.5 + 1.0 + + + 3.5 + 4.0 + + + + + + + 0.5 + -4.0 + + + 3.5 + -1.0 + + + + + 3.5 + 1.0 + + + 7.5 + 4.0 + + + + + + 4 + 1.0 + + 0.5 + 1.0 + + + + + + + + RM + rad/m**2 + + + 1.0E-4 + 1.0E-6 + + + + 0.5 + 1.0 + + + 10.5 + 4.0 + + + + + + + 0.5 + 1.0 + + + 10.5 + 4.0 + + + + + + 10 + 1.0 + + 0.5 + 1.0 + + + + + + + 5 + 6 + 1 + 2 + 3 + 4 + 5 + 7 + + + + sliceCtype + sliceCunit + + 1 + + + + sliceCtype + sliceCunit + + 1 + + + + + + RA + deg + + + DEC + deg + + + 1.0 + 1.5 + + + 2.0 + 2.5 + + + + + 1.0 + 10.0 + + + 1.0 + 11.0 + + + + + 20.0 + 12.0 + + + 40.0 + 13.0 + + + + + + + 11.0 + 12.0 + + 0.5 + + + + + 20 + 20 + + + + 10.0 + 11.0 + + + 20.0 + 12.0 + + + 0.05 + 0.0 + 0.0 + 0.05 + + + ICRS + 2000.0 + 0.5 + + + + + WAVE + m + + + 1.0 + 1.5 + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + 4 + 10.0 + + 0.5 + 100.0 + + + + BARCENT + BARCENT + BARCENT + 1.0 + 2.0 + 3.0 + 4.0 + 5.0 + energy bandpassName + 6.0 + + H + 21cm + + + + + + TIME + d + + + 1.0 + 1.5 + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + 4 + 10.0 + + 0.5 + 100.0 + + + + UTC + TOPOCENTER + 1.0 + 2.0 + + + + + STOKES + + + 1.0 + 1.5 + + + + 0.5 + 1.0 + + + 3.5 + 4.0 + + + + + + + 0.5 + -4.0 + + + 3.5 + -1.0 + + + + + 3.5 + 1.0 + + + 7.5 + 4.0 + + + + + + 4 + 1.0 + + 0.5 + 1.0 + + + + + + + + RM + rad/m**2 + + + 1.0E-4 + 1.0E-6 + + + + 0.5 + 1.0 + + + 10.5 + 4.0 + + + + + + + 0.5 + 1.0 + + + 10.5 + 4.0 + + + + + + 10 + 1.0 + + 0.5 + 1.0 + + + + + + + + + x1 + this + + + 5 + 6 + 1 + 2 + 3 + 4 + 5 + 7 + + + + sliceCtype + sliceCunit + + 1 + + + + sliceCtype + sliceCunit + + 1 + + + + + + RA + deg + + + DEC + deg + + + 1.0 + 1.5 + + + 2.0 + 2.5 + + + + + 1.0 + 10.0 + + + 1.0 + 11.0 + + + + + 20.0 + 12.0 + + + 40.0 + 13.0 + + + + + + + 11.0 + 12.0 + + 0.5 + + + + + 20 + 20 + + + + 10.0 + 11.0 + + + 20.0 + 12.0 + + + 0.05 + 0.0 + 0.0 + 0.05 + + + ICRS + 2000.0 + 0.5 + + + + + WAVE + m + + + 1.0 + 1.5 + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + 4 + 10.0 + + 0.5 + 100.0 + + + + BARCENT + BARCENT + BARCENT + 1.0 + 2.0 + 3.0 + 4.0 + 5.0 + energy bandpassName + 6.0 + + H + 21cm + + + + + + TIME + d + + + 1.0 + 1.5 + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + 4 + 10.0 + + 0.5 + 100.0 + + + + UTC + TOPOCENTER + 1.0 + 2.0 + + + + + STOKES + + + 1.0 + 1.5 + + + + 0.5 + 1.0 + + + 3.5 + 4.0 + + + + + + + 0.5 + -4.0 + + + 3.5 + -1.0 + + + + + 3.5 + 1.0 + + + 7.5 + 4.0 + + + + + + 4 + 1.0 + + 0.5 + 1.0 + + + + + + + + RM + rad/m**2 + + + 1.0E-4 + 1.0E-6 + + + + 0.5 + 1.0 + + + 10.5 + 4.0 + + + + + + + 0.5 + 1.0 + + + 10.5 + 4.0 + + + + + + 10 + 1.0 + + 0.5 + 1.0 + + + + + + + 5 + 6 + 1 + 2 + 3 + 4 + 5 + 7 + + + + sliceCtype + sliceCunit + + 1 + + + + sliceCtype + sliceCunit + + 1 + + + + + + RA + deg + + + DEC + deg + + + 1.0 + 1.5 + + + 2.0 + 2.5 + + + + + 1.0 + 10.0 + + + 1.0 + 11.0 + + + + + 20.0 + 12.0 + + + 40.0 + 13.0 + + + + + + + 11.0 + 12.0 + + 0.5 + + + + + 20 + 20 + + + + 10.0 + 11.0 + + + 20.0 + 12.0 + + + 0.05 + 0.0 + 0.0 + 0.05 + + + ICRS + 2000.0 + 0.5 + + + + + WAVE + m + + + 1.0 + 1.5 + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + 4 + 10.0 + + 0.5 + 100.0 + + + + BARCENT + BARCENT + BARCENT + 1.0 + 2.0 + 3.0 + 4.0 + 5.0 + energy bandpassName + 6.0 + + H + 21cm + + + + + + TIME + d + + + 1.0 + 1.5 + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + 4 + 10.0 + + 0.5 + 100.0 + + + + UTC + TOPOCENTER + 1.0 + 2.0 + + + + + STOKES + + + 1.0 + 1.5 + + + + 0.5 + 1.0 + + + 3.5 + 4.0 + + + + + + + 0.5 + -4.0 + + + 3.5 + -1.0 + + + + + 3.5 + 1.0 + + + 7.5 + 4.0 + + + + + + 4 + 1.0 + + 0.5 + 1.0 + + + + + + + + RM + rad/m**2 + + + 1.0E-4 + 1.0E-6 + + + + 0.5 + 1.0 + + + 10.5 + 4.0 + + + + + + + 0.5 + 1.0 + + + 10.5 + 4.0 + + + + + + 10 + 1.0 + + 0.5 + 1.0 + + + + + + + + + + + ad:foo/bar1 + 6de + this + data + 2025-01-10T23:55:32.467 + + ivo://cadc.nrc.ca/gms?C + ivo://cadc.nrc.ca/gms?D + + application/fits + 12345 + md5:d41d8cd98f00b204e9800998ecf8427e + desc:TEST/science-ready-data + + + x0 + this + + + 5 + 6 + 1 + 2 + 3 + 4 + 5 + 7 + + + + sliceCtype + sliceCunit + + 1 + + + + sliceCtype + sliceCunit + + 1 + + + + + + RA + deg + + + DEC + deg + + + 1.0 + 1.5 + + + 2.0 + 2.5 + + + + + 1.0 + 10.0 + + + 1.0 + 11.0 + + + + + 20.0 + 12.0 + + + 40.0 + 13.0 + + + + + + + 11.0 + 12.0 + + 0.5 + + + + + 20 + 20 + + + + 10.0 + 11.0 + + + 20.0 + 12.0 + + + 0.05 + 0.0 + 0.0 + 0.05 + + + ICRS + 2000.0 + 0.5 + + + + + WAVE + m + + + 1.0 + 1.5 + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + 4 + 10.0 + + 0.5 + 100.0 + + + + BARCENT + BARCENT + BARCENT + 1.0 + 2.0 + 3.0 + 4.0 + 5.0 + energy bandpassName + 6.0 + + H + 21cm + + + + + + TIME + d + + + 1.0 + 1.5 + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + 4 + 10.0 + + 0.5 + 100.0 + + + + UTC + TOPOCENTER + 1.0 + 2.0 + + + + + STOKES + + + 1.0 + 1.5 + + + + 0.5 + 1.0 + + + 3.5 + 4.0 + + + + + + + 0.5 + -4.0 + + + 3.5 + -1.0 + + + + + 3.5 + 1.0 + + + 7.5 + 4.0 + + + + + + 4 + 1.0 + + 0.5 + 1.0 + + + + + + + + RM + rad/m**2 + + + 1.0E-4 + 1.0E-6 + + + + 0.5 + 1.0 + + + 10.5 + 4.0 + + + + + + + 0.5 + 1.0 + + + 10.5 + 4.0 + + + + + + 10 + 1.0 + + 0.5 + 1.0 + + + + + + + 5 + 6 + 1 + 2 + 3 + 4 + 5 + 7 + + + + sliceCtype + sliceCunit + + 1 + + + + sliceCtype + sliceCunit + + 1 + + + + + + RA + deg + + + DEC + deg + + + 1.0 + 1.5 + + + 2.0 + 2.5 + + + + + 1.0 + 10.0 + + + 1.0 + 11.0 + + + + + 20.0 + 12.0 + + + 40.0 + 13.0 + + + + + + + 11.0 + 12.0 + + 0.5 + + + + + 20 + 20 + + + + 10.0 + 11.0 + + + 20.0 + 12.0 + + + 0.05 + 0.0 + 0.0 + 0.05 + + + ICRS + 2000.0 + 0.5 + + + + + WAVE + m + + + 1.0 + 1.5 + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + 4 + 10.0 + + 0.5 + 100.0 + + + + BARCENT + BARCENT + BARCENT + 1.0 + 2.0 + 3.0 + 4.0 + 5.0 + energy bandpassName + 6.0 + + H + 21cm + + + + + + TIME + d + + + 1.0 + 1.5 + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + 4 + 10.0 + + 0.5 + 100.0 + + + + UTC + TOPOCENTER + 1.0 + 2.0 + + + + + STOKES + + + 1.0 + 1.5 + + + + 0.5 + 1.0 + + + 3.5 + 4.0 + + + + + + + 0.5 + -4.0 + + + 3.5 + -1.0 + + + + + 3.5 + 1.0 + + + 7.5 + 4.0 + + + + + + 4 + 1.0 + + 0.5 + 1.0 + + + + + + + + RM + rad/m**2 + + + 1.0E-4 + 1.0E-6 + + + + 0.5 + 1.0 + + + 10.5 + 4.0 + + + + + + + 0.5 + 1.0 + + + 10.5 + 4.0 + + + + + + 10 + 1.0 + + 0.5 + 1.0 + + + + + + + + + x1 + this + + + 5 + 6 + 1 + 2 + 3 + 4 + 5 + 7 + + + + sliceCtype + sliceCunit + + 1 + + + + sliceCtype + sliceCunit + + 1 + + + + + + RA + deg + + + DEC + deg + + + 1.0 + 1.5 + + + 2.0 + 2.5 + + + + + 1.0 + 10.0 + + + 1.0 + 11.0 + + + + + 20.0 + 12.0 + + + 40.0 + 13.0 + + + + + + + 11.0 + 12.0 + + 0.5 + + + + + 20 + 20 + + + + 10.0 + 11.0 + + + 20.0 + 12.0 + + + 0.05 + 0.0 + 0.0 + 0.05 + + + ICRS + 2000.0 + 0.5 + + + + + WAVE + m + + + 1.0 + 1.5 + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + 4 + 10.0 + + 0.5 + 100.0 + + + + BARCENT + BARCENT + BARCENT + 1.0 + 2.0 + 3.0 + 4.0 + 5.0 + energy bandpassName + 6.0 + + H + 21cm + + + + + + TIME + d + + + 1.0 + 1.5 + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + 4 + 10.0 + + 0.5 + 100.0 + + + + UTC + TOPOCENTER + 1.0 + 2.0 + + + + + STOKES + + + 1.0 + 1.5 + + + + 0.5 + 1.0 + + + 3.5 + 4.0 + + + + + + + 0.5 + -4.0 + + + 3.5 + -1.0 + + + + + 3.5 + 1.0 + + + 7.5 + 4.0 + + + + + + 4 + 1.0 + + 0.5 + 1.0 + + + + + + + + RM + rad/m**2 + + + 1.0E-4 + 1.0E-6 + + + + 0.5 + 1.0 + + + 10.5 + 4.0 + + + + + + + 0.5 + 1.0 + + + 10.5 + 4.0 + + + + + + 10 + 1.0 + + 0.5 + 1.0 + + + + + + + 5 + 6 + 1 + 2 + 3 + 4 + 5 + 7 + + + + sliceCtype + sliceCunit + + 1 + + + + sliceCtype + sliceCunit + + 1 + + + + + + RA + deg + + + DEC + deg + + + 1.0 + 1.5 + + + 2.0 + 2.5 + + + + + 1.0 + 10.0 + + + 1.0 + 11.0 + + + + + 20.0 + 12.0 + + + 40.0 + 13.0 + + + + + + + 11.0 + 12.0 + + 0.5 + + + + + 20 + 20 + + + + 10.0 + 11.0 + + + 20.0 + 12.0 + + + 0.05 + 0.0 + 0.0 + 0.05 + + + ICRS + 2000.0 + 0.5 + + + + + WAVE + m + + + 1.0 + 1.5 + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + 4 + 10.0 + + 0.5 + 100.0 + + + + BARCENT + BARCENT + BARCENT + 1.0 + 2.0 + 3.0 + 4.0 + 5.0 + energy bandpassName + 6.0 + + H + 21cm + + + + + + TIME + d + + + 1.0 + 1.5 + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + + 0.5 + 100.0 + + + 4.5 + 140.0 + + + + + + 4 + 10.0 + + 0.5 + 100.0 + + + + UTC + TOPOCENTER + 1.0 + 2.0 + + + + + STOKES + + + 1.0 + 1.5 + + + + 0.5 + 1.0 + + + 3.5 + 4.0 + + + + + + + 0.5 + -4.0 + + + 3.5 + -1.0 + + + + + 3.5 + 1.0 + + + 7.5 + 4.0 + + + + + + 4 + 1.0 + + 0.5 + 1.0 + + + + + + + + RM + rad/m**2 + + + 1.0E-4 + 1.0E-6 + + + + 0.5 + 1.0 + + + 10.5 + 4.0 + + + + + + + 0.5 + 1.0 + + + 10.5 + 4.0 + + + + + + 10 + 1.0 + + 0.5 + 1.0 + + + + + + + + + + + + + + caom:foo/bar + caom:foo/baz + + diff --git a/caom2/caom2/tests/test_artifact.py b/caom2/caom2/tests/test_artifact.py index 5fe0b54e..42e76176 100644 --- a/caom2/caom2/tests/test_artifact.py +++ b/caom2/caom2/tests/test_artifact.py @@ -90,10 +90,10 @@ def test_all(self): with self.assertRaises(TypeError): test_artifact = artifact.Artifact("caom:GEMINI/12345", artifact.ReleaseType.META, - artifact.ProductType.THUMBNAIL) + artifact.DataLinkSemantics.THUMBNAIL) with self.assertRaises(TypeError): test_artifact = artifact.Artifact("caom:GEMINI/12345", - artifact.ProductType.THUMBNAIL, + artifact.DataLinkSemantics.THUMBNAIL, None) with self.assertRaises(TypeError): test_artifact = artifact.Artifact("caom:GEMINI/12345", @@ -101,15 +101,15 @@ def test_all(self): artifact.ReleaseType.META) test_artifact = artifact.Artifact("caom:GEMINI/12345", - artifact.ProductType.THUMBNAIL, + artifact.DataLinkSemantics.THUMBNAIL, artifact.ReleaseType.META) urlparse("caom:GEMINI/12345") self.assertEqual("caom:GEMINI/12345", test_artifact.uri, "Artifact URI") - self.assertEqual(artifact.ProductType.THUMBNAIL, + self.assertEqual(artifact.DataLinkSemantics.THUMBNAIL, test_artifact.product_type, - "Artifact ProductType") + "Artifact DataLinkSemantics") self.assertEqual(artifact.ReleaseType.META, test_artifact.release_type, "Artifact ReleaseType") @@ -122,8 +122,8 @@ def test_all(self): test_artifact.content_length = 23000000000000 self.assertEqual(23000000000000, test_artifact.content_length, "Content length") - test_artifact.product_type = artifact.ProductType.PREVIEW - self.assertEqual(artifact.ProductType.PREVIEW, + test_artifact.product_type = artifact.DataLinkSemantics.PREVIEW + self.assertEqual(artifact.DataLinkSemantics.PREVIEW, test_artifact.product_type, "Product type") @@ -165,7 +165,7 @@ def test_all(self): test_artifact = artifact.Artifact( "caom://#observation://? something#//", artifact.ReleaseType('META'), - artifact.ProductType('THUMBNAIL')) + artifact.DataLinkSemantics('THUMBNAIL')) except ValueError: exception = True self.assertTrue(exception, "Missing exception") @@ -175,7 +175,7 @@ def test_all(self): test_artifact = artifact.Artifact( "observation/something", artifact.ReleaseType('META'), - artifact.ProductType('THUMBNAIL')) + artifact.DataLinkSemantics('THUMBNAIL')) # TODO re-enable when check enforced # with self.assertRaises(ValueError): diff --git a/caom2/caom2/tests/test_caom_util.py b/caom2/caom2/tests/test_caom_util.py index 04c3b36b..e8ed2068 100644 --- a/caom2/caom2/tests/test_caom_util.py +++ b/caom2/caom2/tests/test_caom_util.py @@ -71,7 +71,7 @@ from builtins import str, int -from .. import artifact +from .. import artifact, dali from .. import caom_util from .. import chunk from .. import part @@ -130,7 +130,8 @@ def test_typed_list(self): self.assertEqual(0, len(my_list2), "list2 length") def test_validate_path_component(self): - energy = plane.Energy() + bounds = dali.Interval(1.0, 2.0) + energy = plane.Energy(bounds, [bounds]) caom_util.validate_path_component(energy, "something", "some:test\\path") @@ -177,9 +178,9 @@ def test_typed_set(self): def test_typed_ordered_dict(self): # test validation and constructor with an empty dictionary - test_plane10 = plane.Plane('key10') + test_plane10 = plane.Plane('caom:CFHT/anobs/key10') test_artifact66 = artifact.Artifact("caom:CFHT/55/66", - chunk.ProductType.SCIENCE, + chunk.DataLinkSemantics.SCIENCE, artifact.ReleaseType.DATA) test_part10 = part.Part("10") test_plane_uri = plane.PlaneURI('caom:CFHT/55/66') @@ -201,60 +202,61 @@ def test_typed_ordered_dict(self): my_dict_plane['key1'] = float(2.0) # test assignment my_dict = caom_util.TypedOrderedDict(plane.Plane, ) - test_plane2 = plane.Plane('key2') - test_plane1 = plane.Plane('key1') - my_dict['key2'] = test_plane2 - my_dict['key1'] = test_plane1 + obs_uri = 'caom:TEST/obs1' + test_plane2 = plane.Plane(obs_uri + '/key2') + test_plane1 = plane.Plane(obs_uri + '/key1') + my_dict[test_plane2.uri] = test_plane2 + my_dict[test_plane1.uri] = test_plane1 # need to cast to list in order to make it work with both python # 2 and 3 self.assertEqual(2, len(my_dict), 'mismatch in the number of entries in dictionary.') - self.assertEqual('key2', list(my_dict.keys())[0], + self.assertEqual(test_plane2.uri, list(my_dict.keys())[0], 'key mismatch for 1st key') - self.assertEqual('key1', list(my_dict.keys())[1], + self.assertEqual(test_plane1.uri, list(my_dict.keys())[1], 'key mismatch for 2nd key') self.assertEqual(test_plane2, list(my_dict.values())[0], 'value mismatch for 1st value') self.assertEqual(test_plane1, list(my_dict.values())[1], 'value mismatch for 2nd value') # test constructor with non-empty dictionary - test_plane1 = plane.Plane('key1') - test_plane2 = plane.Plane('key2') my_dict1 = caom_util.TypedOrderedDict(plane.Plane, - ('key1', test_plane1), - ('key2', test_plane2)) + (test_plane1.uri, test_plane1), + (test_plane2.uri, test_plane2)) self.assertEqual(2, len(my_dict1), 'mismatch in the number of entries in dictionary.') # test assignment via setdefault self.assertRaises(TypeError, my_dict1.setdefault, 'key3', 'wrong value') - my_dict1.setdefault('key3', plane.Plane('key3')) + test_plane3 = plane.Plane('caom:TEST/obs1/key3') + my_dict1.setdefault(test_plane3.uri, test_plane3) self.assertEqual(3, len(my_dict1), 'mismatch in the number of entries in dictionary.') # test assignment via update my_dict1.update(my_dict) self.assertEqual(3, len(my_dict1), 'mismatch in the number of entries in dictionary.') - self.assertEqual('key2', list(my_dict.keys())[0], + self.assertEqual(test_plane2.uri, list(my_dict.keys())[0], 'key mismatch for 1st key') - self.assertEqual('key1', list(my_dict.keys())[1], + self.assertEqual(test_plane1.uri, list(my_dict.keys())[1], 'key mismatch for 2nd key') # test add function - my_dict1.add(plane.Plane('key4')) + test_plane4 = plane.Plane('caom:TEST/obs1/key4') + my_dict1.add(test_plane4) self.assertEqual(4, len(my_dict1), 'mismatch in the number of entries in dictionary.') - self.assertEqual('key1', list(my_dict1.keys())[0], + self.assertEqual(test_plane1.uri, list(my_dict1.keys())[0], 'key mismatch for 1st key') - self.assertEqual('key2', list(my_dict1.keys())[1], + self.assertEqual(test_plane2.uri, list(my_dict1.keys())[1], 'key mismatch for 2nd key') - self.assertEqual('key3', list(my_dict1.keys())[2], + self.assertEqual(test_plane3.uri, list(my_dict1.keys())[2], 'key mismatch for 3rd key') - self.assertEqual('key4', list(my_dict1.keys())[3], + self.assertEqual(test_plane4.uri, list(my_dict1.keys())[3], 'key mismatch for 4th key') - plane5 = plane.Plane("key5") - my_dict1[plane5._key()] = plane5 + test_plane5 = plane.Plane('caom:TEST/obs1/key5') + my_dict1[test_plane5.uri] = test_plane5 with self.assertRaises(TypeError): my_dict1.add(test_plane_uri) @@ -262,26 +264,26 @@ def test_typed_ordered_dict(self): # test pop function self.assertEqual(5, len(my_dict1), 'mismatch in the number of entries in dictionary.') - my_value = my_dict1.pop('key4') - self.assertEqual('key4', my_value._key(), + my_value = my_dict1.pop(test_plane4.uri) + self.assertEqual(test_plane4.uri, my_value._key(), 'popped the wrong entry from dictionary.') self.assertEqual(4, len(my_dict1), 'mismatch in the number of entries in dictionary.') - my_value = my_dict1.pop('key5') - self.assertEqual('key5', my_value._key(), + my_value = my_dict1.pop(test_plane5.uri) + self.assertEqual(test_plane5.uri, my_value._key(), 'popped the wrong entry from dictionary.') self.assertEqual(3, len(my_dict1), 'mismatch in the number of entries in dictionary.') - my_value = my_dict1.pop('key3') - self.assertEqual('key3', my_value._key(), + my_value = my_dict1.pop(test_plane3.uri) + self.assertEqual(test_plane3.uri, my_value._key(), 'popped the wrong entry from dictionary.') self.assertEqual(2, len(my_dict1), 'mismatch in the number of entries in dictionary.') - my_value = my_dict1.pop('key2') - self.assertEqual('key2', my_value._key(), + my_value = my_dict1.pop(test_plane2.uri) + self.assertEqual(test_plane2.uri, my_value._key(), 'popped the wrong entry from dictionary.') self.assertEqual(1, len(my_dict1), 'mismatch in the number of entries in dictionary.') - my_value = my_dict1.pop('key1') - self.assertEqual('key1', my_value._key(), + my_value = my_dict1.pop(test_plane1.uri) + self.assertEqual(test_plane1.uri, my_value._key(), 'popped the wrong entry from dictionary.') diff --git a/caom2/caom2/tests/test_checksum.py b/caom2/caom2/tests/test_checksum.py index d167fa9e..d4e7e601 100644 --- a/caom2/caom2/tests/test_checksum.py +++ b/caom2/caom2/tests/test_checksum.py @@ -2,7 +2,7 @@ # ****************** CANADIAN ASTRONOMY DATA CENTRE ******************* # ************* CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** # -# (c) 2023. (c) 2023. +# (c) 2025. (c) 2025. # Government of Canada Gouvernement du Canada # National Research Council Conseil national de recherches # Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 @@ -137,7 +137,7 @@ def test_primitive_checksum(): assert ('5b71d023d4729575d550536dce8439e6' == md5.hexdigest()) -def test_compatibility(): +def atest_compatibility(): # tests loads a previously generated observation and checks the checksums # against the previously calculated (in Java) checksums @@ -304,7 +304,7 @@ def test_compatibility(): _common_check(obs) -def test_compatibility_simple_obs(): +def atest_compatibility_simple_obs(): # tests loads a previously generated observation and checks the checksums # against the previously calculated (in Java) checksums logger = logging.getLogger('checksum') @@ -327,7 +327,7 @@ def test_compatibility_simple_obs(): logger.setLevel(level) -def test_round_trip(): +def atest_round_trip(): source_file_path = os.path.join(THIS_DIR, TEST_DATA, 'SampleComposite-CAOM-2.3.xml') reader = obs_reader_writer.ObservationReader(True) @@ -351,8 +351,8 @@ def test_round_trip(): def test_checksum_diff(): for source_file_path in \ - [os.path.join(THIS_DIR, TEST_DATA, x) for x in - ['SampleDerived-CAOM-2.4.xml', 'SampleComposite-CAOM-2.3.xml']]: + [os.path.join(THIS_DIR, TEST_DATA, x) for x in ['sample-derived-caom25.xml']]: + #['SampleDerived-CAOM-2.4.xml', 'SampleComposite-CAOM-2.3.xml']]: logging.debug(source_file_path) output_file = tempfile.NamedTemporaryFile() sys.argv = 'caom2_checksum -d -o {} {}'.format( diff --git a/caom2/caom2/tests/test_chunk.py b/caom2/caom2/tests/test_chunk.py index aaa8d923..196dbba0 100644 --- a/caom2/caom2/tests/test_chunk.py +++ b/caom2/caom2/tests/test_chunk.py @@ -78,35 +78,35 @@ class TestEnums(unittest.TestCase): def test_all(self): # test for invalid value with self.assertRaises(ValueError): - chunk.ProductType("no_such_string") + chunk.DataLinkSemantics("no_such_string") with self.assertRaises(ValueError): - chunk.ProductType(None) + chunk.DataLinkSemantics(None) with self.assertRaises(ValueError): - chunk.ProductType(1) + chunk.DataLinkSemantics(1) # test that we can get the object for each enum by name - self.assertEqual(chunk.ProductType.SCIENCE.name, "SCIENCE") - self.assertEqual(chunk.ProductType[ - chunk.ProductType.SCIENCE.name].name, "SCIENCE") - self.assertEqual(chunk.ProductType['SCIENCE'].value, "science") - self.assertEqual(chunk.ProductType[ - chunk.ProductType.SCIENCE.name].value, "science") - - self.assertEqual(chunk.ProductType.THIS.value, "this") - self.assertEqual(chunk.ProductType.SCIENCE.value, "science") - self.assertEqual(chunk.ProductType.CALIBRATION.value, "calibration") - self.assertEqual(chunk.ProductType.PREVIEW.value, "preview") - self.assertEqual(chunk.ProductType.NOISE.value, "noise") - self.assertEqual(chunk.ProductType.WEIGHT.value, "weight") - self.assertEqual(chunk.ProductType.AUXILIARY.value, "auxiliary") - self.assertEqual(chunk.ProductType.THUMBNAIL.value, "thumbnail") - self.assertEqual(chunk.ProductType.BIAS.value, "bias") - self.assertEqual(chunk.ProductType.DARK.value, "dark") - self.assertEqual(chunk.ProductType.FLAT.value, "flat") - self.assertEqual(chunk.ProductType.CODERIVED.value, "coderived") - self.assertEqual(chunk.ProductType.DOCUMENTATION.value, "documentation") - self.assertEqual(chunk.ProductType.PREVIEW_IMAGE.value, "preview-image") - self.assertEqual(chunk.ProductType.PREVIEW_PLOT.value, "preview-plot") + self.assertEqual(chunk.DataLinkSemantics.SCIENCE.name, "SCIENCE") + self.assertEqual(chunk.DataLinkSemantics[ + chunk.DataLinkSemantics.SCIENCE.name].name, "SCIENCE") + self.assertEqual(chunk.DataLinkSemantics['SCIENCE'].value, "science") + self.assertEqual(chunk.DataLinkSemantics[ + chunk.DataLinkSemantics.SCIENCE.name].value, "science") + + self.assertEqual(chunk.DataLinkSemantics.THIS.value, "this") + self.assertEqual(chunk.DataLinkSemantics.SCIENCE.value, "science") + self.assertEqual(chunk.DataLinkSemantics.CALIBRATION.value, "calibration") + self.assertEqual(chunk.DataLinkSemantics.PREVIEW.value, "preview") + self.assertEqual(chunk.DataLinkSemantics.NOISE.value, "noise") + self.assertEqual(chunk.DataLinkSemantics.WEIGHT.value, "weight") + self.assertEqual(chunk.DataLinkSemantics.AUXILIARY.value, "auxiliary") + self.assertEqual(chunk.DataLinkSemantics.THUMBNAIL.value, "thumbnail") + self.assertEqual(chunk.DataLinkSemantics.BIAS.value, "bias") + self.assertEqual(chunk.DataLinkSemantics.DARK.value, "dark") + self.assertEqual(chunk.DataLinkSemantics.FLAT.value, "flat") + self.assertEqual(chunk.DataLinkSemantics.CODERIVED.value, "coderived") + self.assertEqual(chunk.DataLinkSemantics.DOCUMENTATION.value, "documentation") + self.assertEqual(chunk.DataLinkSemantics.PREVIEW_IMAGE.value, "preview-image") + self.assertEqual(chunk.DataLinkSemantics.PREVIEW_PLOT.value, "preview-plot") class TestChunk(unittest.TestCase): @@ -145,8 +145,8 @@ def test_attributes(self): test_chunk.polarization = float(1.0) test_chunk.custom = float(1.0) - test_chunk.product_type = chunk.ProductType.SCIENCE - self.assertEqual(chunk.ProductType.SCIENCE.name, + test_chunk.product_type = chunk.DataLinkSemantics.SCIENCE + self.assertEqual(chunk.DataLinkSemantics.SCIENCE.name, test_chunk.product_type.name) test_chunk.naxis = int(5) diff --git a/caom2/caom2/tests/test_common.py b/caom2/caom2/tests/test_common.py index 52d67f91..cc298e23 100644 --- a/caom2/caom2/tests/test_common.py +++ b/caom2/caom2/tests/test_common.py @@ -85,7 +85,7 @@ def test_all(self): test_entity = common.AbstractCaomEntity() print(test_entity._id, test_entity._last_modified) test_artifact = artifact.Artifact("caom2:/blah/blah", - chunk.ProductType.SCIENCE, + chunk.DataLinkSemantics.SCIENCE, artifact.ReleaseType.DATA) print(test_artifact._id, test_artifact._last_modified) @@ -93,13 +93,14 @@ def test_all(self): print(test_chunk._id, test_chunk._last_modified) algorithm = observation.Algorithm("myAlg") - test_observation = observation.Observation("colect", "obs", algorithm) + test_observation = observation.Observation("colect", "caom:COLLECTION/obs", algorithm) print(test_observation._id, test_observation._last_modified) test_part = part.Part("part") print(test_part._id, test_part._last_modified) - test_plane = plane.Plane("prodid") + plane_uri = '{}/{}'.format(test_observation.uri.uri, "obs") + test_plane = plane.Plane(plane_uri) print(test_plane._id, test_plane._last_modified) self.assertIsNone(test_plane.last_modified, "last_modified null") @@ -132,7 +133,7 @@ def test_all(self): test_entity = common.AbstractCaomEntity() print(test_entity._id, test_entity._last_modified) test_artifact = artifact.Artifact("caom2:/blah/blah", - chunk.ProductType.SCIENCE, + chunk.DataLinkSemantics.SCIENCE, artifact.ReleaseType.DATA) with self.assertRaises(NotImplementedError): test_artifact.compute_meta_checksum() diff --git a/caom2/caom2/tests/test_dali.py b/caom2/caom2/tests/test_dali.py new file mode 100644 index 00000000..bfa0f79a --- /dev/null +++ b/caom2/caom2/tests/test_dali.py @@ -0,0 +1,110 @@ +# *********************************************************************** +# ****************** CANADIAN ASTRONOMY DATA CENTRE ******************* +# ************* CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** +# +# (c) 2025. (c) 2025. +# Government of Canada Gouvernement du Canada +# National Research Council Conseil national de recherches +# Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 +# All rights reserved Tous droits réservés +# +# NRC disclaims any warranties, Le CNRC dénie toute garantie +# expressed, implied, or énoncée, implicite ou légale, +# statutory, of any kind with de quelque nature que ce +# respect to the software, soit, concernant le logiciel, +# including without limitation y compris sans restriction +# any warranty of merchantability toute garantie de valeur +# or fitness for a particular marchande ou de pertinence +# purpose. NRC shall not be pour un usage particulier. +# liable in any event for any Le CNRC ne pourra en aucun cas +# damages, whether direct or être tenu responsable de tout +# indirect, special or general, dommage, direct ou indirect, +# consequential or incidental, particulier ou général, +# arising from the use of the accessoire ou fortuit, résultant +# software. Neither the name de l'utilisation du logiciel. Ni +# of the National Research le nom du Conseil National de +# Council of Canada nor the Recherches du Canada ni les noms +# names of its contributors may de ses participants ne peuvent +# be used to endorse or promote être utilisés pour approuver ou +# products derived from this promouvoir les produits dérivés +# software without specific prior de ce logiciel sans autorisation +# written permission. préalable et particulière +# par écrit. +# +# This file is part of the Ce fichier fait partie du projet +# OpenCADC project. OpenCADC. +# +# OpenCADC is free software: OpenCADC est un logiciel libre ; +# you can redistribute it and/or vous pouvez le redistribuer ou le +# modify it under the terms of modifier suivant les termes de +# the GNU Affero General Public la “GNU Affero General Public +# License as published by the License” telle que publiée +# Free Software Foundation, par la Free Software Foundation +# either version 3 of the : soit la version 3 de cette +# License, or (at your option) licence, soit (à votre gré) +# any later version. toute version ultérieure. +# +# OpenCADC is distributed in the OpenCADC est distribué +# hope that it will be useful, dans l’espoir qu’il vous +# but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE +# without even the implied GARANTIE : sans même la garantie +# warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ +# or FITNESS FOR A PARTICULAR ni d’ADÉQUATION À UN OBJECTIF +# PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence +# General Public License for Générale Publique GNU Affero +# more details. pour plus de détails. +# +# You should have received Vous devriez avoir reçu une +# a copy of the GNU Affero copie de la Licence Générale +# General Public License along Publique GNU Affero avec +# with OpenCADC. If not, see OpenCADC ; si ce n’est +# . pas le cas, consultez : +# . +# +# $Revision: 4 $ +# +# *********************************************************************** +# + +import math +import pytest +import unittest + +from .. import shape +from .. import dali + + +class TestInterval(unittest.TestCase): + def test_all(self): + + lower = 1.0 + upper = 2.0 + self.assertRaises(TypeError, dali.Interval, None, None) + self.assertRaises(TypeError, dali.Interval, None, 1.0) + self.assertRaises(TypeError, dali.Interval, 1.0, None) + self.assertRaises(TypeError, dali.Interval, None, "string") + self.assertRaises(TypeError, dali.Interval, "string", None) + self.assertRaises(TypeError, dali.Interval, "string1", "string2") + # validate errors + self.assertRaises(ValueError, dali.Interval, upper, lower) + + # test cannot set interval with upper < lower + interval = dali.Interval(lower, upper) + with self.assertRaises(ValueError): + interval.upper = 0.5 + # test instance methods + i1 = dali.Interval(10.0, 15.0) + self.assertEqual(i1.get_width(), 5) + + # test class methods + i1 = dali.Interval(10.0, 15.0) + i2 = dali.Interval(5.0, 8.0) + intersect1 = dali.Interval.intersection(i1, i2) + self.assertEqual(intersect1, None) + intersect2 = dali.Interval.intersection(i2, i1) + self.assertEqual(intersect2, None) + i3 = dali.Interval(8.0, 12.0) + lb = max(i1.lower, i3.lower) + ub = min(i1.upper, i3.upper) + intersect3 = dali.Interval.intersection(i1, i3) + self.assertEqual(intersect3, dali.Interval(lb, ub)) diff --git a/caom2/caom2/tests/test_diffs.py b/caom2/caom2/tests/test_diffs.py index d14e8adb..afaf405e 100644 --- a/caom2/caom2/tests/test_diffs.py +++ b/caom2/caom2/tests/test_diffs.py @@ -2,7 +2,7 @@ # ****************** CANADIAN ASTRONOMY DATA CENTRE ******************* # ************* CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** # -# (c) 2022. (c) 2022. +# (c) 2025. (c) 2025. # Government of Canada Gouvernement du Canada # National Research Council Conseil national de recherches # Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 @@ -69,7 +69,7 @@ import os import unittest -from caom2 import Point, shape, Vertex, SegmentType, Position +from caom2 import Point, shape, SegmentType, Position, MultiShape from .. import diff from .. import observation @@ -81,7 +81,7 @@ class TestCaomUtil(unittest.TestCase): def test_get_differences(self): expected_simple = observation.SimpleObservation( collection='test_collection', - observation_id='test_observation_id', + uri='caom:test_collection/test_observation_id', algorithm=observation.Algorithm('EXPOSURE')) # meta_producer field is ignored by get_difference expected_simple.meta_producer = 'meta_producer/0.1' @@ -91,22 +91,23 @@ def test_get_differences(self): actual_simple = observation.SimpleObservation( collection='test_collection', - observation_id='test_observation_id', + uri='caom:test_collection/test_observation_id', algorithm=observation.Algorithm('EXPOSURE')) report = diff.get_differences(expected_simple, actual_simple, 'obs') self.assertTrue(report is None, repr(report)) - - act_plane = observation.Plane(product_id='test_product_id1') - actual_simple.planes['test_product_id1'] = act_plane + puri = 'caom:TEST/TESTOBS/test_plane1' + act_plane = observation.Plane(puri) + actual_simple.planes[act_plane.uri] = act_plane report = diff.get_differences(expected_simple, actual_simple, 'obs') self.assertTrue(report is not None, repr(report)) self.assertTrue(len(report) == 1, repr(report)) - ex_plane = observation.Plane(product_id='test_product_id2') - expected_simple.planes['test_product_id2'] = ex_plane + puri2 = 'caom:TEST/TESTOBS/test_plane2' + ex_plane = observation.Plane(uri=puri2) + expected_simple.planes[ex_plane.uri] = ex_plane report = diff.get_differences(expected_simple, actual_simple, 'obs') self.assertTrue(report is not None, repr(report)) @@ -163,22 +164,11 @@ def test_plane_level_position(self): Point(cval1=100.25, cval2=30.0), Point(cval1=float('nan'), cval2=float('nan'))] - v1 = [Vertex(p1[0].cval1, p1[0].cval2, SegmentType.MOVE), - Vertex(p1[1].cval1, p1[1].cval2, SegmentType.LINE), - Vertex(p1[2].cval1, p1[2].cval2, SegmentType.LINE), - Vertex(p1[3].cval1, p1[3].cval2, SegmentType.LINE), - Vertex(p1[0].cval1, p1[0].cval2, SegmentType.CLOSE)] - v2 = [Vertex(p2[0].cval1, p2[0].cval2, SegmentType.MOVE), - Vertex(p2[1].cval1, p2[1].cval2, SegmentType.LINE), - Vertex(p2[2].cval1, p2[2].cval2, SegmentType.LINE), - Vertex(p2[3].cval1, p2[3].cval2, SegmentType.LINE), - Vertex(p2[0].cval1, p2[0].cval2, SegmentType.CLOSE)] - - poly1 = shape.Polygon(points=p1, samples=shape.MultiPolygon(v1)) - poly2 = shape.Polygon(points=p2, samples=shape.MultiPolygon(v2)) - - o1 = Position(time_dependent=False, bounds=poly1) - o2 = Position(time_dependent=False, bounds=poly2) + poly1 = shape.Polygon(points=p1) + poly2 = shape.Polygon(points=p2) + + o1 = Position(bounds=poly1, samples=MultiShape([poly1]), time_dependent=False) + o2 = Position(bounds=poly2, samples=MultiShape([poly2]), time_dependent=False) report = diff.get_differences(o1, o2, 'caom test instances') assert report is None, 'NaN comparison failure' diff --git a/caom2/caom2/tests/test_obs_reader_writer.py b/caom2/caom2/tests/test_obs_reader_writer.py index 528b8ca1..312d92e6 100644 --- a/caom2/caom2/tests/test_obs_reader_writer.py +++ b/caom2/caom2/tests/test_obs_reader_writer.py @@ -2,7 +2,7 @@ # ****************** CANADIAN ASTRONOMY DATA CENTRE ******************* # ************* CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** # -# (c) 2022. (c) 2022. +# (c) 2025. (c) 2025. # Government of Canada Gouvernement du Canada # National Research Council Conseil national de recherches # Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 @@ -77,6 +77,8 @@ from . import caom_test_instances from .xml_compare import xml_compare +from .. import caom_util +from .. import dali from .. import obs_reader_writer from .. import observation from .. import plane @@ -130,9 +132,9 @@ def complete_derived(depth, bounds_is_circle, version, short_uuid=False): class TestObservationReaderWriter(unittest.TestCase): def test_invalid_long_id(self): - simple_observation = minimal_simple(1, False, 22) + simple_observation = minimal_simple(1, False, 23) writer = obs_reader_writer.ObservationWriter( - False, False, "caom2", obs_reader_writer.CAOM22_NAMESPACE) + False, False, "caom2", obs_reader_writer.CAOM23_NAMESPACE) output = BytesIO() writer.write(simple_observation, output) xml = output.getvalue() @@ -149,9 +151,9 @@ def test_invalid_long_id(self): pass def test_invalid_uuid(self): - simple_observation = minimal_simple(1, False, 22) + simple_observation = minimal_simple(1, False, 23) writer = obs_reader_writer.ObservationWriter(False, - False) # default is 2.1 + False) output = BytesIO() writer.write(simple_observation, output) xml = output.getvalue() @@ -168,7 +170,7 @@ def test_invalid_uuid(self): pass def test_complete_simple(self): - for version in (22, 23, 24): + for version in (23, 24): # TODO 25 for i in range(1, 6): print("Test Complete Simple {} version {}".format(i, version)) # CoordBounds2D as CoordCircle2D @@ -186,7 +188,7 @@ def test_complete_simple(self): def test_minimal_derived(self): # * composite for the pre-2.4 versions - for version in (22, 23, 24): + for version in (23, 24): # TODO 25 for i in range(1, 6): if version >= 24: print("Test Minimal Derived {} version {}". @@ -213,7 +215,7 @@ def test_minimal_derived(self): def test_complete_derived(self): # * formerly known as composite - for version in (22, 23, 24): + for version in (23, 24): # TODO 25 for i in range(1, 6): if version >= 24: print("Test Complete Derived {} version {}". @@ -239,31 +241,17 @@ def test_complete_derived(self): version) def test_versions(self): - derived_observation = complete_derived(6, True, 22) - complete_derived(6, True, 22, short_uuid=True) - print("check 2.2 schema with 2.2 doc") - self.observation_test(derived_observation, True, True, 22) - print("check 2.3 schema with 2.2 doc") - self.observation_test(derived_observation, True, True, 23) - print("check 2.4 schema with 2.2 doc") - self.observation_test(derived_observation, True, True, 24) + derived_observation = complete_derived(6, True, 23) + complete_derived(6, True, 23, short_uuid=True) derived_observation = complete_derived(6, True, 23) complete_derived(6, True, 23, short_uuid=True) - print("check 2.2 schema with 2.3 doc") - with self.assertRaises(AttributeError): - # creator ID and shape cannot be serialized in v22. - self.observation_test(derived_observation, True, True, 22) print("check 2.3 schema with 2.3 doc") self.observation_test(derived_observation, True, True, 23) print("check 2.4 schema with 2.3 doc") self.observation_test(derived_observation, True, True, 24) derived_observation = complete_derived(6, True, 24) - print("check 2.2 schema with 2.4 doc") - with self.assertRaises(AttributeError): - # creator ID and shape cannot be serialized in v22. - self.observation_test(derived_observation, True, True, 22) print("check 2.3 schema with 2.4 doc") with self.assertRaises(AttributeError): self.observation_test(derived_observation, True, True, 23) @@ -291,25 +279,22 @@ def test_versions(self): self.observation_test(derived_observation, True, True, 23) - # remove shape and creator IDs ad retest with v22 - for p in derived_observation.planes.values(): - p.position.bounds = None - p.creator_id = None - self.observation_test(derived_observation, True, True, 22) - def observation_test(self, obs, validate, write_empty_collections, version): - if version == 22: + if version == 23: writer = obs_reader_writer.ObservationWriter( validate, write_empty_collections, "caom2", - obs_reader_writer.CAOM22_NAMESPACE) - elif version == 23: + obs_reader_writer.CAOM23_NAMESPACE) + elif version == 24: writer = obs_reader_writer.ObservationWriter( validate, write_empty_collections, "caom2", - obs_reader_writer.CAOM23_NAMESPACE) - else: + obs_reader_writer.CAOM24_NAMESPACE) + elif version == 25: writer = obs_reader_writer.ObservationWriter( - validate, write_empty_collections) + validate, write_empty_collections, "caom2", + obs_reader_writer.CAOM25_NAMESPACE) + else: + raise ValueError("Unsupported version {}".format(version)) xml_file = open('/tmp/test.xml', 'wb') writer.write(obs, xml_file) xml_file.close() @@ -338,9 +323,9 @@ def compare_observations(self, expected, actual, version): self.assertIsNotNone(actual.collection) self.assertEqual(expected.collection, actual.collection) - self.assertIsNotNone(expected.observation_id) - self.assertIsNotNone(actual.observation_id) - self.assertEqual(expected.observation_id, actual.observation_id) + self.assertIsNotNone(expected.uri) + self.assertIsNotNone(actual.uri) + self.assertEqual(expected.uri, actual.uri) self.assertIsNotNone(expected._id) self.assertIsNotNone(actual._id) @@ -420,6 +405,7 @@ def compare_telescope(self, expected, actual): self.assertEqual(expected.geo_location_z, actual.geo_location_z) for keyword in expected.keywords: self.assertTrue(keyword in actual.keywords) + self.assertEqual(expected.tracking_mode, actual.tracking_mode) def compare_instrument(self, expected, actual): if expected is None and actual is None: @@ -459,7 +445,7 @@ def compare_observation_uri(self, expected, actual): self.assertIsNotNone(actual) self.assertEqual(expected.uri, actual.uri) self.assertEqual(expected.collection, actual.collection) - self.assertEqual(expected.observation_id, actual.observation_id) + self.assertEqual(expected.uri, actual.uri) def compare_requirements(self, expected, actual): if expected is None and actual is None: @@ -480,16 +466,12 @@ def compare_planes(self, expected, actual, version): actual_plane = actual[key] self.assertIsNotNone(expected_plane) self.assertIsNotNone(actual_plane) - self.assertEqual(expected_plane.product_id, - actual_plane.product_id) + self.assertEqual(expected_plane.uri, + actual_plane.uri) self.assertIsNotNone(expected_plane._id) self.assertIsNotNone(actual_plane._id) self.assertEqual(expected_plane._id, actual_plane._id) - self.assertEqual( - expected_plane.creator_id, actual_plane.creator_id, - "creator_id") - self.compare_entity_attributes(expected_plane, actual_plane) self.assertEqual(expected_plane.meta_release, @@ -527,6 +509,14 @@ def compare_position(self, expected, actual): self.assertIsNone(actual, "position") else: self.compare_shape(expected.bounds, actual.bounds) + self.assertTrue(isinstance(expected.samples, shape.MultiShape), + "mismatched samples" + + actual.__class__.__name__) + self.assertEqual(len(expected.samples.shapes), len(actual.samples.shapes), + "mismatched number of samples") + for index, sample in enumerate(expected.samples.shapes): + # assumes that the shapes are in the same order + self.compare_shape(sample, actual.samples.shapes[index]) self.compare_dimension2d(expected.dimension, actual.dimension) self.assertEqual(expected.resolution, actual.resolution, "resolution") @@ -541,6 +531,7 @@ def compare_energy(self, expected, actual): self.assertIsNone(actual, "energy") else: self.compare_interval(expected.bounds, actual.bounds) + self.compare_samples(expected.samples, actual.samples) self.assertEqual(expected.dimension, actual.dimension, "dimension") self.assertEqual(expected.resolving_power, actual.resolving_power, "resolving_power") @@ -559,6 +550,7 @@ def compare_time(self, expected, actual): self.assertIsNone(actual, "time") else: self.compare_interval(expected.bounds, actual.bounds) + self.compare_samples(expected.samples, actual.samples) self.assertEqual(expected.dimension, actual.dimension, "dimension") self.assertEqual(expected.resolution, actual.resolution, "resolution") @@ -588,6 +580,7 @@ def compare_custom(self, expected, actual): else: self.assertEqual(expected.ctype, actual.ctype, 'ctype') self.compare_interval(expected.bounds, actual.bounds) + self.compare_samples(expected.samples, actual.samples) self.assertEqual(expected.dimension, actual.dimension, "dimension") def compare_shape(self, expected, actual): @@ -605,18 +598,6 @@ def compare_shape(self, expected, actual): "different number of points") for index, point in enumerate(expected_points): self.compare_point(point, actual_points[index]) - actual_samples = actual.samples - self.assertIsNotNone(actual_samples, "shape is None") - self.assertTrue(isinstance(actual_samples, shape.MultiPolygon), - "mismatched shapes" + - actual.__class__.__name__) - expected_samples = expected.samples - expected_vertices = expected_samples.vertices - actual_vertices = actual_samples.vertices - self.assertEqual(len(expected_vertices), len(actual_vertices), - "different number of vertices") - for index, vertex in enumerate(expected_vertices): - self.compare_vertices(vertex, actual_vertices[index]) elif isinstance(expected, shape.Circle): self.assertIsNotNone(actual, "shape is None") self.assertTrue(isinstance(actual, shape.Circle), @@ -638,13 +619,12 @@ def compare_interval(self, expected, actual): else: self.assertEqual(expected.lower, actual.lower, "lower") self.assertEqual(expected.upper, actual.upper, "upper") - if expected.samples is None: - self.assertIsNone(actual.samples, "samples") - else: - self.assertEqual(len(actual.samples), len(expected.samples), - "samples") - for index, sample in enumerate(expected.samples): - self.compare_sub_interval(sample, actual.samples[index]) + + def compare_samples(self, expected, actual): + self.assertEqual(len(actual), len(expected), + "samples") + for index, sample in enumerate(expected): + self.compare_sub_interval(sample, actual[index]) def compare_sub_interval(self, expected, actual): if expected is None: @@ -1082,11 +1062,11 @@ def compare_entity_attributes(self, expected, actual): def test_roundtrip_floats(self): """ - Tests floats precission in a round trip + Tests floats precision in a round trip """ expected_obs = observation.SimpleObservation( - "TEST_COLLECTION", "33", "ALG") + "TEST_COLLECTION", "caom:TEST_COLLECTION/33", "ALG") writer = obs_reader_writer.ObservationWriter( True, False, "caom2", obs_reader_writer.CAOM23_NAMESPACE) @@ -1095,9 +1075,10 @@ def test_roundtrip_floats(self): shape.Point(-0.00518884856598203, -0.00518884856598), 'test') # create empty energy - pl = plane.Plane('productID') - pl.energy = plane.Energy(sample_size=2.0) - expected_obs.planes['productID'] = pl + plane_uri = '{}/{}'.format(expected_obs.uri.uri, 'planeID') + pl = plane.Plane(plane_uri) + pl.energy = plane.Energy(dali.Interval(1.0, 2.0), [dali.Interval(1.0, 2.0)]) + expected_obs.planes[pl.uri] = pl tmpfile = tempfile.TemporaryFile() writer.write(expected_obs, tmpfile) @@ -1147,21 +1128,22 @@ def test_round_trip(self): 'No XML files in test data directory') reader = obs_reader_writer.ObservationReader(True) - writer22 = obs_reader_writer.ObservationWriter( - True, False, "caom2", obs_reader_writer.CAOM22_NAMESPACE) writer23 = obs_reader_writer.ObservationWriter( True, False, "caom2", obs_reader_writer.CAOM23_NAMESPACE) writer24 = obs_reader_writer.ObservationWriter( True, False, "caom2", obs_reader_writer.CAOM24_NAMESPACE) + writer25 = obs_reader_writer.ObservationWriter( + True, False, "caom2", obs_reader_writer.CAOM25_NAMESPACE) for filename in files: - if filename.endswith("CAOM-2.4.xml"): + if filename.endswith("CAOM-2.5.xml"): + print("test: {}".format(filename)) + self.do_test(reader, writer25, filename) + elif filename.endswith("CAOM-2.4.xml"): print("test: {}".format(filename)) self.do_test(reader, writer24, filename) - elif filename.endswith("CAOM-2.3.xml"): + else: print("test: {}".format(filename)) self.do_test(reader, writer23, filename) - else: - self.do_test(reader, writer22, filename) except Exception: raise @@ -1172,7 +1154,7 @@ class TestSchemaValidator(unittest.TestCase): def _create_observation(self): obs = observation.SimpleObservation( - "SCHEMA_VALIDATOR_COLLECTION", "schemaValidatorObsID") + "SCHEMA_VALIDATOR_COLLECTION", "caom:SCHEMA_VALIDATOR_COLLECTION/schemaValidatorObsID") obs.intent = observation.ObservationIntentType.SCIENCE return obs diff --git a/caom2/caom2/tests/test_observation.py b/caom2/caom2/tests/test_observation.py index bd8f5533..b12bb06f 100644 --- a/caom2/caom2/tests/test_observation.py +++ b/caom2/caom2/tests/test_observation.py @@ -2,7 +2,7 @@ # ****************** CANADIAN ASTRONOMY DATA CENTRE ******************* # ************* CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** # -# (c) 2022. (c) 2022. +# (c) 2025. (c) 2025. # Government of Canada Gouvernement du Canada # National Research Council Conseil national de recherches # Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 @@ -117,9 +117,9 @@ def test_all(self): class TestObservation(unittest.TestCase): def test_all(self): algorithm = observation.Algorithm("myAlg") - obs = observation.Observation("GSA", "A12345", algorithm) + obs = observation.Observation("GSA", "caom:GSA/A12345", algorithm) self.assertEqual("GSA", obs.collection, "Collection") - self.assertEqual("A12345", obs.observation_id, "Observation ID") + self.assertEqual(observation.ObservationURI("caom:GSA/A12345"), obs.uri, "Observation URI") self.assertEqual(algorithm, obs.algorithm, "Algorithm") new_algorithm = observation.Algorithm("myNewAlg") @@ -183,27 +183,29 @@ def test_all(self): obs.meta_release, "Metadata release") self.assertEqual(0, len(obs.planes), "Default planes") - plane1 = plane.Plane("myPlaneID") - obs.planes["myPlaneID"] = plane1 + puri1 = "caom:TEST/TESTOBS/myPlaneID" + plane1 = plane.Plane(puri1) + obs.planes[plane1.uri] = plane1 self.assertEqual(1, len(obs.planes), "Planes") - self.assertTrue("myPlaneID" in obs.planes.keys()) + self.assertTrue(plane1.uri in obs.planes.keys()) - plane2 = plane.Plane("myPlaneID2") - obs.planes["myPlaneID2"] = plane2 + puri2 = "caom:TEST/TESTOBS/myPlaneID2" + plane2 = plane.Plane(puri2) + obs.planes[plane2.uri] = plane2 self.assertEqual(2, len(obs.planes), "Planes") - self.assertTrue("myPlaneID" in obs.planes) - self.assertTrue("myPlaneID2" in obs.planes.keys()) + self.assertTrue(plane1.uri in obs.planes) + self.assertTrue(plane2.uri in obs.planes.keys()) # test duplicates - plane3 = plane.Plane("myPlaneID2") - obs.planes["myPlaneID2"] = plane3 + plane3 = plane.Plane(puri2) + obs.planes[plane2.uri] = plane3 self.assertEqual(2, len(obs.planes), "Planes") - self.assertTrue("myPlaneID" in obs.planes) - self.assertTrue("myPlaneID2" in obs.planes.keys()) + self.assertTrue(plane3.uri in obs.planes) + self.assertTrue(plane3.uri in obs.planes.keys()) observation.Observation( obs.collection, - obs.observation_id, + obs.uri.uri, obs.algorithm, planes=obs.planes, sequence_number=obs.sequence_number, @@ -222,9 +224,9 @@ class TestSimpleObservation(unittest.TestCase): def test_all(self): algorithm = observation.Algorithm( observation.SimpleObservation._DEFAULT_ALGORITHM_NAME) - obs = observation.SimpleObservation("GSA", "A12345") + obs = observation.SimpleObservation("GSA", "caom:GSA/A12345") self.assertEqual("GSA", obs.collection, "Collection") - self.assertEqual("A12345", obs.observation_id, "Observation ID") + self.assertEqual(observation.ObservationURI("caom:GSA/A12345"), obs.uri, "Observation URI") self.assertEqual(algorithm, obs.algorithm, "Algorithm") obs.algorithm = algorithm @@ -300,7 +302,7 @@ def test_all(self): # Test the complete constructor def test_complete_init(self): collection = "CFHT" - observation_id = "543210" + uri = "caom:CFHT/543210" algorithm = observation.Algorithm( observation.SimpleObservation._DEFAULT_ALGORITHM_NAME) sequence_number = int(3) @@ -316,7 +318,7 @@ def test_complete_init(self): obs = observation.SimpleObservation( collection, - observation_id, + uri, algorithm, sequence_number, intent, @@ -334,8 +336,8 @@ def test_complete_init(self): self.assertIsNotNone(obs.collection, "Collection") self.assertEqual(collection, obs.collection, "Collection") - self.assertIsNotNone(obs.observation_id, "Observation ID") - self.assertEqual(observation_id, obs.observation_id, "Observation ID") + self.assertIsNotNone(obs.uri, "Observation URI") + self.assertEqual(observation.ObservationURI(uri), obs.uri, "Observation URI") self.assertIsNotNone(obs.algorithm, "Algorithm") self.assertEqual(algorithm, obs.algorithm, "Algorithm") @@ -374,9 +376,9 @@ def test_complete_init(self): class TestCompositeObservation(unittest.TestCase): def test_all(self): algorithm = observation.Algorithm("mozaic") - obs = observation.CompositeObservation("GSA", "A12345", algorithm) + obs = observation.CompositeObservation("GSA", "caom:GSA/A12345", algorithm) self.assertEqual("GSA", obs.collection, "Collection") - self.assertEqual("A12345", obs.observation_id, "Observation ID") + self.assertEqual(observation.ObservationURI("caom:GSA/A12345"), obs.uri, "Observation URI") self.assertEqual(algorithm, obs.algorithm, "Algorithm") obs.algorithm = algorithm self.assertEqual(algorithm, obs.algorithm, "Algorithm") @@ -479,7 +481,7 @@ def test_all(self): # Test the complete constructor def test_complete_init(self): collection = "CFHT" - observation_id = "543210" + uri = "caom:CFHT/543210" algorithm = observation.Algorithm("algo") sequence_number = int(3) intent = observation.ObservationIntentType.SCIENCE @@ -496,7 +498,7 @@ def test_complete_init(self): obs = observation.DerivedObservation( collection, - observation_id, + uri, algorithm, sequence_number, intent, @@ -515,8 +517,8 @@ def test_complete_init(self): self.assertIsNotNone(obs.collection, "Collection") self.assertEqual(collection, obs.collection, "Collection") - self.assertIsNotNone(obs.observation_id, "Observation ID") - self.assertEqual(observation_id, obs.observation_id, "Observation ID") + self.assertIsNotNone(obs.uri, "Observation URI") + self.assertEqual(observation.ObservationURI(uri), obs.uri, "Observation URI") self.assertIsNotNone(obs.algorithm, "Algorithm") self.assertEqual(algorithm, obs.algorithm, "Algorithm") diff --git a/caom2/caom2/tests/test_part.py b/caom2/caom2/tests/test_part.py index 98e5286b..c7cd189b 100644 --- a/caom2/caom2/tests/test_part.py +++ b/caom2/caom2/tests/test_part.py @@ -81,8 +81,8 @@ def test_init(self): self.assertIsNone(test_part.product_type) self.assertTrue(len(test_part.chunks) == 0) - test_part.product_type = chunk.ProductType.SCIENCE - self.assertEqual(chunk.ProductType.SCIENCE, test_part.product_type) + test_part.product_type = chunk.DataLinkSemantics.SCIENCE + self.assertEqual(chunk.DataLinkSemantics.SCIENCE, test_part.product_type) test_chunk = chunk.Chunk() test_chunk.naxis = 5 diff --git a/caom2/caom2/tests/test_plane.py b/caom2/caom2/tests/test_plane.py index b0a66f61..e3246435 100644 --- a/caom2/caom2/tests/test_plane.py +++ b/caom2/caom2/tests/test_plane.py @@ -2,7 +2,7 @@ # ****************** CANADIAN ASTRONOMY DATA CENTRE ******************* # ************* CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** # -# (c) 2022. (c) 2022. +# (c) 2025. (c) 2025. # Government of Canada Gouvernement du Canada # National Research Council Conseil national de recherches # Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 @@ -72,9 +72,11 @@ from datetime import datetime from .. import artifact +from .. import caom_util from .. import chunk from .. import observation from .. import plane +from .. import dali from .. import shape from .. import wcs @@ -163,12 +165,8 @@ def test_all(self): class TestPlane(unittest.TestCase): def test_all(self): - test_plane = plane.Plane("ProdID") - self.assertEqual("ProdID", test_plane.product_id, "Product ID") - self.assertEqual(None, test_plane.creator_id, "Creator ID") - test_plane.creator_id = "ivo://cadc.nrc.ca/users?tester" - self.assertEqual("ivo://cadc.nrc.ca/users?tester", - test_plane.creator_id, "Creator ID") + test_plane = plane.Plane("caom:TEST/obs/ProdID") + self.assertEqual(plane.PlaneURI("caom:TEST/obs/ProdID"), test_plane.uri, "Plane URI") self.assertEqual(0, len(test_plane.artifacts), "Default number of artifacts") self.assertIsNone(test_plane.meta_release, "Default meta release date") @@ -220,14 +218,14 @@ def test_all(self): self.assertIsNone(test_plane.polarization, "Default polarization") test_artifact1 = artifact.Artifact("caom:GEMINI/222/333", - chunk.ProductType.SCIENCE, + chunk.DataLinkSemantics.SCIENCE, artifact.ReleaseType.DATA) test_plane.artifacts["caom:GEMINI/222/333"] = test_artifact1 self.assertEqual(1, len(test_plane.artifacts), "Artifacts") self.assertTrue("caom:GEMINI/222/333" in test_plane.artifacts.keys()) test_artifact2 = artifact.Artifact("caom:CFHT/55/66", - chunk.ProductType.SCIENCE, + chunk.DataLinkSemantics.SCIENCE, artifact.ReleaseType.DATA) test_plane.artifacts["caom:CFHT/55/66"] = test_artifact2 self.assertEqual(2, len(test_plane.artifacts), "Artifacts") @@ -236,7 +234,7 @@ def test_all(self): # try to append a duplicate artifact test_artifact3 = artifact.Artifact("caom:GEMINI/222/333", - chunk.ProductType.SCIENCE, + chunk.DataLinkSemantics.SCIENCE, artifact.ReleaseType.DATA) test_plane.artifacts["caom:GEMINI/222/333"] = test_artifact3 self.assertEqual(2, len(test_plane.artifacts), "Artifacts") @@ -296,24 +294,12 @@ def test_all(self): plane_uri = plane.PlaneURI("caom:GEMINI/12345/3333") self.assertEqual("caom:GEMINI/12345/3333", plane_uri.uri, "Plane URI") - self.assertEqual("GEMINI", plane_uri.get_observation_uri().collection, - "Collection") - self.assertEqual("12345", - plane_uri.get_observation_uri().observation_id, - "Observation ID") - self.assertEqual("3333", plane_uri.get_product_id(), "Product ID") plane_uri = plane.PlaneURI.get_plane_uri( observation.ObservationURI("caom:CFHT/654321"), "555") self.assertEqual("caom:CFHT/654321/555", plane_uri.uri, "Observation URI") - self.assertEqual("CFHT", plane_uri.get_observation_uri().collection, - "Collection") - self.assertEqual("654321", - plane_uri.get_observation_uri().observation_id, - "Observation ID") - self.assertEqual("555", plane_uri.get_product_id(), "Product ID") exception = False try: @@ -460,9 +446,10 @@ def test_all(self): class TestPosition(unittest.TestCase): def test_all(self): - position = plane.Position() + circle = shape.Circle(center=shape.Point(1.0, 2.0), radius=3.0) + position = plane.Position(bounds=circle, samples=shape.MultiShape([circle])) - self.assertIsNone(position.bounds, "Default bounds") + self.assertEqual(circle, position.bounds, "Default bounds") # position.bounds = 123 # self.assertEqual(123, position.bounds, "Bounds") self.assertIsNone(position.dimension, "Default dimension") @@ -481,11 +468,12 @@ def test_all(self): class TestEnergy(unittest.TestCase): def test_all(self): - energy = plane.Energy() - self.assertIsNone(energy.bounds, "Default energy bounds") - energy.bounds = shape.Interval(1.0, 2.0) + bounds = dali.Interval(1.0, 2.0) + samples = [dali.Interval(1.1, 1.2), dali.Interval(1.9, 2.0)] + energy = plane.Energy(bounds, samples) self.assertEqual(1.0, energy.bounds.lower, "Energy lower bounds") self.assertEqual(2.0, energy.bounds.upper, "Energy upper bounds") + self.assertEqual(2, len(energy.samples)) self.assertIsNone(energy.dimension, "Default energy dimension") energy.dimension = 1000 self.assertEqual(1000, energy.dimension, "Energy dimension") @@ -551,13 +539,12 @@ def test_all(self): class TestTime(unittest.TestCase): def test_all(self): - time = plane.Time() - self.assertIsNone(time.bounds, "Default bounds") - self.assertIsNone(time.dimension, "Default dimension") - self.assertIsNone(time.resolution, "Default resolution") - self.assertIsNone(time.sample_size, "Default sample size") - self.assertIsNone(time.exposure, "Default exposure") - + bounds = dali.Interval(1.0, 2.0) + samples = [dali.Interval(1.1, 1.2)] + time = plane.Time(bounds, samples) + self.assertEqual(1.0, time.bounds.lower, "Time lower bounds") + self.assertEqual(2.0, time.bounds.upper, "Time upper bounds") + self.assertEqual(1, len(time.samples)) time.dimension = 777 self.assertEqual(777, time.dimension, "Dimension") time.resolution = 77.777 @@ -571,21 +558,23 @@ def test_all(self): class TestCustomAxis(unittest.TestCase): def test_all(self): with self.assertRaises(AttributeError): - plane.CustomAxis(None) - my_axis = plane.CustomAxis('Foo') + plane.CustomAxis(None, None, None) + my_axis = plane.CustomAxis('Foo', dali.Interval(1.0, 2.0), + samples=[dali.Interval(1.0, 2.0)]) self.assertEqual('Foo', my_axis.ctype, 'CTYPE missmatch') - self.assertIsNone(my_axis.bounds, "Default bounds") + self.assertEqual(dali.Interval(1.0, 2.0), my_axis.bounds, "Bounds mismatch") self.assertIsNone(my_axis.dimension, "Default dimension") my_axis.dimension = 777 - my_axis.bounds = shape.Interval(1.0, 2.0) self.assertEqual(777, my_axis.dimension, "Dimension") - self.assertEqual(1.0, my_axis.bounds.lower, "Bounds mismatch") + self.assertEqual(1.0, len(my_axis.samples), "Samples mismatch") self.assertEqual(2.0, my_axis.bounds.upper, "Bounad mismatch") - my_axis = plane.CustomAxis('Blah', bounds=shape.Interval(3.0, 4.0), - dimension=33) + bounds = dali.Interval(1.0, 2.0) + my_axis = plane.CustomAxis('Blah', bounds=dali.Interval(3.0, 4.0), + dimension=33, samples=[bounds]) self.assertEqual('Blah', my_axis.ctype, 'CTYPE missmatch') self.assertEqual(33, my_axis.dimension, 'Dimension missmatch') - self.assertEqual(3.0, my_axis.bounds.lower, "Bounds mismatch") - self.assertEqual(4.0, my_axis.bounds.upper, "Bounad mismatch") + self.assertEqual(3.0, my_axis.bounds.lower, "Bounds lower mismatch") + self.assertEqual(4.0, my_axis.bounds.upper, "Bounds upper mismatch") + self.assertEqual(1.0, len(my_axis.samples), "Samples mismatch") diff --git a/caom2/caom2/tests/test_shape.py b/caom2/caom2/tests/test_shape.py index 3ced4b11..f3a8ec2c 100644 --- a/caom2/caom2/tests/test_shape.py +++ b/caom2/caom2/tests/test_shape.py @@ -2,7 +2,7 @@ # ****************** CANADIAN ASTRONOMY DATA CENTRE ******************* # ************* CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** # -# (c) 2022. (c) 2022. +# (c) 2025. (c) 2025. # Government of Canada Gouvernement du Canada # National Research Council Conseil national de recherches # Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 @@ -67,33 +67,11 @@ # import math -import pytest import unittest from .. import shape -class TestEnums(unittest.TestCase): - def test_all(self): - # test for invalid key - with self.assertRaises(KeyError): - shape.SegmentType["foo"] - with self.assertRaises(Exception): - shape.SegmentType[None] - with self.assertRaises(Exception): - shape.SegmentType[999] - # test for invalid value - with self.assertRaises(ValueError): - shape.SegmentType("foo") - with self.assertRaises(ValueError): - shape.SegmentType(None) - with self.assertRaises(ValueError): - shape.SegmentType(4) - self.assertEqual(shape.SegmentType.CLOSE.value, 0) - self.assertEqual(shape.SegmentType.LINE.value, 1) - self.assertEqual(shape.SegmentType.MOVE.value, 2) - - class TestBox(unittest.TestCase): def test_all(self): self.assertRaises(TypeError, shape.Box, None, None, None) @@ -138,101 +116,6 @@ def test_all(self): self.assertEqual(circle.get_size(), 2.0 * radius) -class TestInterval(unittest.TestCase): - def test_all(self): - - lower = 1.0 - upper = 2.0 - lower1 = 1.1 - upper1 = 2.1 - lower2 = 1.2 - upper2 = 2.2 - samples = [shape.SubInterval(lower, lower1), - shape.SubInterval(lower2, upper), - shape.SubInterval(upper1, upper2)] - invalid_samples_lower_mismatch = [shape.SubInterval(lower, upper)] - invalid_samples_upper_mismatch = [shape.SubInterval(lower, upper2)] - invalid_samples_middle_bounds_overlap = [ - shape.SubInterval(lower, upper), shape.SubInterval(lower1, upper1)] - - self.assertRaises(TypeError, shape.Interval, None, None, None) - self.assertRaises(TypeError, shape.Interval, None, None, 1.0) - self.assertRaises(TypeError, shape.Interval, None, 1.0, None) - self.assertRaises(TypeError, shape.Interval, 1.0, None, None) - self.assertRaises(TypeError, shape.Interval, None, None, samples) - self.assertRaises(TypeError, shape.Interval, None, int(1), samples) - self.assertRaises(TypeError, shape.Interval, int(1), None, samples) - self.assertRaises(TypeError, shape.Interval, None, "string", samples) - self.assertRaises(TypeError, shape.Interval, "string", None, samples) - self.assertRaises(TypeError, shape.Interval, "string1", "string2", - int(1)) - self.assertRaises(ValueError, shape.Interval, 2.0, 1.0, None) - # validate errors - self.assertRaises(ValueError, shape.Interval, lower, lower, []) - self.assertRaises(ValueError, shape.Interval, lower1, upper, - invalid_samples_lower_mismatch) - self.assertRaises(ValueError, shape.Interval, lower, upper, - invalid_samples_upper_mismatch) - self.assertRaises(ValueError, shape.Interval, lower, upper2, - invalid_samples_middle_bounds_overlap) - - # test cannot set interval with upper < lower - interval = shape.Interval(lower, upper2, samples) - has_assertionError = False - try: - interval.upper = 0.5 - except ValueError: - has_assertionError = True - self.assertEqual(has_assertionError, True) - - # test intervals in samples - actual_samples = interval.samples - - actual_subInterval = actual_samples[0] - expected_subInterval = samples[0] - actual_lower = actual_subInterval.lower - actual_upper = actual_subInterval.upper - expected_lower = expected_subInterval.lower - expected_upper = expected_subInterval.upper - self.assertEqual(actual_lower, expected_lower) - self.assertEqual(actual_upper, expected_upper) - - actual_subInterval = actual_samples[1] - expected_subInterval = samples[1] - actual_lower = actual_subInterval.lower - actual_upper = actual_subInterval.upper - expected_lower = expected_subInterval.lower - expected_upper = expected_subInterval.upper - self.assertEqual(actual_lower, expected_lower) - self.assertEqual(actual_upper, expected_upper) - - actual_subInterval = actual_samples[2] - expected_subInterval = samples[2] - actual_lower = actual_subInterval.lower - actual_upper = actual_subInterval.upper - expected_lower = expected_subInterval.lower - expected_upper = expected_subInterval.upper - self.assertEqual(actual_lower, expected_lower) - self.assertEqual(actual_upper, expected_upper) - - # test instance methods - i1 = shape.Interval(10.0, 15.0) - self.assertEqual(i1.get_width(), 5) - - # test class methods - i1 = shape.Interval(10.0, 15.0) - i2 = shape.Interval(5.0, 8.0) - intersec1 = shape.Interval.intersection(i1, i2) - self.assertEqual(intersec1, None) - intersec2 = shape.Interval.intersection(i2, i1) - self.assertEqual(intersec2, None) - i3 = shape.Interval(8.0, 12.0) - lb = max(i1.lower, i3.lower) - ub = min(i1.upper, i3.upper) - intersec3 = shape.Interval.intersection(i1, i3) - self.assertEqual(intersec3, shape.Interval(lb, ub)) - - class TestPoint(unittest.TestCase): def test_all(self): self.assertRaises(TypeError, shape.Point, None, None) @@ -246,49 +129,3 @@ def test_all(self): self.assertEqual(point.cval2, 2.0) -class TestSubInterval(unittest.TestCase): - def test_all(self): - - self.assertRaises(TypeError, shape.SubInterval, None, None) - self.assertRaises(TypeError, shape.SubInterval, None, 1.0) - self.assertRaises(TypeError, shape.SubInterval, 1.0, None) - self.assertRaises(TypeError, shape.SubInterval, "string1", "string2") - self.assertRaises(ValueError, shape.SubInterval, 2.0, 1.0) - - # test cannot set subInterval with upper < lower - subInterval = shape.SubInterval(1.0, 2.0) - has_assertionError = False - try: - subInterval.upper = 0.5 - except ValueError: - has_assertionError = True - self.assertEqual(has_assertionError, True) - - # test construction method - shape.SubInterval(10.0, 15.0) - - -class TestVertex(): - def test_all(self): - pytest.raises(TypeError, shape.Vertex, None, None, None) - pytest.raises(TypeError, shape.Vertex, 1.0, 2.0, None) - pytest.raises(TypeError, shape.Vertex, 1.0, 2.0, 1.0) - pytest.raises(TypeError, shape.Vertex, None, None, - shape.SegmentType.LINE) - pytest.raises(TypeError, shape.Vertex, None, 2.0, - shape.SegmentType.LINE) - pytest.raises(TypeError, shape.Vertex, 1.0, None, - shape.SegmentType.LINE) - pytest.raises(TypeError, shape.Vertex, None, "string", - shape.SegmentType.LINE) - pytest.raises(TypeError, shape.Vertex, "string", None, - shape.SegmentType.LINE) - pytest.raises(TypeError, shape.Vertex, None, int(1), - shape.SegmentType.LINE) - pytest.raises(TypeError, shape.Vertex, int(1), None, - shape.SegmentType.LINE) - - vertex = shape.Vertex(1.0, 2.0, shape.SegmentType.LINE) - assert(vertex.cval1 == 1.0) - assert(vertex.cval2 == 2.0) - assert(vertex.type == shape.SegmentType.LINE) From 8e0ac3933da5ca5ddfd97b1d1b1e4dfa9a61e992 Mon Sep 17 00:00:00 2001 From: Adrian Damian Date: Wed, 29 Jan 2025 11:02:09 -0800 Subject: [PATCH 2/6] Before checksum matches --- caom2/caom2/obs_reader_writer.py | 152 ++++++++++++++---- caom2/caom2/plane.py | 149 ++++++++++------- .../data/CompleteCompositeCircle-CAOM-2.3.xml | 1 - .../CompleteCompositePolygon-CAOM-2.3.xml | 1 - .../data/CompleteSimpleCircle-CAOM-2.3.xml | 1 - .../data/CompleteSimplePolygon-CAOM-2.3.xml | 1 - .../data/MinimalCompositeCircle-CAOM-2.3.xml | 1 - .../data/MinimalCompositePolygon-CAOM-2.3.xml | 1 - .../data/MinimalSimpleCircle-CAOM-2.3.xml | 1 - .../data/MinimalSimplePolygon-CAOM-2.3.xml | 1 - .../tests/data/SampleComposite-CAOM-2.3.xml | 4 - .../tests/data/SampleDerived-CAOM-2.4.xml | 4 - ...-caom25.xml => SampleDerived-CAOM-2.5.xml} | 0 .../tests/data/SampleSimple-CAOM-2.3.xml | 2 - caom2/caom2/tests/test_checksum.py | 2 +- caom2/caom2/tests/test_diffs.py | 4 +- caom2/caom2/tests/test_obs_reader_writer.py | 4 +- caom2/caom2/tests/test_plane.py | 3 - 18 files changed, 219 insertions(+), 113 deletions(-) rename caom2/caom2/tests/data/{sample-derived-caom25.xml => SampleDerived-CAOM-2.5.xml} (100%) diff --git a/caom2/caom2/obs_reader_writer.py b/caom2/caom2/obs_reader_writer.py index cd600963..8949ecea 100644 --- a/caom2/caom2/obs_reader_writer.py +++ b/caom2/caom2/obs_reader_writer.py @@ -90,6 +90,8 @@ from . import common import logging +from .plane import CalibrationStatus, Ucd + DATA_PKG = 'data' CAOM22_SCHEMA_FILE = 'CAOM-2.2.xsd' @@ -556,9 +558,14 @@ def _add_members(self, members, parent, ns): """ el = self._get_child_element("members", parent, ns, False) if el is not None: - for member_element in el.iterchildren( - "{" + ns + "}observationURI"): - members.add(observation.ObservationURI(member_element.text)) + if self.version < 25: + for member_element in el.iterchildren( + "{" + ns + "}observationURI"): + members.add(observation.ObservationURI(member_element.text)) + else: + for member_element in el.iterchildren( + "{" + ns + "}member"): + members.add(observation.ObservationURI(member_element.text)) def _add_inputs(self, inputs, parent, ns): """Create PlaneURI objects from an XML representation of the planeURI @@ -650,7 +657,7 @@ def _get_metrics(self, element_tag, parent, ns, required): return metrics def _get_quality(self, element_tag, parent, ns, required): - """Build an Quality object from an XML representation + """Build a Quality object from an XML representation Arguments: elTag : element tag which identifies the element @@ -669,12 +676,32 @@ def _get_quality(self, element_tag, parent, ns, required): data_quality = plane.DataQuality(plane.Quality(flag)) return data_quality + def _get_observable(self, parent, ns): + """Build an Observable object from an XML representation + + Arguments: + parent : element containing the Observale element + ns : namespace of the document + return : a Observable object or None if the document does not contain one + raise : ObservationParsingException + """ + el = self._get_child_element("observable", parent, ns, False) + if el is None: + return None + else: + ucd = self._get_child_text("ucd", el, ns, True) + observable = plane.Observable(Ucd(ucd)) + calib = self._get_child_text("calibration", el, ns, False) + if calib: + observable.calibration = CalibrationStatus(calib) + return observable + def _get_point(self, point, ns, required): """Build an Point object from an XML representation of an Point element. Arguments: - point : the point element element + point : the point element ns : namespace of the document required : indicate whether the element is required return : an Point object @@ -1170,7 +1197,7 @@ def _get_spectral_wcs(self, element_tag, parent, ns, required): Arguments: elTag : element tag which indentifies the element - parent : element containing the position element + parent : element containing the spectral wcs element ns : namespace of the document required : boolean indicating whether the element is required return : a SpectralWCS object or @@ -1211,8 +1238,8 @@ def _get_temporal_wcs(self, element_tag, parent, ns, required): element. Arguments: - elTag : element tag which indentifies the element - parent : element containing the position element + elTag : element tag which identifies the element + parent : element containing the temporal wcs element ns : namespace of the document required : boolean indicating whether the element is required return : a TemporalWCS object or @@ -1243,7 +1270,7 @@ def _get_polarization_wcs(self, element_tag, parent, ns, required): Arguments: elTag : element tag which indentifies the element - parent : element containing the position element + parent : element containing the polarization element ns : namespace of the document required : boolean indicating whether the element is required return : a PolarizationWCS object or @@ -1263,7 +1290,7 @@ def _get_custom_wcs(self, element_tag, parent, ns, required): Arguments: elTag : element tag which indentifies the element - parent : element containing the position element + parent : element containing the custom axis element ns : namespace of the document required : boolean indicating whether the element is required return : a CustomWCS object or @@ -1295,16 +1322,20 @@ def _get_position(self, element_tag, parent, ns, required): return None bounds, samples = self._get_shape("bounds", el, ns, False) pos = plane.Position(bounds=bounds, samples=samples) + min_bounds = self._get_shape("minBounds", el, ns, False) + if min_bounds: + # ignore samples returned by get_shape + pos.min_bounds = min_bounds[0] pos.dimension = self._get_dimension2d("dimension", el, ns, False) + pos.max_recoverable_scale = self._get_interval("maxRecoverableScale", el, ns, False) pos.resolution = self._get_child_text_as_float("resolution", el, ns, False) pos.resolution_bounds = self._get_interval("resolutionBounds", el, ns, False) pos.sample_size = self._get_child_text_as_float("sampleSize", el, ns, False) - if self.version < 25: - pos.time_dependent = self._get_child_text_as_boolean("timeDependent", - el, ns, False) + # pos.time_dependent = self._get_child_text_as_boolean("timeDependent", + # el, ns, False) pos.calibration = self._get_child_text("calibration", el, ns, False) return pos @@ -1344,8 +1375,11 @@ def _get_energy(self, element_tag, parent, ns, required): energy.bandpass_name = \ self._get_child_text("bandpassName", el, ns, False) self._add_energy_bands(energy.energy_bands, el, ns) - energy.restwav = \ - self._get_child_text_as_float("restwav", el, ns, False) + if self.version < 25: + energy.rest = self._get_child_text_as_float("restwav", el, ns, False) + else: + energy.rest = \ + self._get_child_text_as_float("rest", el, ns, False) _transition_el = \ self._get_child_element("transition", el, ns, required) if _transition_el is not None: @@ -1421,6 +1455,23 @@ def _get_custom(self, element_tag, parent, ns, required): self._get_child_text_as_int("dimension", el, ns, False) return custom + def _get_visibility(self, parent, ns): + """Build a Visibility object from an XML representation + + Arguments: + parent : element containing the position element + ns : namespace of the document + return : a Visibility object or None if the document does not contain one + raise : ObservationParsingException + """ + el = self._get_child_element("visibility", parent, ns, False) + if el is None: + return None + distance = self._get_interval("distance", el, ns, True) + de = self._get_child_text_as_float("distributionEccentricity", el, ns, True) + df = self._get_child_text_as_float("distributionFill", el, ns, True) + return plane.Visibility(distance, de, df) + def _get_polarization(self, element_tag, parent, ns, required): """Build a Polarization object from an XML representation of a polarization element. @@ -1752,6 +1803,9 @@ def _add_planes(self, obs, parent, ns): else: _uri = plane.PlaneURI(self._get_child_text("uri", plane_element, ns, True)) _plane = plane.Plane(_uri.uri) + _plane.meta_release = caom_util.str2ivoa( + self._get_child_text("metaRelease", plane_element, ns, + False)) _plane.data_release = caom_util.str2ivoa( self._get_child_text("dataRelease", plane_element, ns, False)) @@ -1777,6 +1831,8 @@ def _add_planes(self, obs, parent, ns): _plane.provenance = \ self._get_provenance("provenance", plane_element, ns, False) + _plane.observable = self._get_observable(plane_element, ns) + _plane.metrics = \ self._get_metrics("metrics", plane_element, ns, False) _plane.quality = \ @@ -1792,6 +1848,7 @@ def _add_planes(self, obs, parent, ns): False) _plane.custom = \ self._get_custom("custom", plane_element, ns, False) + _plane.visibility = self._get_visibility(plane_element, ns) self._add_artifacts(_plane.artifacts, plane_element, ns) self._set_entity_attributes(plane_element, ns, _plane) obs.planes[_plane.uri] = _plane @@ -2111,7 +2168,10 @@ def _add_members_element(self, members, parent): element = self._get_caom_element("members", parent) for member in members: - member_element = self._get_caom_element("observationURI", element) + if self._output_version < 25: + member_element = self._get_caom_element("observationURI", element) + else: + member_element = self._get_caom_element("member", element) member_element.text = member.uri def _add_groups_element(self, name, groups, parent): @@ -2167,6 +2227,7 @@ def _add_planes_element(self, planes, parent): _plane.calibration_level.value, plane_element) self._add_provenance_element(_plane.provenance, plane_element) + self._add_observable_element(_plane.observable, plane_element) self._add_metrics_element(_plane.metrics, plane_element) self._add_quality_element(_plane.quality, plane_element) @@ -2182,15 +2243,34 @@ def _add_planes_element(self, planes, parent): else: self._add_custom_element(_plane.custom, plane_element) + self._add_visibility_element(_plane.visibility, plane_element) + self._add_artifacts_element(_plane.artifacts, plane_element) + def _add_visibility_element(self, visibility, parent): + if visibility is None: + return + + if self._output_version < 25: + raise AttributeError("Attempt to output CAOM2.5 attribute (Plane.visibility) as " + "{} Observation".format(self._output_version)) + + element = self._get_caom_element("visibility", parent) + self._add_interval_element("distance", visibility.distance, element) + self._add_element("distributionEccentricity", visibility.distribution_eccentricity, + element) + self._add_element("distributionFill", visibility.distribution_fill, element) + def _add_position_element(self, position, parent): if position is None: return element = self._get_caom_element("position", parent) self._add_bounds_and_samples(position, element) + if position.min_bounds: + self._add_shape_element("minBounds", element, position.min_bounds) self._add_dimension2d_element("dimension", position.dimension, element) + self._add_interval_element("maxRecoverableScale", position.max_recoverable_scale, element) self._add_element("resolution", position.resolution, element) if self._output_version < 24: if position.resolution_bounds is not None: @@ -2201,14 +2281,6 @@ def _add_position_element(self, position, parent): self._add_interval_element("resolutionBounds", position.resolution_bounds, element) self._add_element("sampleSize", position.sample_size, element) - if position.time_dependent is not None: - if self._output_version < 25: - self._add_boolean_element("timeDependent", position.time_dependent, - element) - else: - raise AttributeError( - "Attempt to write CAOM2.5 element that contains " - "deprecated Position.timeDependent attribute") if position.calibration: if self._output_version < 25: raise AttributeError( @@ -2237,9 +2309,9 @@ def _add_energy_element(self, energy, parent): "Attempt to write CAOM2.4 element " "(energy.resolving_power_bands) as " "CAOM2.3 Observation") - else: - self._add_interval_element("resolvingPowerBounds", - energy.resolving_power_bounds, element) + else: + self._add_interval_element("resolvingPowerBounds", + energy.resolving_power_bounds, element) if energy.resolution is not None: if self._output_version < 25: raise AttributeError( @@ -2273,7 +2345,10 @@ def _add_energy_element(self, energy, parent): eb_element = self._get_caom_element("energyBands", element) for bb in energy.energy_bands: self._add_element("emBand", bb.value, eb_element) - self._add_element("restwav", energy.restwav, element) + if self._output_version < 25: + self._add_element("restwav", energy.rest, element) + else: + self._add_element("rest", energy.rest, element) if energy.transition: transition = self._get_caom_element("transition", element) self._add_element("species", energy.transition.species, transition) @@ -2315,9 +2390,8 @@ def _add_time_element(self, time, parent): raise AttributeError( "Attempt to write CAOM2.5 element " "(time.exposure_bounds) as {} Observation".format(self._output_version)) - else: - self.add_interval_element("exposureBounds", - time.exposure_bounds, element) + else: + self._add_interval_element("exposureBounds", time.exposure_bounds, element) if time.calibration: if self._output_version < 25: raise AttributeError( @@ -2457,6 +2531,20 @@ def _add_quality_element(self, quality, parent): element = self._get_caom_element("quality", parent) self._add_element("flag", quality.flag.value, element) + def _add_observable_element(self, observable, parent): + if observable is None: + return + + element = self._get_caom_element("observable", parent) + self._add_element("ucd", observable.ucd.value, element) + if observable.calibration: + if self._output_version < 25: + raise AttributeError( + "Attempt to write CAOM2.5 element (observable.calibration) as " + "{} Observation".format(self._output_version)) + else: + self._add_element("calibration", observable.calibration, element) + def _add_transition_element(self, transition, parent): if transition is None: return @@ -2527,7 +2615,7 @@ def _add_parts_element(self, parts, parent): self._add_chunks_element(_part.chunks, part_element) def _add_chunks_element(self, chunks, parent): - if chunks is None: + if not chunks: return element = self._get_caom_element("chunks", parent) diff --git a/caom2/caom2/plane.py b/caom2/caom2/plane.py index 1b7d1ade..6d7eadb3 100644 --- a/caom2/caom2/plane.py +++ b/caom2/caom2/plane.py @@ -109,11 +109,12 @@ class CalibrationLevel(Enum): ANALYSIS_PRODUCT = int_32(4) -class Ucd(OrderedEnum): +class Ucd(CaomObject): """ UCD - enum of UCDs""" UCD_VOCAB = "https://ivoa.net/documents/UCD1+/20230125/ucd-list.txt" - # TODO no values yet + def __init__(self, value): + self.value = value class CalibrationStatus(OrderedEnum): @@ -225,22 +226,20 @@ class Quality(Enum): JUNK = VocabularyTerm(_CAOM_DATA_PRODUCT_TYPE_NS, "junk", True).get_value() -class Observable(): +class Observable(CaomObject): """ Observable class""" def __init__(self, ucd, calibration=None): - self.ucd = ucd + if not ucd: + raise ValueError("Observable.ucd cannot be None") + caom_util.type_check(ucd, Ucd, 'ucd') + self._ucd = ucd self.calibration = calibration @property def ucd(self): return self._ucd - @ucd.setter - def ucd(self, value): - caom_util.type_check(value, Ucd, 'ucd', override=False) - self._ucd = value - @property def calibration(self): return self._calibration @@ -254,11 +253,45 @@ def calibration(self, value): self._calibration = None +class Visibility(CaomObject): + + def __init__(self, distance, distribution_eccentricity, distribution_fill): + + if distance is not None: + caom_util.type_check(distance, shape.Interval,'distance') + else: + raise ValueError("Visibility.distance cannot be None") + self._distance = distance + + if distribution_eccentricity is not None: + caom_util.type_check(distribution_eccentricity, float,'distribution_eccentricity') + else: + raise ValueError("Visibility.distribution_eccentricity cannot be None") + self._distribution_eccentricity = distribution_eccentricity + + if distribution_fill is not None: + caom_util.type_check(distribution_fill, float,'distribution_fill') + else: + raise ValueError("Visibility.distribution_fill cannot be None") + self._distribution_fill = distribution_fill + + @property + def distance(self): + return self._distance + + @property + def distribution_eccentricity(self): + return self._distribution_eccentricity + + @property + def distribution_fill(self): + return self._distribution_fill + + class Plane(AbstractCaomEntity): """ Plane class """ def __init__(self, uri, - creator_id=None, # deprecated since 2.5 artifacts=None, meta_release=None, data_release=None, @@ -269,7 +302,8 @@ def __init__(self, uri, provenance=None, metrics=None, quality=None, - observable=None): + observable=None, + visibility=None): """ Initialize a Plane instance @@ -279,7 +313,6 @@ def __init__(self, uri, super(Plane, self).__init__() validate_uri(uri) self._uri = PlaneURI(uri) - self.creator_id = creator_id if artifacts is None: artifacts = caom_util.TypedOrderedDict(Artifact, ) self.artifacts = artifacts @@ -302,6 +335,7 @@ def __init__(self, uri, self._polarization = None self._custom = None self.observable = observable + self.visibility = visibility def _key(self): return self.uri @@ -320,25 +354,6 @@ def uri(self): """ return self._uri - @ property - def creator_id(self): - """A URI that identifies the creator of this plane. - - eg: ivo://cadc.nrc.ca/users?tester - type: URI - """ - return self._creator_id - - @ creator_id.setter - def creator_id(self, value): - caom_util.type_check(value, str, 'creator_id') - if value is not None: - tmp = urlsplit(value) - - if tmp.geturl() != value: - raise ValueError("Invalid URI: " + value) - self._creator_id = value - @property def artifacts(self): """A TypeList of artifacts that are part of this plane. @@ -514,7 +529,7 @@ def observable(self): @observable.setter def observable(self, value): - caom_util.type_check(value, str, 'observable') + caom_util.type_check(value, Observable, 'observable') self._observable = value @property @@ -617,8 +632,18 @@ def compute_polarization(self): "Aggregation of polarization " + "has not been implemented in this module") + @property + def visibility(self): + return self._visibility + + @visibility.setter + def visibility(self, value): + if value: + caom_util.type_check(value, Visibility, "visibility") + self._visibility = value + -#TODO not sure this is needed anymore +# TODO not sure this is needed anymore class PlaneURI(CaomObject): """ Plane URI """ def __init__(self, uri): @@ -959,11 +984,12 @@ class Position(CaomObject): def __init__(self, bounds, samples, + min_bounds=None, dimension=None, + max_recoverable_scale=None, resolution=None, resolution_bounds=None, sample_size=None, - time_dependent=None, # deprecated since 2.5 calibration=None ): """ @@ -983,11 +1009,12 @@ def __init__(self, bounds, raise ValueError("No samples provided") caom_util.type_check(samples, shape.MultiShape, 'samples') self._samples = samples + self.min_bounds = min_bounds self.dimension = dimension + self.max_recoverable_scale = max_recoverable_scale self.resolution = resolution self.resolution_bounds = resolution_bounds self.sample_size = sample_size - self.time_dependent = time_dependent self.calibration = calibration # Properties @@ -1002,6 +1029,19 @@ def samples(self): """ Samples """ return self._samples + @property + def min_bounds(self): + """ Minimum bounds """ + return self._min_bounds + + @min_bounds.setter + def min_bounds(self, value): + if value is not None: + caom_util.type_check(value, + (shape.Box, shape.Circle, shape.Polygon), + 'min_bounds', override=False) + self._min_bounds = value + @property def dimension(self): """ Dimension """ @@ -1014,6 +1054,18 @@ def dimension(self, value): 'dimension', override=False) self._dimension = value + @property + def max_recoverable_scale(self): + """ Maximum Recoverable Scale """ + return self._max_recoverable_scale + + @max_recoverable_scale.setter + def max_recoverable_scale(self, value): + if value is not None: + caom_util.type_check(value, shape.Interval, 'max_recoverable_scale', + override=False) + self._max_recoverable_scale = value + @property def resolution(self): """ Resolution """ @@ -1047,17 +1099,6 @@ def sample_size(self, value): caom_util.type_check(value, float, 'sample_size') self._sample_size = value - @property - def time_dependent(self): - """ Time dependent """ - return self._time_dependent - - @time_dependent.setter - def time_dependent(self, value): - if value is not None: - caom_util.type_check(value, bool, 'time_dependent') - self._time_dependent = value - @property def calibration(self): return self._calibration @@ -1077,7 +1118,7 @@ class Energy(CaomObject): def __init__(self, bounds, samples, dimension=None, resolving_power=None, resolving_power_bounds=None, resolution=None, resolution_bounds=None, energy_bands=None, sample_size=None, bandpass_name=None, em_band=None, - transition=None, restwav=None, calibration=None): + transition=None, rest=None, calibration=None): """ Initialize an Energy instance. @@ -1098,7 +1139,7 @@ def __init__(self, bounds, samples, dimension=None, resolving_power=None, if em_band is not None: self.energy_bands.add(em_band) self.transition = transition - self.restwav = restwav + self.rest = rest self.calibration = calibration # Properties @@ -1250,15 +1291,15 @@ def transition(self, value): self._transition = value @property - def restwav(self): + def rest(self): """ rest wavelength of the target energy transition """ - return self._restwav + return self._rest - @restwav.setter - def restwav(self, value): + @rest.setter + def rest(self, value): if value is not None: - caom_util.type_check(value, float, 'restwav') - self._restwav = value + caom_util.type_check(value, float, 'rest') + self._rest = value @property def calibration(self): diff --git a/caom2/caom2/tests/data/CompleteCompositeCircle-CAOM-2.3.xml b/caom2/caom2/tests/data/CompleteCompositeCircle-CAOM-2.3.xml index 74f74055..ac687762 100644 --- a/caom2/caom2/tests/data/CompleteCompositeCircle-CAOM-2.3.xml +++ b/caom2/caom2/tests/data/CompleteCompositeCircle-CAOM-2.3.xml @@ -100,7 +100,6 @@ x science - diff --git a/caom2/caom2/tests/data/CompleteCompositePolygon-CAOM-2.3.xml b/caom2/caom2/tests/data/CompleteCompositePolygon-CAOM-2.3.xml index 91c3a8a9..311c6495 100644 --- a/caom2/caom2/tests/data/CompleteCompositePolygon-CAOM-2.3.xml +++ b/caom2/caom2/tests/data/CompleteCompositePolygon-CAOM-2.3.xml @@ -97,7 +97,6 @@ x science - diff --git a/caom2/caom2/tests/data/CompleteSimpleCircle-CAOM-2.3.xml b/caom2/caom2/tests/data/CompleteSimpleCircle-CAOM-2.3.xml index 2f123365..3c90e938 100644 --- a/caom2/caom2/tests/data/CompleteSimpleCircle-CAOM-2.3.xml +++ b/caom2/caom2/tests/data/CompleteSimpleCircle-CAOM-2.3.xml @@ -100,7 +100,6 @@ x science - diff --git a/caom2/caom2/tests/data/CompleteSimplePolygon-CAOM-2.3.xml b/caom2/caom2/tests/data/CompleteSimplePolygon-CAOM-2.3.xml index 025ab4de..bc622296 100644 --- a/caom2/caom2/tests/data/CompleteSimplePolygon-CAOM-2.3.xml +++ b/caom2/caom2/tests/data/CompleteSimplePolygon-CAOM-2.3.xml @@ -100,7 +100,6 @@ x science - diff --git a/caom2/caom2/tests/data/MinimalCompositeCircle-CAOM-2.3.xml b/caom2/caom2/tests/data/MinimalCompositeCircle-CAOM-2.3.xml index 93bf7a10..6894718c 100644 --- a/caom2/caom2/tests/data/MinimalCompositeCircle-CAOM-2.3.xml +++ b/caom2/caom2/tests/data/MinimalCompositeCircle-CAOM-2.3.xml @@ -16,7 +16,6 @@ x - diff --git a/caom2/caom2/tests/data/MinimalCompositePolygon-CAOM-2.3.xml b/caom2/caom2/tests/data/MinimalCompositePolygon-CAOM-2.3.xml index 64bbf828..372c2f47 100644 --- a/caom2/caom2/tests/data/MinimalCompositePolygon-CAOM-2.3.xml +++ b/caom2/caom2/tests/data/MinimalCompositePolygon-CAOM-2.3.xml @@ -16,7 +16,6 @@ x - diff --git a/caom2/caom2/tests/data/MinimalSimpleCircle-CAOM-2.3.xml b/caom2/caom2/tests/data/MinimalSimpleCircle-CAOM-2.3.xml index bd2abefb..952713bd 100644 --- a/caom2/caom2/tests/data/MinimalSimpleCircle-CAOM-2.3.xml +++ b/caom2/caom2/tests/data/MinimalSimpleCircle-CAOM-2.3.xml @@ -16,7 +16,6 @@ x - diff --git a/caom2/caom2/tests/data/MinimalSimplePolygon-CAOM-2.3.xml b/caom2/caom2/tests/data/MinimalSimplePolygon-CAOM-2.3.xml index 22c1a03c..051b6894 100644 --- a/caom2/caom2/tests/data/MinimalSimplePolygon-CAOM-2.3.xml +++ b/caom2/caom2/tests/data/MinimalSimplePolygon-CAOM-2.3.xml @@ -16,7 +16,6 @@ x - diff --git a/caom2/caom2/tests/data/SampleComposite-CAOM-2.3.xml b/caom2/caom2/tests/data/SampleComposite-CAOM-2.3.xml index 26660ac9..7ba88675 100644 --- a/caom2/caom2/tests/data/SampleComposite-CAOM-2.3.xml +++ b/caom2/caom2/tests/data/SampleComposite-CAOM-2.3.xml @@ -70,7 +70,6 @@ productID0 - ivo://example.org/foo?productID0 2017-10-10T19:57:03.770 2017-10-10T19:57:03.770 image @@ -149,7 +148,6 @@ 0.05 0.025 - false @@ -2478,7 +2476,6 @@ productID1 - ivo://example.org/foo?productID1 2017-10-10T19:57:03.770 2017-10-10T19:57:03.770 image @@ -2524,7 +2521,6 @@ 0.05 0.025 - false diff --git a/caom2/caom2/tests/data/SampleDerived-CAOM-2.4.xml b/caom2/caom2/tests/data/SampleDerived-CAOM-2.4.xml index 43741dac..9aab32ba 100644 --- a/caom2/caom2/tests/data/SampleDerived-CAOM-2.4.xml +++ b/caom2/caom2/tests/data/SampleDerived-CAOM-2.4.xml @@ -75,7 +75,6 @@ productID0 - ivo://example.org/foo?productID0 2019-10-18T20:22:53.979 ivo://cadc.nrc.ca/gms?A @@ -163,7 +162,6 @@ 0.05 0.025 - false @@ -2881,7 +2879,6 @@ productID1 - ivo://example.org/foo?productID1 2019-10-18T20:22:53.979 ivo://cadc.nrc.ca/gms?A @@ -2936,7 +2933,6 @@ 0.05 0.025 - false diff --git a/caom2/caom2/tests/data/sample-derived-caom25.xml b/caom2/caom2/tests/data/SampleDerived-CAOM-2.5.xml similarity index 100% rename from caom2/caom2/tests/data/sample-derived-caom25.xml rename to caom2/caom2/tests/data/SampleDerived-CAOM-2.5.xml diff --git a/caom2/caom2/tests/data/SampleSimple-CAOM-2.3.xml b/caom2/caom2/tests/data/SampleSimple-CAOM-2.3.xml index 87351d0b..7564808d 100644 --- a/caom2/caom2/tests/data/SampleSimple-CAOM-2.3.xml +++ b/caom2/caom2/tests/data/SampleSimple-CAOM-2.3.xml @@ -63,7 +63,6 @@ y34b0101t-RAW_STANDARD - ivo://archive.stsci.edu/HST?y34b0101t/y34b0101t-RAW_STANDARD 1996-02-28T17:38:41.000 1997-02-28T00:54:45.000 spectrum @@ -125,7 +124,6 @@ - false diff --git a/caom2/caom2/tests/test_checksum.py b/caom2/caom2/tests/test_checksum.py index d4e7e601..ba9dd7da 100644 --- a/caom2/caom2/tests/test_checksum.py +++ b/caom2/caom2/tests/test_checksum.py @@ -351,7 +351,7 @@ def atest_round_trip(): def test_checksum_diff(): for source_file_path in \ - [os.path.join(THIS_DIR, TEST_DATA, x) for x in ['sample-derived-caom25.xml']]: + [os.path.join(THIS_DIR, TEST_DATA, x) for x in ['SampleDerived-CAOM-2.5.xml']]: #['SampleDerived-CAOM-2.4.xml', 'SampleComposite-CAOM-2.3.xml']]: logging.debug(source_file_path) output_file = tempfile.NamedTemporaryFile() diff --git a/caom2/caom2/tests/test_diffs.py b/caom2/caom2/tests/test_diffs.py index afaf405e..803d2dcc 100644 --- a/caom2/caom2/tests/test_diffs.py +++ b/caom2/caom2/tests/test_diffs.py @@ -167,8 +167,8 @@ def test_plane_level_position(self): poly1 = shape.Polygon(points=p1) poly2 = shape.Polygon(points=p2) - o1 = Position(bounds=poly1, samples=MultiShape([poly1]), time_dependent=False) - o2 = Position(bounds=poly2, samples=MultiShape([poly2]), time_dependent=False) + o1 = Position(bounds=poly1, samples=MultiShape([poly1])) + o2 = Position(bounds=poly2, samples=MultiShape([poly2])) report = diff.get_differences(o1, o2, 'caom test instances') assert report is None, 'NaN comparison failure' diff --git a/caom2/caom2/tests/test_obs_reader_writer.py b/caom2/caom2/tests/test_obs_reader_writer.py index 312d92e6..4e7541d5 100644 --- a/caom2/caom2/tests/test_obs_reader_writer.py +++ b/caom2/caom2/tests/test_obs_reader_writer.py @@ -215,7 +215,7 @@ def test_minimal_derived(self): def test_complete_derived(self): # * formerly known as composite - for version in (23, 24): # TODO 25 + for version in (23, 24, 25): for i in range(1, 6): if version >= 24: print("Test Complete Derived {} version {}". @@ -522,8 +522,6 @@ def compare_position(self, expected, actual): "resolution") self.assertEqual(expected.sample_size, actual.sample_size, "sample_size") - self.assertEqual(expected.time_dependent, actual.time_dependent, - "time_dependent") def compare_energy(self, expected, actual): print("comparing energy") diff --git a/caom2/caom2/tests/test_plane.py b/caom2/caom2/tests/test_plane.py index e3246435..87b29fe0 100644 --- a/caom2/caom2/tests/test_plane.py +++ b/caom2/caom2/tests/test_plane.py @@ -461,9 +461,6 @@ def test_all(self): self.assertIsNone(position.sample_size, "Default sample size") position.sample_size = 321.123 self.assertEqual(321.123, position.sample_size, "Sample size") - self.assertFalse(position.time_dependent, "Default time dependent") - position.time_dependent = True - self.assertTrue(position.time_dependent, "Time dependent") class TestEnergy(unittest.TestCase): From 4550f3841e220573df13eb42750da622117f6255 Mon Sep 17 00:00:00 2001 From: Adrian Damian Date: Thu, 6 Feb 2025 12:48:43 -0800 Subject: [PATCH 3/6] Checksum pass --- caom2/caom2/artifact.py | 3 +- caom2/caom2/checksum.py | 66 ++++-- caom2/caom2/common.py | 2 +- caom2/caom2/obs_reader_writer.py | 53 ++--- caom2/caom2/observation.py | 28 +-- caom2/caom2/plane.py | 198 +++++++++--------- caom2/caom2/tests/caom_test_instances.py | 6 +- .../tests/data/SampleDerived-CAOM-2.5.xml | 116 ++++++---- caom2/caom2/tests/test_caom_util.py | 9 +- caom2/caom2/tests/test_obs_reader_writer.py | 4 +- caom2/caom2/tests/test_observation.py | 12 +- caom2/caom2/tests/test_plane.py | 92 ++++---- 12 files changed, 326 insertions(+), 263 deletions(-) diff --git a/caom2/caom2/artifact.py b/caom2/caom2/artifact.py index e86bb903..0f3831a7 100644 --- a/caom2/caom2/artifact.py +++ b/caom2/caom2/artifact.py @@ -269,7 +269,8 @@ def content_checksum(self, value): self._content_checksum = None else: caom_util.type_check(value, ChecksumURI, "checksum_uri", False) - self._content_checksum = value + # TODO necessary? + self._content_checksum = value.uri @property def content_release(self): diff --git a/caom2/caom2/checksum.py b/caom2/caom2/checksum.py index 78d96eef..2419d29a 100644 --- a/caom2/caom2/checksum.py +++ b/caom2/caom2/checksum.py @@ -267,8 +267,8 @@ def update_checksum(checksum, value, attribute=''): if isinstance(value, ObservationURI) or isinstance(value, ChecksumURI): b = value.uri.encode('utf-8') elif isinstance(value, CaomObject): - logger.debug('Process object {}'.format(attribute)) - update_caom_checksum(checksum, value, attribute) + #logger.debug('Process object {}'.format(attribute)) + return update_caom_checksum(checksum, value, attribute) elif isinstance(value, bytes): b = value elif isinstance(value, bool): @@ -284,18 +284,22 @@ def update_checksum(checksum, value, attribute=''): b = value.strip().encode('utf-8') elif isinstance(value, datetime): b = struct.pack('!q', int( - (value - datetime(1970, 1, 1)).total_seconds())) + (value - datetime(1970, 1, 1)).total_seconds()*1000)) elif isinstance(value, set) or \ (isinstance(value, TypedSet) and not isinstance(value.key_type, AbstractCaomEntity)): + updated = False for i in sorted(value): - update_checksum(checksum, i, attribute) + updated |= update_checksum(checksum, i, attribute) + return updated elif isinstance(value, list) or isinstance(value, TypedList): + updated = False for i in value: if not isinstance(i, AbstractCaomEntity): - update_checksum(checksum, i, attribute) + updated |= update_checksum(checksum, i, attribute) + return updated elif isinstance(value, Enum): - update_checksum(checksum, value.value, attribute) + return update_checksum(checksum, value.value, attribute) elif isinstance(value, uuid.UUID): b = value.bytes elif isinstance(value, TypedOrderedDict): @@ -303,11 +307,13 @@ def update_checksum(checksum, value, attribute=''): # alphabetical order of their ids # Note: ignore dictionaries of AbstractCaomEntity types checksums = [] + updated = False for i in value: if not isinstance(value[i], AbstractCaomEntity): checksums.append(value[i]._id) for i in sorted(checksums): - update_checksum(checksum, checksum[i], attribute) + updated &= update_checksum(checksum, checksum[i], attribute) + return updated else: raise ValueError( 'Cannot transform in bytes: {}({})'.format(value, type(value))) @@ -315,11 +321,22 @@ def update_checksum(checksum, value, attribute=''): if b is not None: checksum.update(b) if logger.isEnabledFor(logging.DEBUG): - md5 = hashlib.md5() - md5.update(b) - logger.debug('Encoded attribute ({}) {} = {} -- {}'. - format(type(value), attribute, - value, md5.hexdigest())) + logger.debug("Encoded attribute value - {} = {} {} bytes".format(attribute, value, len(b))) + return True + return False + + +def to_model_name(attribute): + """ + Converts the attribute name to the corresponding model name + :param attribute: name of attribute + :return: camel case name of the attribute in the model + """ + # Replace underscores and capitalize the first letter of each word + components = attribute.split('_') + # The first component should remain lowercase + return components[0] + ''.join( + word.capitalize() for word in components[1:]) def update_caom_checksum(checksum, entity, parent=None): @@ -334,10 +351,16 @@ def update_caom_checksum(checksum, entity, parent=None): if not isinstance(entity, CaomObject): raise AttributeError('CaomObject expected') # get the id first + updated = False if isinstance(entity, AbstractCaomEntity): - update_checksum(checksum, entity._id) + update_checksum(checksum, entity._id, "Entity.id") if entity._meta_producer: - update_checksum(checksum, entity._meta_producer) + if update_checksum(checksum, entity._meta_producer, "Entity.metaProducer"): + updated = True + model_name = "Entity.metaProducer" + checksum.update(model_name.encode('utf-8')) + logger.debug('Encoded attribute name {} = {}'. + format('_meta_producer', model_name)) # determine the excluded fields if necessary checksum_excluded_fields = [] @@ -352,9 +375,18 @@ def update_caom_checksum(checksum, entity, parent=None): for i in sorted(dir(entity)): if not callable(getattr(entity, i)) and not i.startswith('_') and \ i not in checksum_excluded_fields: - if getattr(entity, i) is not None: + val = getattr(entity, i) + if val is not None: atrib = '{}.{}'.format(parent, i) if parent is not None else i - update_checksum(checksum, getattr(entity, i), atrib) + if update_checksum(checksum, val, atrib): + updated = True + type_name = type(entity).__name__ + if type_name in ['DerivedObservation', 'SimpleObservation'] and to_model_name(i) != 'members': + type_name = 'Observation' + model_name = (type_name + "." + to_model_name(i)).lower() + checksum.update(model_name.encode('utf-8')) + logger.debug('Encoded attribute name - {} = {}'.format(atrib, model_name)) + return updated def checksum_diff(): @@ -430,7 +462,7 @@ def _print_diff(orig, actual): mistmatches += 1 if elem_type != 'chunk': - # do the accummulated checksums + # do the accumulated checksums if orig.acc_meta_checksum == actual.acc_meta_checksum: print('{}: {} {} == {}'. format(elem_type, orig._id, orig.acc_meta_checksum.checksum, diff --git a/caom2/caom2/common.py b/caom2/caom2/common.py index 979bbbf5..286b81f7 100644 --- a/caom2/caom2/common.py +++ b/caom2/caom2/common.py @@ -300,7 +300,7 @@ def meta_producer(self, value): self._meta_producer = value -class VocabularyTerm(object): +class VocabularyTerm(CaomObject): """ VocabularyTerm """ def __init__(self, namespace, term, base=False): diff --git a/caom2/caom2/obs_reader_writer.py b/caom2/caom2/obs_reader_writer.py index 8949ecea..97482848 100644 --- a/caom2/caom2/obs_reader_writer.py +++ b/caom2/caom2/obs_reader_writer.py @@ -373,7 +373,7 @@ def _get_proposal(self, element_tag, parent, ns, required): else: proposal = observation.Proposal( self._get_child_text("id", el, ns, True)) - proposal.pi_name = self._get_child_text("pi", el, ns, False) + proposal.pi = self._get_child_text("pi", el, ns, False) proposal.project = self._get_child_text("project", el, ns, False) proposal.title = self._get_child_text("title", el, ns, False) self._add_keywords(proposal.keywords, el, ns, False) @@ -400,7 +400,7 @@ def _get_target(self, element_tag, parent, ns, required): self._get_child_text("name", el, ns, True)) target_type = self._get_child_text("type", el, ns, False) if target_type: - target.target_type = observation.TargetType(target_type) + target.type = observation.TargetType(target_type) target_standard = self._get_child_text("standard", el, ns, False) if target_standard is not None: target.standard = ("true" == target_standard) @@ -568,12 +568,12 @@ def _add_members(self, members, parent, ns): members.add(observation.ObservationURI(member_element.text)) def _add_inputs(self, inputs, parent, ns): - """Create PlaneURI objects from an XML representation of the planeURI - elements and add them to the set of PlaneURIs. + """Create URI objects from an XML representation of the planeURI + elements and add them to the set of plane URIs. Arguments: - inputs : set of PlaneURI from the Provenance - parent : element containing the PlaneURI elements + inputs : set of plane URIs from the Provenance + parent : element containing the plane uri elements ns : namespace of the document raise : ObservationParsingException """ @@ -581,10 +581,10 @@ def _add_inputs(self, inputs, parent, ns): if el is not None: if self.version < 25: for uri_element in el.iterchildren("{" + ns + "}planeURI"): - inputs.add(plane.PlaneURI(str(uri_element.text))) + inputs.add(str(uri_element.text)) else: for uri_element in el.iterchildren("{" + ns + "}input"): - inputs.add(plane.PlaneURI(str(uri_element.text))) + inputs.add(str(uri_element.text)) if not inputs: error = "No planeURI element found in members" @@ -1491,12 +1491,12 @@ def _get_polarization(self, element_tag, parent, ns, required): polarization = plane.Polarization() _pstates_el = self._get_child_element("states", el, ns, False) if _pstates_el is not None: - _polarization_states = list() + _states = list() for _pstate_el in _pstates_el.iterchildren("{" + ns + "}state"): _pstate = _pstate_el.text - _polarization_state = plane.PolarizationState(_pstate) - _polarization_states.append(_polarization_state) - polarization.polarization_states = _polarization_states + _state = plane.PolarizationState(_pstate) + _states.append(_state) + polarization.states = _states polarization.dimension = self._get_child_text_as_int("dimension", el, ns, False) @@ -1796,13 +1796,14 @@ def _add_planes(self, obs, parent, ns): else: for plane_element in el.iterchildren("{" + ns + "}plane"): if self.version < 25: - _uri = plane.PlaneURI.get_plane_uri( + # TODO + _uri = "{}/{}".format( obs.uri, self._get_child_text("productID", plane_element, ns, True)) else: - _uri = plane.PlaneURI(self._get_child_text("uri", plane_element, ns, True)) - _plane = plane.Plane(_uri.uri) + _uri = self._get_child_text("uri", plane_element, ns, True) + _plane = plane.Plane(_uri) _plane.meta_release = caom_util.str2ivoa( self._get_child_text("metaRelease", plane_element, ns, False)) @@ -2082,7 +2083,7 @@ def _add_proposal_element(self, proposal, parent): element = self._get_caom_element("proposal", parent) self._add_element("id", proposal.id, element) - self._add_element("pi", proposal.pi_name, element) + self._add_element("pi", proposal.pi, element) self._add_element("project", proposal.project, element) self._add_element("title", proposal.title, element) self._add_element("reference", proposal.reference, element) @@ -2101,8 +2102,8 @@ def _add_target_element(self, target, parent): raise AttributeError( "Attempt to write CAOM2.4 element (target.targetID) " "as CAOM2.3 Observation") - if target.target_type is not None: - self._add_element("type", target.target_type.value, element) + if target.type is not None: + self._add_element("type", target.type.value, element) self._add_boolean_element("standard", target.standard, element) self._add_element("redshift", target.redshift, element) self._add_boolean_element("moving", target.moving, element) @@ -2195,13 +2196,13 @@ def _add_planes_element(self, planes, parent): plane_element = self._get_caom_element("plane", element) self._add_entity_attributes(_plane, plane_element) if self._output_version < 25: - _comp = _plane.uri.uri.split('/') + _comp = _plane.uri.split('/') if len(_comp) != 3: raise ValueError("Attempt to write CAOM2.4 but can't deduce " "Plane.productID in Plane.uri=" + _plane.uri) self._add_element("productID", _comp[-1], plane_element) else: - self._add_element("uri", _plane.uri.uri, plane_element) + self._add_element("uri", _plane.uri, plane_element) self._add_datetime_element("metaRelease", _plane.meta_release, plane_element) if self._output_version < 24 and _plane.meta_read_groups: @@ -2417,9 +2418,9 @@ def _add_polarization_element(self, polarization, parent): if polarization is None: return element = self._get_caom_element("polarization", parent) - if polarization.polarization_states: + if polarization.states: _pstates_el = self._get_caom_element("states", element) - for _state in polarization.polarization_states: + for _state in polarization.states: self._add_element("state", _state.value, _pstates_el) self._add_element("dimension", polarization.dimension, element) @@ -2536,7 +2537,7 @@ def _add_observable_element(self, observable, parent): return element = self._get_caom_element("observable", parent) - self._add_element("ucd", observable.ucd.value, element) + self._add_element("ucd", observable.ucd, element) if observable.calibration: if self._output_version < 25: raise AttributeError( @@ -2588,7 +2589,7 @@ def _add_artifacts_element(self, artifacts, parent): artifact_element) if _artifact.content_checksum: self._add_element("contentChecksum", - _artifact.content_checksum.uri, + _artifact.content_checksum, artifact_element) if _artifact.description_id is not None: if self._output_version < 25: @@ -2986,9 +2987,9 @@ def _add_inputs_element(self, name, collection, parent): element = self._get_caom_element(name, parent) for plane_uri in collection: if self._output_version < 25: - self._add_element("planeURI", plane_uri.uri, element) + self._add_element("planeURI", plane_uri, element) else: - self._add_element("input", plane_uri.uri, element) + self._add_element("input", plane_uri, element) def _get_caom_element(self, tag, parent): return etree.SubElement(parent, self._caom2_namespace + tag) diff --git a/caom2/caom2/observation.py b/caom2/caom2/observation.py index 615aaff9..4d2772ec 100644 --- a/caom2/caom2/observation.py +++ b/caom2/caom2/observation.py @@ -879,7 +879,7 @@ class Proposal(CaomObject): def __init__(self, id, - pi_name=None, + pi=None, project=None, title=None, reference=None): @@ -891,7 +891,7 @@ def __init__(self, """ self.id = id - self.pi_name = pi_name + self.pi = pi self.project = project self.title = title self.keywords = set() @@ -931,18 +931,18 @@ def keywords(self, value): self._keywords = value @property - def pi_name(self): + def pi(self): """The name (First Last) of the Principle Investigator of the Proposal. type: unicode string """ - return self._pi_name + return self._pi - @pi_name.setter - def pi_name(self, value): - caom_util.type_check(value, str, 'pi_name') - self._pi_name = value + @pi.setter + def pi(self, value): + caom_util.type_check(value, str, 'pi') + self._pi = value @property def project(self): @@ -1011,7 +1011,7 @@ class Target(CaomObject): """ Target """ def __init__(self, name, - target_type=None, + type=None, standard=None, redshift=None, keywords=None, @@ -1026,7 +1026,7 @@ def __init__(self, name, """ self.name = name - self.target_type = target_type + self.type = type self.standard = standard self.redshift = redshift if keywords is None: @@ -1053,7 +1053,7 @@ def name(self, value): self._name = value @property - def target_type(self): + def type(self): """A keyword describing the type of target. must be from the list """ + str(list(TargetType)) + """ @@ -1062,11 +1062,11 @@ def target_type(self): """ return self._type - @target_type.setter - def target_type(self, value): + @type.setter + def type(self, value): if isinstance(value, str): value = TargetType(value) - caom_util.type_check(value, TargetType, "target_type") + caom_util.type_check(value, TargetType, "type") self._type = value @property diff --git a/caom2/caom2/plane.py b/caom2/caom2/plane.py index 6d7eadb3..ff57f58e 100644 --- a/caom2/caom2/plane.py +++ b/caom2/caom2/plane.py @@ -89,7 +89,7 @@ __all__ = ['CalibrationLevel', 'DataProductType', 'EnergyBand', 'PolarizationState', 'Quality', 'Plane', - 'PlaneURI', 'DataQuality', 'Metrics', 'Provenance', 'Position', + 'DataQuality', 'Metrics', 'Provenance', 'Position', 'Energy', 'Polarization', 'Time', 'Observable'] @@ -109,7 +109,7 @@ class CalibrationLevel(Enum): ANALYSIS_PRODUCT = int_32(4) -class Ucd(CaomObject): +class Ucd: """ UCD - enum of UCDs""" UCD_VOCAB = "https://ivoa.net/documents/UCD1+/20230125/ucd-list.txt" @@ -233,7 +233,7 @@ def __init__(self, ucd, calibration=None): if not ucd: raise ValueError("Observable.ucd cannot be None") caom_util.type_check(ucd, Ucd, 'ucd') - self._ucd = ucd + self._ucd = ucd.value self.calibration = calibration @property @@ -312,7 +312,7 @@ def __init__(self, uri, """ super(Plane, self).__init__() validate_uri(uri) - self._uri = PlaneURI(uri) + self._uri = uri if artifacts is None: artifacts = caom_util.TypedOrderedDict(Artifact, ) self.artifacts = artifacts @@ -644,90 +644,90 @@ def visibility(self, value): # TODO not sure this is needed anymore -class PlaneURI(CaomObject): - """ Plane URI """ - def __init__(self, uri): - """ - Initializes an Plane instance - - Arguments: - uri : URI corresponding to the plane - - Throws: - TypeError : if uri is not a string - ValueError : if uri is invalid - ValueError : if the uri is valid but does not contain the expected - fields (collection, observation_id and product_id) - """ - - self.uri = uri - - def _key(self): - return self.uri - - def __hash__(self): - return hash(self._key()) - - def __lt__(self, other): - if not isinstance(other, PlaneURI): - raise ValueError( - 'Cannot compare PlaneURI with {}'.format(type(other))) - return self.uri < other.uri - - def __eq__(self, other): - if not isinstance(other, PlaneURI): - raise ValueError( - 'Cannot compare PlaneURI with {}'.format(type(other))) - return self.uri == other.uri - - @classmethod - def get_plane_uri(cls, observation_uri, product_id): - """ - Initializes an Plane URI instance - - Arguments: - observation_uri : the uri of the observation - product_id : ID of the product - """ - caom_util.type_check(observation_uri, ObservationURI, - "observation_uri", - override=False) - caom_util.type_check(product_id, str, "product_id", - override=False) - caom_util.validate_path_component(cls, "product_id", product_id) - - path = urlsplit(observation_uri.uri).path - uri = SplitResult(ObservationURI._SCHEME, "", path + "/" + - product_id, "", "").geturl() - return cls(uri) - - # Properties - @property - def uri(self): - """A uri that locates the plane object inside caom""" - return self._uri - - @uri.setter - def uri(self, value): - - caom_util.type_check(value, str, "uri", override=False) - tmp = urlsplit(value) - - if tmp.scheme != ObservationURI._SCHEME: - raise ValueError("{} doesn't have an allowed scheme".format(value)) - if tmp.geturl() != value: - raise ValueError("Failed to parse uri correctly: {}".format(value)) - - (collection, observation_id, product_id) = tmp.path.split("/") - - if product_id is None: - raise ValueError("Faield to get product ID from uri: {}" - .format(value)) - - self._product_id = product_id - self._observation_uri = \ - ObservationURI.get_observation_uri(collection, observation_id) - self._uri = value +# class PlaneURI(CaomObject): +# """ Plane URI """ +# def __init__(self, uri): +# """ +# Initializes a Plane instance +# +# Arguments: +# uri : URI corresponding to the plane +# +# Throws: +# TypeError : if uri is not a string +# ValueError : if uri is invalid +# ValueError : if the uri is valid but does not contain the expected +# fields (collection, observation_id and product_id) +# """ +# +# self.uri = uri +# +# def _key(self): +# return self.uri +# +# def __hash__(self): +# return hash(self._key()) +# +# def __lt__(self, other): +# if not isinstance(other, PlaneURI): +# raise ValueError( +# 'Cannot compare PlaneURI with {}'.format(type(other))) +# return self.uri < other.uri +# +# def __eq__(self, other): +# if not isinstance(other, PlaneURI): +# raise ValueError( +# 'Cannot compare PlaneURI with {}'.format(type(other))) +# return self.uri == other.uri +# +# @classmethod +# def get_plane_uri(cls, observation_uri, product_id): +# """ +# Initializes an Plane URI instance +# +# Arguments: +# observation_uri : the uri of the observation +# product_id : ID of the product +# """ +# caom_util.type_check(observation_uri, ObservationURI, +# "observation_uri", +# override=False) +# caom_util.type_check(product_id, str, "product_id", +# override=False) +# caom_util.validate_path_component(cls, "product_id", product_id) +# +# path = urlsplit(observation_uri.uri).path +# uri = SplitResult(ObservationURI._SCHEME, "", path + "/" + +# product_id, "", "").geturl() +# return cls(uri) +# +# # Properties +# @property +# def uri(self): +# """A uri that locates the plane object inside caom""" +# return self._uri +# +# @uri.setter +# def uri(self, value): +# +# caom_util.type_check(value, str, "uri", override=False) +# tmp = urlsplit(value) +# +# if tmp.scheme != ObservationURI._SCHEME: +# raise ValueError("{} doesn't have an allowed scheme".format(value)) +# if tmp.geturl() != value: +# raise ValueError("Failed to parse uri correctly: {}".format(value)) +# +# (collection, observation_id, product_id) = tmp.path.split("/") +# +# if product_id is None: +# raise ValueError("Faield to get product ID from uri: {}" +# .format(value)) +# +# self._product_id = product_id +# self._observation_uri = \ +# ObservationURI.get_observation_uri(collection, observation_id) +# self._uri = value class DataQuality(CaomObject): @@ -895,7 +895,7 @@ def __init__(self, name, self.last_executed = last_executed self._keywords = set() - self._inputs = caom_util.TypedSet(PlaneURI, ) + self._inputs = caom_util.TypedSet(str, ) # Properties @@ -1319,7 +1319,7 @@ class Polarization(CaomObject): def __init__(self, dimension=None, - polarization_states=None): + states=None): """ Initialize a Polarization instance. @@ -1327,7 +1327,7 @@ def __init__(self, None """ self.dimension = dimension - self.polarization_states = polarization_states + self.states = states # Properties @property @@ -1341,23 +1341,23 @@ def dimension(self): @dimension.setter def dimension(self, value): - caom_util.type_check(value, int, 'dimension') + caom_util.type_check(value, int_32, 'dimension') caom_util.value_check(value, 0, 1E10, 'dimension') - self._dimension = value + self._dimension = int_32(value) if value is not None else None @property - def polarization_states(self): + def states(self): """ type: list """ - return self._polarization_states + return self._states - @polarization_states.setter - def polarization_states(self, value): + @states.setter + def states(self, value): if value is not None: - caom_util.type_check(value, list, 'polarization_states', + caom_util.type_check(value, list, 'states', override=False) - self._polarization_states = value + self._states = value class Time(CaomObject): diff --git a/caom2/caom2/tests/caom_test_instances.py b/caom2/caom2/tests/caom_test_instances.py index f74c9ed3..ed8d9f1e 100644 --- a/caom2/caom2/tests/caom_test_instances.py +++ b/caom2/caom2/tests/caom_test_instances.py @@ -464,9 +464,9 @@ def get_provenance(self): return provenance def get_inputs(self): - return caom_util.TypedSet(plane.PlaneURI, - plane.PlaneURI("caom:foo/bar/plane1"), - plane.PlaneURI("caom:foo/bar/plane2")) + return caom_util.TypedSet(str, + "caom:foo/bar/plane1", + "caom:foo/bar/plane2") def get_metrics(self): metrics = plane.Metrics() diff --git a/caom2/caom2/tests/data/SampleDerived-CAOM-2.5.xml b/caom2/caom2/tests/data/SampleDerived-CAOM-2.5.xml index e075f587..79ff0377 100644 --- a/caom2/caom2/tests/data/SampleDerived-CAOM-2.5.xml +++ b/caom2/caom2/tests/data/SampleDerived-CAOM-2.5.xml @@ -1,9 +1,9 @@ - + TEST caom:TEST/observationID 7c1 - 2025-01-10T23:55:32.467 + 2025-02-05T00:12:21.039 ivo://cadc.nrc.ca/gms?A ivo://cadc.nrc.ca/gms?B @@ -76,14 +76,14 @@ true - + caom:collection/observation/plane0 - 2025-01-10T23:55:32.467 + 2025-02-05T00:12:21.039 ivo://cadc.nrc.ca/gms?A ivo://cadc.nrc.ca/gms?B - 2025-01-10T23:55:32.467 + 2025-02-05T00:12:21.039 ivo://cadc.nrc.ca/gms?C ivo://cadc.nrc.ca/gms?D @@ -97,7 +97,7 @@ provenanceProducer provenanceRunID http://foo/bar - 2025-01-10T23:55:32.467 + 2025-02-05T00:12:21.039 keyword1 keyword2 @@ -298,12 +298,12 @@ 0.85 - + ad:foo/bar0 109 this data - 2025-01-10T23:55:32.467 + 2025-02-05T00:12:21.039 ivo://cadc.nrc.ca/gms?C ivo://cadc.nrc.ca/gms?D @@ -313,11 +313,12 @@ md5:d41d8cd98f00b204e9800998ecf8427e desc:TEST/science-ready-data - + x0 this - + + this 5 6 1 @@ -519,6 +520,7 @@ UTC TOPOCENTER + 50000.0 1.0 2.0 @@ -620,7 +622,8 @@ - + + this 5 6 1 @@ -822,6 +825,7 @@ UTC TOPOCENTER + 50000.0 1.0 2.0 @@ -925,11 +929,12 @@ - + x1 this - + + this 5 6 1 @@ -1131,6 +1136,7 @@ UTC TOPOCENTER + 50000.0 1.0 2.0 @@ -1232,7 +1238,8 @@ - + + this 5 6 1 @@ -1434,6 +1441,7 @@ UTC TOPOCENTER + 50000.0 1.0 2.0 @@ -1539,12 +1547,12 @@ - + ad:foo/bar1 6de this data - 2025-01-10T23:55:32.467 + 2025-02-05T00:12:21.039 ivo://cadc.nrc.ca/gms?C ivo://cadc.nrc.ca/gms?D @@ -1554,11 +1562,12 @@ md5:d41d8cd98f00b204e9800998ecf8427e desc:TEST/science-ready-data - + x0 this - + + this 5 6 1 @@ -1760,6 +1769,7 @@ UTC TOPOCENTER + 50000.0 1.0 2.0 @@ -1861,7 +1871,8 @@ - + + this 5 6 1 @@ -2063,6 +2074,7 @@ UTC TOPOCENTER + 50000.0 1.0 2.0 @@ -2166,11 +2178,12 @@ - + x1 this - + + this 5 6 1 @@ -2372,6 +2385,7 @@ UTC TOPOCENTER + 50000.0 1.0 2.0 @@ -2473,7 +2487,8 @@ - + + this 5 6 1 @@ -2675,6 +2690,7 @@ UTC TOPOCENTER + 50000.0 1.0 2.0 @@ -2782,14 +2798,14 @@ - + caom:collection/observation/plane1 - 2025-01-10T23:55:32.467 + 2025-02-05T00:12:21.039 ivo://cadc.nrc.ca/gms?A ivo://cadc.nrc.ca/gms?B - 2025-01-10T23:55:32.467 + 2025-02-05T00:12:21.039 ivo://cadc.nrc.ca/gms?C ivo://cadc.nrc.ca/gms?D @@ -2803,7 +2819,7 @@ provenanceProducer provenanceRunID http://foo/bar - 2025-01-10T23:55:32.467 + 2025-02-05T00:12:21.039 keyword1 keyword2 @@ -2977,12 +2993,12 @@ 0.85 - + ad:foo/bar0 109 this data - 2025-01-10T23:55:32.467 + 2025-02-05T00:12:21.039 ivo://cadc.nrc.ca/gms?C ivo://cadc.nrc.ca/gms?D @@ -2992,11 +3008,12 @@ md5:d41d8cd98f00b204e9800998ecf8427e desc:TEST/science-ready-data - + x0 this - + + this 5 6 1 @@ -3198,6 +3215,7 @@ UTC TOPOCENTER + 50000.0 1.0 2.0 @@ -3299,7 +3317,8 @@ - + + this 5 6 1 @@ -3501,6 +3520,7 @@ UTC TOPOCENTER + 50000.0 1.0 2.0 @@ -3604,11 +3624,12 @@ - + x1 this - + + this 5 6 1 @@ -3810,6 +3831,7 @@ UTC TOPOCENTER + 50000.0 1.0 2.0 @@ -3911,7 +3933,8 @@ - + + this 5 6 1 @@ -4113,6 +4136,7 @@ UTC TOPOCENTER + 50000.0 1.0 2.0 @@ -4218,12 +4242,12 @@ - + ad:foo/bar1 6de this data - 2025-01-10T23:55:32.467 + 2025-02-05T00:12:21.039 ivo://cadc.nrc.ca/gms?C ivo://cadc.nrc.ca/gms?D @@ -4233,11 +4257,12 @@ md5:d41d8cd98f00b204e9800998ecf8427e desc:TEST/science-ready-data - + x0 this - + + this 5 6 1 @@ -4439,6 +4464,7 @@ UTC TOPOCENTER + 50000.0 1.0 2.0 @@ -4540,7 +4566,8 @@ - + + this 5 6 1 @@ -4742,6 +4769,7 @@ UTC TOPOCENTER + 50000.0 1.0 2.0 @@ -4845,11 +4873,12 @@ - + x1 this - + + this 5 6 1 @@ -5051,6 +5080,7 @@ UTC TOPOCENTER + 50000.0 1.0 2.0 @@ -5152,7 +5182,8 @@ - + + this 5 6 1 @@ -5354,6 +5385,7 @@ UTC TOPOCENTER + 50000.0 1.0 2.0 diff --git a/caom2/caom2/tests/test_caom_util.py b/caom2/caom2/tests/test_caom_util.py index e8ed2068..e045ac5a 100644 --- a/caom2/caom2/tests/test_caom_util.py +++ b/caom2/caom2/tests/test_caom_util.py @@ -183,7 +183,7 @@ def test_typed_ordered_dict(self): chunk.DataLinkSemantics.SCIENCE, artifact.ReleaseType.DATA) test_part10 = part.Part("10") - test_plane_uri = plane.PlaneURI('caom:CFHT/55/66') + test_plane_uri = 'caom:CFHT/55/66' my_dict_plane = caom_util.TypedOrderedDict(plane.Plane, ) with self.assertRaises(ValueError): my_dict_plane['key11'] = test_plane10 @@ -193,8 +193,8 @@ def test_typed_ordered_dict(self): my_dict_part = caom_util.TypedOrderedDict(part.Part, ) with self.assertRaises(ValueError): my_dict_part['11'] = test_part10 - my_dict_wrong_type = caom_util.TypedOrderedDict(plane.PlaneURI, ) - with self.assertRaises(ValueError): + my_dict_wrong_type = caom_util.TypedOrderedDict(plane.Plane, ) + with self.assertRaises(TypeError): my_dict_wrong_type['caom:CFHT/55/67'] = test_plane_uri with self.assertRaises(TypeError): my_dict_plane['key2'] = 'value2' @@ -258,9 +258,6 @@ def test_typed_ordered_dict(self): test_plane5 = plane.Plane('caom:TEST/obs1/key5') my_dict1[test_plane5.uri] = test_plane5 - with self.assertRaises(TypeError): - my_dict1.add(test_plane_uri) - # test pop function self.assertEqual(5, len(my_dict1), 'mismatch in the number of entries in dictionary.') diff --git a/caom2/caom2/tests/test_obs_reader_writer.py b/caom2/caom2/tests/test_obs_reader_writer.py index 4e7541d5..7b469029 100644 --- a/caom2/caom2/tests/test_obs_reader_writer.py +++ b/caom2/caom2/tests/test_obs_reader_writer.py @@ -363,7 +363,7 @@ def compare_proposal(self, expected, actual): self.assertIsNotNone(expected) self.assertIsNotNone(actual) self.assertEqual(expected.id, actual.id) - self.assertEqual(expected.pi_name, actual.pi_name) + self.assertEqual(expected.pi, actual.pi) self.assertEqual(expected.project, actual.project) self.assertEqual(expected.title, actual.title) self.assertEqual(len(expected.keywords), len(actual.keywords)) @@ -376,7 +376,7 @@ def compare_target(self, expected, actual): self.assertIsNotNone(expected) self.assertIsNotNone(actual) self.assertEqual(expected.name, actual.name) - self.assertEqual(expected.target_type, actual.target_type) + self.assertEqual(expected.type, actual.type) self.assertEqual(expected.redshift, actual.redshift) self.assertEqual(expected.moving, actual.moving) self.assertEqual(expected.standard, actual.standard) diff --git a/caom2/caom2/tests/test_observation.py b/caom2/caom2/tests/test_observation.py index b12bb06f..d306dfcd 100644 --- a/caom2/caom2/tests/test_observation.py +++ b/caom2/caom2/tests/test_observation.py @@ -626,9 +626,9 @@ def test_all(self): proposal.keywords.add("optical") self.assertEqual(1, len(proposal.keywords), "Number of keywords") self.assertTrue("optical" in proposal.keywords, "Keyword not found") - self.assertIsNone(proposal.pi_name, "Default PI") - proposal.pi_name = "John Doe" - self.assertEqual("John Doe", proposal.pi_name, "PI") + self.assertIsNone(proposal.pi, "Default PI") + proposal.pi = "John Doe" + self.assertEqual("John Doe", proposal.pi, "PI") self.assertIsNone(proposal.project, "Default PI") proposal.project = "Project A" self.assertEqual("Project A", proposal.project, "Project") @@ -650,9 +650,9 @@ def test_all(self): target = observation.Target("myTarget") self.assertEqual("myTarget", target.name, "target name") - target.target_type = observation.TargetType.FIELD + target.type = observation.TargetType.FIELD self.assertEqual(observation.TargetType.FIELD.name, - target.target_type.name, "target type") + target.type.name, "target type") self.assertEqual(0, len(target.keywords), "Default number of keywords") target.keywords.add("optical") @@ -679,7 +679,7 @@ def test_all(self): observation.TargetType.OBJECT, False, 1.2, {"radio"}, False, target_id='mytargetID') self.assertEqual("myOtherTarget", target.name, "target name") - self.assertEqual(observation.TargetType.OBJECT, target.target_type, + self.assertEqual(observation.TargetType.OBJECT, target.type, "target type") self.assertFalse(target.standard, "Standard") self.assertEqual(1.2, target.redshift, "Redshift") diff --git a/caom2/caom2/tests/test_plane.py b/caom2/caom2/tests/test_plane.py index 87b29fe0..4bd70fbf 100644 --- a/caom2/caom2/tests/test_plane.py +++ b/caom2/caom2/tests/test_plane.py @@ -166,7 +166,7 @@ def test_all(self): class TestPlane(unittest.TestCase): def test_all(self): test_plane = plane.Plane("caom:TEST/obs/ProdID") - self.assertEqual(plane.PlaneURI("caom:TEST/obs/ProdID"), test_plane.uri, "Plane URI") + self.assertEqual("caom:TEST/obs/ProdID", test_plane.uri, "Plane URI") self.assertEqual(0, len(test_plane.artifacts), "Default number of artifacts") self.assertIsNone(test_plane.meta_release, "Default meta release date") @@ -288,48 +288,48 @@ def test_all(self): self.assertTrue(exception, "compute_polarization implemented" " - Testing needed") - -class TestPlaneURI(unittest.TestCase): - def test_all(self): - plane_uri = plane.PlaneURI("caom:GEMINI/12345/3333") - self.assertEqual("caom:GEMINI/12345/3333", plane_uri.uri, - "Plane URI") - - plane_uri = plane.PlaneURI.get_plane_uri( - observation.ObservationURI("caom:CFHT/654321"), - "555") - self.assertEqual("caom:CFHT/654321/555", plane_uri.uri, - "Observation URI") - - exception = False - try: - plane_uri = plane.PlaneURI.get_plane_uri(None, "123") - except TypeError: - exception = True - self.assertTrue(exception, "Missing exception") - - exception = False - try: - plane_uri = plane.PlaneURI.get_plane_uri("GEMINI", None) - except TypeError: - exception = True - self.assertTrue(exception, "Missing exception") - - # wrong scheme - exception = False - try: - plane_uri = plane.PlaneURI("somescheme:GEMINI/12345/3333") - except ValueError: - exception = True - self.assertTrue(exception, "Missing exception") - - exception = False - try: - plane_uri = plane.PlaneURI("caom:GEMINI/12345") - except ValueError: - exception = True - self.assertTrue(exception, "Missing exception") - +# +# class TestPlaneURI(unittest.TestCase): +# def test_all(self): +# plane_uri = plane.PlaneURI("caom:GEMINI/12345/3333") +# self.assertEqual("caom:GEMINI/12345/3333", plane_uri.uri, +# "Plane URI") +# +# plane_uri = plane.PlaneURI.get_plane_uri( +# observation.ObservationURI("caom:CFHT/654321"), +# "555") +# self.assertEqual("caom:CFHT/654321/555", plane_uri.uri, +# "Observation URI") +# +# exception = False +# try: +# plane_uri = plane.PlaneURI.get_plane_uri(None, "123") +# except TypeError: +# exception = True +# self.assertTrue(exception, "Missing exception") +# +# exception = False +# try: +# plane_uri = plane.PlaneURI.get_plane_uri("GEMINI", None) +# except TypeError: +# exception = True +# self.assertTrue(exception, "Missing exception") +# +# # wrong scheme +# exception = False +# try: +# plane_uri = plane.PlaneURI("somescheme:GEMINI/12345/3333") +# except ValueError: +# exception = True +# self.assertTrue(exception, "Missing exception") +# +# exception = False +# try: +# plane_uri = plane.PlaneURI("caom:GEMINI/12345") +# except ValueError: +# exception = True +# self.assertTrue(exception, "Missing exception") +# class TestDataQuality(unittest.TestCase): def test_all(self): @@ -386,19 +386,19 @@ def test_all(self): self.assertIsNone(provenance.reference, "Default reference") self.assertEqual(0, len(provenance.inputs), "Default inputs") - plane_uri1 = plane.PlaneURI("caom:HST/11/00") + plane_uri1 = "caom:HST/11/00" provenance.inputs.add(plane_uri1) self.assertEqual(1, len(provenance.inputs), "Default inputs") self.assertTrue(plane_uri1 in provenance.inputs) - plane_uri2 = plane.PlaneURI("caom:HST/22/00") + plane_uri2 = "caom:HST/22/00" provenance.inputs.add(plane_uri2) self.assertEqual(2, len(provenance.inputs), "Default inputs") self.assertTrue(plane_uri1 in provenance.inputs) self.assertTrue(plane_uri2 in provenance.inputs) # testing duplicates - plane_uri3 = plane.PlaneURI("caom:HST/22/00") + plane_uri3 = "caom:HST/22/00" provenance.inputs.add(plane_uri3) self.assertEqual(2, len(provenance.inputs), "Default inputs") self.assertTrue(plane_uri1 in provenance.inputs) From 33d24a9b48295417fd9729879cc256a33204aa0f Mon Sep 17 00:00:00 2001 From: Adrian Damian Date: Thu, 6 Feb 2025 15:09:16 -0800 Subject: [PATCH 4/6] Ready for code review --- caom2/caom2/artifact.py | 7 +- caom2/caom2/caom_util.py | 2 - caom2/caom2/checksum.py | 29 +- caom2/caom2/chunk.py | 1 - caom2/caom2/common.py | 351 ++++++++++---------- caom2/caom2/diff.py | 2 +- caom2/caom2/obs_reader_writer.py | 60 ++-- caom2/caom2/observation.py | 12 +- caom2/caom2/plane.py | 21 +- caom2/caom2/shape.py | 2 +- caom2/caom2/tests/caom_test_instances.py | 50 +-- caom2/caom2/tests/test_artifact.py | 9 +- caom2/caom2/tests/test_checksum.py | 13 +- caom2/caom2/tests/test_common.py | 95 +++--- caom2/caom2/tests/test_dali.py | 3 - caom2/caom2/tests/test_diffs.py | 2 +- caom2/caom2/tests/test_obs_reader_writer.py | 25 +- caom2/caom2/tests/test_observation.py | 18 +- caom2/caom2/tests/test_plane.py | 15 +- caom2/caom2/tests/test_shape.py | 2 - caom2/setup.cfg | 3 + 21 files changed, 340 insertions(+), 382 deletions(-) diff --git a/caom2/caom2/artifact.py b/caom2/caom2/artifact.py index 0f3831a7..958cf2cc 100644 --- a/caom2/caom2/artifact.py +++ b/caom2/caom2/artifact.py @@ -75,7 +75,7 @@ from . import caom_util from .common import AbstractCaomEntity, CaomObject, compute_bucket -from .common import ChecksumURI, OrderedEnum +from .common import OrderedEnum from .part import Part from .chunk import DataLinkSemantics from datetime import datetime @@ -268,9 +268,8 @@ def content_checksum(self, value): if value is None: self._content_checksum = None else: - caom_util.type_check(value, ChecksumURI, "checksum_uri", False) - # TODO necessary? - self._content_checksum = value.uri + caom_util.type_check(value, str, "checksum_uri", False) + self._content_checksum = value @property def content_release(self): diff --git a/caom2/caom2/caom_util.py b/caom2/caom2/caom_util.py index 94ca60fc..e70af79f 100644 --- a/caom2/caom2/caom_util.py +++ b/caom2/caom2/caom_util.py @@ -81,8 +81,6 @@ from urllib.parse import urlsplit from builtins import int, str as newstr -from . import dali - __all__ = ['TypedList', 'TypedSet', 'TypedOrderedDict', 'ClassProperty', 'URISet', 'validate_uri'] diff --git a/caom2/caom2/checksum.py b/caom2/caom2/checksum.py index 2419d29a..0409d272 100644 --- a/caom2/caom2/checksum.py +++ b/caom2/caom2/checksum.py @@ -78,8 +78,7 @@ from builtins import bytes, int, str from caom2.caom_util import TypedSet, TypedList, TypedOrderedDict, int_32 -from caom2.common import CaomObject, AbstractCaomEntity, ObservationURI -from caom2.common import ChecksumURI +from caom2.common import CaomObject, AbstractCaomEntity from caom2.observation import Observation from .obs_reader_writer import CAOM25_NAMESPACE, CAOM24_NAMESPACE, \ CAOM23_NAMESPACE @@ -150,7 +149,7 @@ def get_meta_checksum(entity): raise AttributeError('AbstractCaomEntity expected') md5 = hashlib.md5() update_caom_checksum(md5, entity) - return ChecksumURI('md5:{}'.format(md5.hexdigest())) + return 'md5:{}'.format(md5.hexdigest()) def get_acc_meta_checksum(entity, no_logging=False): @@ -171,7 +170,7 @@ def get_acc_meta_checksum(entity, no_logging=False): update_acc_checksum(md5, entity) if no_logging: logger.setLevel(log_level) - return ChecksumURI('md5:{}'.format(md5.hexdigest())) + return 'md5:{}'.format(md5.hexdigest()) def update_meta_checksum(obs): @@ -264,10 +263,8 @@ def update_checksum(checksum, value, attribute=''): b = None - if isinstance(value, ObservationURI) or isinstance(value, ChecksumURI): - b = value.uri.encode('utf-8') - elif isinstance(value, CaomObject): - #logger.debug('Process object {}'.format(attribute)) + if isinstance(value, CaomObject): + logger.debug('Process object {}'.format(attribute)) return update_caom_checksum(checksum, value, attribute) elif isinstance(value, bytes): b = value @@ -453,23 +450,23 @@ def _print_diff(orig, actual): mistmatches = 0 if orig.meta_checksum == actual.meta_checksum: print('{}: {} {} == {}'.format(elem_type, orig._id, - orig.meta_checksum.checksum, - actual.meta_checksum.checksum)) + orig.meta_checksum, + actual.meta_checksum)) else: print('{}: {} {} != {} [MISMATCH]'. - format(elem_type, orig._id, orig.meta_checksum.checksum, - actual.meta_checksum.checksum)) + format(elem_type, orig._id, orig.meta_checksum, + actual.meta_checksum)) mistmatches += 1 if elem_type != 'chunk': # do the accumulated checksums if orig.acc_meta_checksum == actual.acc_meta_checksum: print('{}: {} {} == {}'. - format(elem_type, orig._id, orig.acc_meta_checksum.checksum, - actual.acc_meta_checksum.checksum)) + format(elem_type, orig._id, orig.acc_meta_checksum, + actual.acc_meta_checksum)) else: print('{}: {} {} != {} [MISMATCH]'. - format(elem_type, orig._id, orig.acc_meta_checksum.checksum, - actual.acc_meta_checksum.checksum)) + format(elem_type, orig._id, orig.acc_meta_checksum, + actual.acc_meta_checksum)) mistmatches += 1 return mistmatches diff --git a/caom2/caom2/chunk.py b/caom2/caom2/chunk.py index c2f9e152..67f16709 100644 --- a/caom2/caom2/chunk.py +++ b/caom2/caom2/chunk.py @@ -141,7 +141,6 @@ class DataLinkSemantics(OrderedEnum): # CATALOG = 'catalog' - class Chunk(AbstractCaomEntity): """A caom2.Chunk object. A chunk is a peice of file part. diff --git a/caom2/caom2/common.py b/caom2/caom2/common.py index 286b81f7..3e8d1673 100644 --- a/caom2/caom2/common.py +++ b/caom2/caom2/common.py @@ -71,7 +71,7 @@ from datetime import datetime from builtins import int, str -from urllib.parse import SplitResult, urlparse, urlsplit +from urllib.parse import urlparse, urlsplit import logging from . import caom_util @@ -81,8 +81,7 @@ from aenum import Enum -__all__ = ['CaomObject', 'AbstractCaomEntity', 'ObservationURI', 'ChecksumURI', - 'VocabularyTerm', 'compute_bucket'] +__all__ = ['CaomObject', 'AbstractCaomEntity', 'VocabularyTerm', 'compute_bucket'] logger = logging.getLogger('caom2') @@ -166,7 +165,7 @@ def __str__(self): for arg in args]) def __eq__(self, other): - if type(other) == type(self): + if type(other) is type(self): return self.__dict__ == other.__dict__ else: return False @@ -257,7 +256,7 @@ def meta_checksum(self, value): if value is None: self._meta_checksum = None else: - caom_util.type_check(value, ChecksumURI, "meta_checksum", False) + caom_util.type_check(value, str, "meta_checksum", False) self._meta_checksum = value @property @@ -274,7 +273,7 @@ def acc_meta_checksum(self, value): if value is None: self._acc_meta_checksum = None else: - caom_util.type_check(value, ChecksumURI, "acc_meta_checksum", + caom_util.type_check(value, str, "acc_meta_checksum", False) self._acc_meta_checksum = value @@ -367,173 +366,173 @@ def base(self, value): caom_util.type_check(value, bool, "base") self._base = value - -class ObservationURI(CaomObject): - """ Observation URI """ - - _SCHEME = str("caom") - - def __init__(self, uri): - """ - Initializes an Observation instance - - Arguments: - uri : URI corresponding to observation - """ - super(CaomObject, self).__init__() - tmp = urlparse(uri) - - if tmp.scheme != ObservationURI._SCHEME: - raise ValueError( - "uri must be have scheme of {}. received: {}" - .format(ObservationURI._SCHEME, uri)) - if tmp.geturl() != uri: - raise ValueError( - "uri parsing failure. received: {}".format(uri)) - - self._uri = tmp.geturl() - (collection, observation_id) = tmp.path.split("/") - if collection is None: - raise ValueError( - "uri did not contain a collection part. received: {}" - .format(uri)) - caom_util.validate_path_component(self, "collection", collection) - if observation_id is None: - raise ValueError( - "uri did not contain an observation_id part. received: {}" - .format(uri)) - caom_util.validate_path_component(self, "observation_id", - observation_id) - (self._collection, self._observation_id) = (collection, observation_id) - self._print_attributes = ['uri', 'collection', 'observation_id'] - - def _key(self): - return self.uri - - def __hash__(self): - return hash(self._key()) - - def __lt__(self, other): - if not isinstance(other, ObservationURI): - raise ValueError( - 'Cannot compare ObservationURI with {}'.format(type(other))) - return self.uri < other.uri - - def __eq__(self, other): - if not isinstance(other, ObservationURI): - raise ValueError( - 'Cannot compare ObservationURI with {}'.format(type(other))) - return self.uri == other.uri - - @classmethod - def get_observation_uri(cls, collection, observation_id): - """ - Initializes an Observation URI instance - - Arguments: - collection : collection - observation_id : ID of the observation - """ - - caom_util.type_check(collection, str, "collection", override=False) - caom_util.type_check(observation_id, str, "observation_id", - override=False) - - caom_util.validate_path_component(cls, "collection", collection) - caom_util.validate_path_component(cls, "observation_id", - observation_id) - - uri = SplitResult(ObservationURI._SCHEME, "", - collection + "/" + observation_id, - "", "").geturl() - return cls(uri) - - # Properties - - @property - @classmethod - def scheme(cls): - """The scheme defines where this Observation can be looked up. - - Only 'caom' is currently supported.""" - return cls._SCHEME - - @property - def uri(self): - """The uri that the caom service can use to find the observation""" - return self._uri - - @property - def collection(self): - """The collection part of this Observations uri""" - return self._collection - - @property - def observation_id(self): - """The observation_id of this Observations uri""" - return self._observation_id - - -class ChecksumURI(CaomObject): - """ Checksum URI """ - - def __init__(self, uri): - """ - Initializes an Checksum URI instance - - Arguments: - uri : Checksum URI in the format Algorithm:ChecksumValue - """ - super(CaomObject, self).__init__() - # note: urlparse does not recognize scheme in uri of form scheme:val - tmp = uri.split(':', 1) - - # TODO change this raise a ValueError when the rule is being enforced - if len(tmp) < 2: - logger.warning(("A checksum scheme noting the algorithm is " - "required.. received: {}").format(uri)) - algorithm = None - checksum = tmp[0] - else: - algorithm = tmp[0] - checksum = tmp[1] - - if checksum is None: - raise ValueError( - "checksum uri did not contain an checksum part. received: {}" - .format(uri)) - caom_util.validate_path_component(self, "checksum", checksum) - - (self._uri, self._algorithm, self._checksum) = ( - uri, algorithm, checksum) - self._print_attributes = ['uri', 'algorithm', 'checksum'] - - def _key(self): - return self.uri - - def __eq__(self, y): - if isinstance(y, ChecksumURI): - return self._key() == y._key() - return False - - def __hash__(self): - return hash(self._key()) - - def get_bytes(self): - return bytearray.fromhex(self._checksum) - - # Properties - @property - def uri(self): - """The uri that the caom service can use to find the observation""" - return self._uri - - @property - def algorithm(self): - """The checksum algorithm""" - return self._algorithm - - @property - def checksum(self): - """The checksum value""" - return self._checksum +# +# class ObservationURI(CaomObject): +# """ Observation URI """ +# +# _SCHEME = str("caom") +# +# def __init__(self, uri): +# """ +# Initializes an Observation instance +# +# Arguments: +# uri : URI corresponding to observation +# """ +# super(CaomObject, self).__init__() +# tmp = urlparse(uri) +# +# if tmp.scheme != ObservationURI._SCHEME: +# raise ValueError( +# "uri must be have scheme of {}. received: {}" +# .format(ObservationURI._SCHEME, uri)) +# if tmp.geturl() != uri: +# raise ValueError( +# "uri parsing failure. received: {}".format(uri)) +# +# self._uri = tmp.geturl() +# (collection, observation_id) = tmp.path.split("/") +# if collection is None: +# raise ValueError( +# "uri did not contain a collection part. received: {}" +# .format(uri)) +# caom_util.validate_path_component(self, "collection", collection) +# if observation_id is None: +# raise ValueError( +# "uri did not contain an observation_id part. received: {}" +# .format(uri)) +# caom_util.validate_path_component(self, "observation_id", +# observation_id) +# (self._collection, self._observation_id) = (collection, observation_id) +# self._print_attributes = ['uri', 'collection', 'observation_id'] +# +# def _key(self): +# return self.uri +# +# def __hash__(self): +# return hash(self._key()) +# +# def __lt__(self, other): +# if not isinstance(other, ObservationURI): +# raise ValueError( +# 'Cannot compare ObservationURI with {}'.format(type(other))) +# return self.uri < other.uri +# +# def __eq__(self, other): +# if not isinstance(other, ObservationURI): +# raise ValueError( +# 'Cannot compare ObservationURI with {}'.format(type(other))) +# return self.uri == other.uri +# +# @classmethod +# def get_observation_uri(cls, collection, observation_id): +# """ +# Initializes an Observation URI instance +# +# Arguments: +# collection : collection +# observation_id : ID of the observation +# """ +# +# caom_util.type_check(collection, str, "collection", override=False) +# caom_util.type_check(observation_id, str, "observation_id", +# override=False) +# +# caom_util.validate_path_component(cls, "collection", collection) +# caom_util.validate_path_component(cls, "observation_id", +# observation_id) +# +# uri = SplitResult(ObservationURI._SCHEME, "", +# collection + "/" + observation_id, +# "", "").geturl() +# return cls(uri) +# +# # Properties +# +# @property +# @classmethod +# def scheme(cls): +# """The scheme defines where this Observation can be looked up. +# +# Only 'caom' is currently supported.""" +# return cls._SCHEME +# +# @property +# def uri(self): +# """The uri that the caom service can use to find the observation""" +# return self._uri +# +# @property +# def collection(self): +# """The collection part of this Observations uri""" +# return self._collection +# +# @property +# def observation_id(self): +# """The observation_id of this Observations uri""" +# return self._observation_id +# +# +# class ChecksumURI(CaomObject): +# """ Checksum URI """ +# +# def __init__(self, uri): +# """ +# Initializes an Checksum URI instance +# +# Arguments: +# uri : Checksum URI in the format Algorithm:ChecksumValue +# """ +# super(CaomObject, self).__init__() +# # note: urlparse does not recognize scheme in uri of form scheme:val +# tmp = uri.split(':', 1) +# +# # TODO change this raise a ValueError when the rule is being enforced +# if len(tmp) < 2: +# logger.warning(("A checksum scheme noting the algorithm is " +# "required.. received: {}").format(uri)) +# algorithm = None +# checksum = tmp[0] +# else: +# algorithm = tmp[0] +# checksum = tmp[1] +# +# if checksum is None: +# raise ValueError( +# "checksum uri did not contain an checksum part. received: {}" +# .format(uri)) +# caom_util.validate_path_component(self, "checksum", checksum) +# +# (self._uri, self._algorithm, self._checksum) = ( +# uri, algorithm, checksum) +# self._print_attributes = ['uri', 'algorithm', 'checksum'] +# +# def _key(self): +# return self.uri +# +# def __eq__(self, y): +# if isinstance(y, ChecksumURI): +# return self._key() == y._key() +# return False +# +# def __hash__(self): +# return hash(self._key()) +# +# def get_bytes(self): +# return bytearray.fromhex(self._checksum) +# +# # Properties +# @property +# def uri(self): +# """The uri that the caom service can use to find the observation""" +# return self._uri +# +# @property +# def algorithm(self): +# """The checksum algorithm""" +# return self._algorithm +# +# @property +# def checksum(self): +# """The checksum value""" +# return self._checksum diff --git a/caom2/caom2/diff.py b/caom2/caom2/diff.py index 047baf4e..a4015131 100644 --- a/caom2/caom2/diff.py +++ b/caom2/caom2/diff.py @@ -105,7 +105,7 @@ def get_differences(expected, actual, parent=None): """ report = [] - if type(expected) != type(actual): + if type(expected) is not type(actual): report.append( 'Types:: expected \'{}\' actual \'{}\''.format(type(expected), type(actual))) diff --git a/caom2/caom2/obs_reader_writer.py b/caom2/caom2/obs_reader_writer.py index 97482848..294ccd65 100644 --- a/caom2/caom2/obs_reader_writer.py +++ b/caom2/caom2/obs_reader_writer.py @@ -73,7 +73,7 @@ import os import uuid from builtins import str, int -from urllib.parse import urlparse +from enum import Enum from lxml import etree @@ -87,10 +87,9 @@ from . import plane from . import shape from . import wcs -from . import common import logging -from .plane import CalibrationStatus, Ucd +from .plane import CalibrationStatus, Ucd, Polarization DATA_PKG = 'data' @@ -135,7 +134,7 @@ def _to_samples(vertices): last_closed_point = shape.Point(vertex.cval1, vertex.cval2) points.append(last_closed_point) samples.append(shape.Polygon(points)) - points = [] # continue with a new polygon + points = [] # continue with a new polygon else: if not points: # no move so start from the last closed point @@ -213,11 +212,9 @@ def _set_entity_attributes(self, element, ns, caom2_entity): caom2_entity._max_last_modified = caom_util.str2ivoa( element_max_last_modified) if element_meta_checksum: - caom2_entity._meta_checksum = common.ChecksumURI( - element_meta_checksum) + caom2_entity._meta_checksum = element_meta_checksum if element_acc_meta_checksum: - caom2_entity._acc_meta_checksum = common.ChecksumURI( - element_acc_meta_checksum) + caom2_entity._acc_meta_checksum = element_acc_meta_checksum if element_meta_producer: caom2_entity._meta_producer = element_meta_producer @@ -544,9 +541,9 @@ def _get_environment(self, element_tag, parent, ns, required): return environment def _add_members(self, members, parent, ns): - """Create ObservationURI objects from an XML representation of - ObservationURI elements found in members element, and add them to the - set of ObservationURI's + """Create observation URI objects from an XML representation of + observation URI elements found in members element, and add them to the + set of observation URI's Arguments: members : Set of member's from the parent Observation object @@ -561,11 +558,11 @@ def _add_members(self, members, parent, ns): if self.version < 25: for member_element in el.iterchildren( "{" + ns + "}observationURI"): - members.add(observation.ObservationURI(member_element.text)) + members.add(member_element.text) else: for member_element in el.iterchildren( "{" + ns + "}member"): - members.add(observation.ObservationURI(member_element.text)) + members.add(member_element.text) def _add_inputs(self, inputs, parent, ns): """Create URI objects from an XML representation of the planeURI @@ -1488,7 +1485,6 @@ def _get_polarization(self, element_tag, parent, ns, required): el = self._get_child_element(element_tag, parent, ns, required) if el is None: return None - polarization = plane.Polarization() _pstates_el = self._get_child_element("states", el, ns, False) if _pstates_el is not None: _states = list() @@ -1496,11 +1492,12 @@ def _get_polarization(self, element_tag, parent, ns, required): _pstate = _pstate_el.text _state = plane.PolarizationState(_pstate) _states.append(_state) - polarization.states = _states - polarization.dimension = self._get_child_text_as_int("dimension", el, - ns, False) + else: + return None + dimension = self._get_child_text_as_int("dimension", el, + ns, False) - return polarization + return Polarization(dimension=dimension, states=_states) def _get_shape(self, element_tag, parent, ns, required): shape_element = self._get_child_element(element_tag, parent, ns, @@ -1513,7 +1510,7 @@ def _get_shape(self, element_tag, parent, ns, required): samples = None if self.version < 25: samples_element = self._get_child_element("samples", shape_element, - ns, True) + ns, True) vertices = list() self._add_vertices(vertices, samples_element, ns) samples = _to_samples(vertices) @@ -1556,7 +1553,7 @@ def _get_polygon(self, ns, shape_element): def _add_energy_bands(self, energy_bands, parent, ns): """Create EnergyBand objects from an XML representation of - ObservationURI elements found in energy_band element, and add them to + observation URI elements found in energy_band element, and add them to the set of energy_bads Arguments: @@ -1756,8 +1753,8 @@ def _add_artifacts(self, artifacts, parent, ns): "Parsed artifact URI bucket {} does not match calculated artifact URI bucket {}". format(sub, _artifact.uri_bucket)) _artifact.description_id = self._get_child_text("descriptionID", - artifact_element, - ns, False) + artifact_element, + ns, False) cr = self._get_child_text("contentRelease", artifact_element, ns, False) _artifact.content_release = caom_util.str2ivoa(cr) @@ -1774,8 +1771,7 @@ def _add_artifacts(self, artifacts, parent, ns): artifact_element, ns, False) if content_checksum: - _artifact.content_checksum = common.ChecksumURI( - content_checksum) + _artifact.content_checksum = content_checksum self._add_parts(_artifact.parts, artifact_element, ns) self._set_entity_attributes(artifact_element, ns, _artifact) artifacts[_artifact.uri] = _artifact @@ -2000,10 +1996,10 @@ def write(self, obs, out): self._add_element("collection", obs.collection, obs_element) if self._output_version < 25: - observation_id = obs.uri.uri.split('/')[-1] + observation_id = obs.uri.split('/')[-1] self._add_element("observationID", observation_id, obs_element) else: - self._add_element("uri", obs.uri.uri, obs_element) + self._add_element("uri", obs.uri, obs_element) self._add_element('uriBucket', obs.uri_bucket, obs_element) self._add_datetime_element("metaRelease", obs.meta_release, obs_element) @@ -2060,10 +2056,10 @@ def _add_entity_attributes(self, entity, element): caom_util.date2ivoa(entity._max_last_modified), element) if entity._meta_checksum is not None: self._add_attribute( - "metaChecksum", entity._meta_checksum.uri, element) + "metaChecksum", entity._meta_checksum, element) if entity._acc_meta_checksum is not None: self._add_attribute( - "accMetaChecksum", entity._acc_meta_checksum.uri, element) + "accMetaChecksum", entity._acc_meta_checksum, element) if self._output_version >= 24: if entity._meta_producer is not None: @@ -2173,7 +2169,7 @@ def _add_members_element(self, members, parent): member_element = self._get_caom_element("observationURI", element) else: member_element = self._get_caom_element("member", element) - member_element.text = member.uri + member_element.text = member def _add_groups_element(self, name, groups, parent): if self._output_version < 24: @@ -2418,6 +2414,8 @@ def _add_polarization_element(self, polarization, parent): if polarization is None: return element = self._get_caom_element("polarization", parent) + if not polarization.states: + raise AttributeError("Polarization.states missing") if polarization.states: _pstates_el = self._get_caom_element("states", element) for _state in polarization.states: @@ -2430,7 +2428,7 @@ def _add_bounds_and_samples(self, position, parent): shape_element = self._add_shape_element("bounds", parent, position.bounds) if self._output_version < 25: - if isinstance(position.bounds,shape.Circle): + if isinstance(position.bounds, shape.Circle): # samples not supported for circles so need to make sure that # samples is the same with bounds if len(position.samples.shapes) > 1 or position.samples.shapes[0] != position.bounds: @@ -2949,6 +2947,8 @@ def _add_element(self, name, value, parent): element = self._get_caom_element(name, parent) if isinstance(value, str): element.text = value + elif isinstance(value, Enum): + element.text = value.value else: element.text = str(value) diff --git a/caom2/caom2/observation.py b/caom2/caom2/observation.py index 4d2772ec..01c756cc 100644 --- a/caom2/caom2/observation.py +++ b/caom2/caom2/observation.py @@ -76,19 +76,18 @@ from . import caom_util from .caom_util import int_32, validate_uri -from .common import AbstractCaomEntity, CaomObject, ObservationURI, \ - VocabularyTerm, OrderedEnum, compute_bucket +from .common import AbstractCaomEntity, CaomObject, VocabularyTerm, OrderedEnum, compute_bucket from .common import _CAOM_DATA_PRODUCT_TYPE_NS from .plane import Plane from .shape import Point -from urllib.parse import urlsplit, urlparse +from urllib.parse import urlsplit with warnings.catch_warnings(): warnings.simplefilter('ignore') from aenum import Enum __all__ = ['ObservationIntentType', 'Status', 'TargetType', - 'Observation', 'ObservationURI', 'Algorithm', 'SimpleObservation', + 'Observation', 'Algorithm', 'SimpleObservation', 'DerivedObservation', 'Environment', 'Instrument', 'Proposal', 'Requirements', 'Target', 'TargetPosition', 'Telescope', 'CompositeObservation'] @@ -208,7 +207,7 @@ def __init__(self, self.collection = collection validate_uri(uri) - self._uri = ObservationURI(uri) + self._uri = uri self._uri_bucket = compute_bucket(uri) if not algorithm: raise AttributeError('Algorithm required') @@ -637,7 +636,7 @@ def __init__(self, planes=planes, environment=environment, target_position=target_position) - self._members = caom_util.TypedSet(ObservationURI, ) + self._members = caom_util.TypedSet(str, ) @property def algorithm(self): @@ -1237,7 +1236,6 @@ def tracking_mode(self): def tracking_mode(self, value): if isinstance(value, str): value = Tracking(value) - caom_util.type_check(value, Tracking, "tracking_mode") self._tracking_mode = value @property diff --git a/caom2/caom2/plane.py b/caom2/caom2/plane.py index ff57f58e..1ab6fe56 100644 --- a/caom2/caom2/plane.py +++ b/caom2/caom2/plane.py @@ -71,7 +71,7 @@ from datetime import datetime from builtins import str, int -from urllib.parse import SplitResult, urlsplit, urlparse +from urllib.parse import urlsplit from deprecated import deprecated from caom2.caom_util import int_32, validate_uri @@ -79,8 +79,7 @@ from . import shape from . import wcs from .artifact import Artifact -from .common import AbstractCaomEntity, CaomObject, ObservationURI,\ - VocabularyTerm, OrderedEnum +from .common import AbstractCaomEntity, CaomObject, VocabularyTerm, OrderedEnum from .common import _CAOM_DATA_PRODUCT_TYPE_NS import warnings with warnings.catch_warnings(): @@ -258,19 +257,19 @@ class Visibility(CaomObject): def __init__(self, distance, distribution_eccentricity, distribution_fill): if distance is not None: - caom_util.type_check(distance, shape.Interval,'distance') + caom_util.type_check(distance, shape.Interval, 'distance') else: raise ValueError("Visibility.distance cannot be None") self._distance = distance if distribution_eccentricity is not None: - caom_util.type_check(distribution_eccentricity, float,'distribution_eccentricity') + caom_util.type_check(distribution_eccentricity, float, 'distribution_eccentricity') else: raise ValueError("Visibility.distribution_eccentricity cannot be None") self._distribution_eccentricity = distribution_eccentricity if distribution_fill is not None: - caom_util.type_check(distribution_fill, float,'distribution_fill') + caom_util.type_check(distribution_fill, float, 'distribution_fill') else: raise ValueError("Visibility.distribution_fill cannot be None") self._distribution_fill = distribution_fill @@ -1167,13 +1166,12 @@ def samples(self, value): if value is None: raise AttributeError('samples in Energy cannot be None') else: - caom_util.type_check(value, list,'samples') + caom_util.type_check(value, list, 'samples') if len(value) == 0: raise ValueError('samples in Energy cannot be empty') # TODO - could check that the intervals are within the bounds? self._samples = value - @property def dimension(self): """DIMENSION (NUMBER OF PIXELS) ALONG ENERGY AXIS.""" @@ -1354,9 +1352,9 @@ def states(self): @states.setter def states(self, value): - if value is not None: - caom_util.type_check(value, list, 'states', - override=False) + if not value: + raise AttributeError('Polarization.state required') + caom_util.type_check(value, list, 'states', override=False) self._states = value @@ -1428,7 +1426,6 @@ def samples(self, value): # TODO - could check that the intervals are within the bounds? self._samples = value - @property def dimension(self): """Number of pixel in the time direction, normally 1. diff --git a/caom2/caom2/shape.py b/caom2/caom2/shape.py index 06a2853b..29fb8a1c 100644 --- a/caom2/caom2/shape.py +++ b/caom2/caom2/shape.py @@ -244,7 +244,7 @@ def upper(self, value): self._upper = value -Interval = dali.Interval # Moved to dali +Interval = dali.Interval # Moved to dali class Point(common.CaomObject): diff --git a/caom2/caom2/tests/caom_test_instances.py b/caom2/caom2/tests/caom_test_instances.py index ed8d9f1e..7115622b 100644 --- a/caom2/caom2/tests/caom_test_instances.py +++ b/caom2/caom2/tests/caom_test_instances.py @@ -73,7 +73,7 @@ import uuid from builtins import int -from caom2 import artifact, MultiShape +from caom2 import artifact, MultiShape, Polarization from caom2 import caom_util from caom2 import chunk from caom2 import common @@ -169,10 +169,8 @@ def get_simple_observation(self, short_uuid=False): if self.caom_version >= 23: simple_observation.max_last_modified =\ common.get_current_ivoa_time() - simple_observation.meta_checksum = common.ChecksumURI( - "md5:9882dbbf9cadc221019b712fd402bcbd") - simple_observation.acc_meta_checksum = common.ChecksumURI( - "md5:844ce247db0844ad9f721430c80e7a21") + simple_observation.meta_checksum = "md5:9882dbbf9cadc221019b712fd402bcbd" + simple_observation.acc_meta_checksum = "md5:844ce247db0844ad9f721430c80e7a21" if self.caom_version >= 24: simple_observation.meta_read_groups.add( "ivo://cadc.nrc.ca/groups?A") @@ -209,10 +207,8 @@ def get_composite_observation(self, short_uuid=False): if self.caom_version >= 23: composite_observation.max_last_modified = \ common.get_current_ivoa_time() - composite_observation.meta_checksum = common.ChecksumURI( - "md5:9882dbbf9cadc221019b712fd402bcbd") - composite_observation.acc_meta_checksum = common.ChecksumURI( - "md5:844ce247db0844ad9f721430c80e7a21") + composite_observation.meta_checksum = "md5:9882dbbf9cadc221019b712fd402bcbd" + composite_observation.acc_meta_checksum = "md5:844ce247db0844ad9f721430c80e7a21" if self.depth > 1: composite_observation.planes.update(self.get_planes()) composite_observation.members.update(self.get_members()) @@ -242,10 +238,8 @@ def get_derived_observation(self, short_uuid=False): common.get_current_ivoa_time() derived_observation.max_last_modified = \ common.get_current_ivoa_time() - derived_observation.meta_checksum = common.ChecksumURI( - "md5:9882dbbf9cadc221019b712fd402bcbd") - derived_observation.acc_meta_checksum = common.ChecksumURI( - "md5:844ce247db0844ad9f721430c80e7a21") + derived_observation.meta_checksum = "md5:9882dbbf9cadc221019b712fd402bcbd" + derived_observation.acc_meta_checksum = "md5:844ce247db0844ad9f721430c80e7a21" derived_observation.meta_read_groups.add( "ivo://cadc.nrc.ca/groups?A") derived_observation.meta_read_groups.add( @@ -310,9 +304,7 @@ def get_environment(self): return env def get_members(self): - members = caom_util.TypedSet( - observation.ObservationURI, - observation.ObservationURI("caom:foo/bar")) + members = caom_util.TypedSet(str, "caom:foo/bar") return members def get_planes(self): @@ -338,10 +330,8 @@ def get_planes(self): if self.caom_version >= 23: _plane.creator_id = "ivo://cadc.nrc.ca?testuser" _plane.max_last_modified = common.get_current_ivoa_time() - _plane.meta_checksum = common.ChecksumURI( - "md5:9882dbbf9cadc221019b712fd402bcbd") - _plane.acc_meta_checksum = common.ChecksumURI( - "md5:844ce247db0844ad9f721430c80e7a21") + _plane.meta_checksum = "md5:9882dbbf9cadc221019b712fd402bcbd" + _plane.acc_meta_checksum = "md5:844ce247db0844ad9f721430c80e7a21" if s == 'polygon': _plane.position = self.get_poly_position() if s == 'circle': @@ -442,14 +432,8 @@ def get_custom(self): custom.dimension = 1 def get_polarization(self): - polarization = plane.Polarization() - p_states = [plane.PolarizationState.LL, plane.PolarizationState.XY] - - polarization.dimension = 2 - polarization.polarization_states = p_states - - return polarization + return Polarization(dimension=2, states=p_states) def get_provenance(self): provenance = plane.Provenance("name") @@ -493,10 +477,8 @@ def get_artifacts(self): _artifact.last_modified = common.get_current_ivoa_time() if self.caom_version >= 23: _artifact.max_last_modified = common.get_current_ivoa_time() - _artifact.meta_checksum = common.ChecksumURI( - "md5:9882dbbf9cadc221019b712fd402bcbd") - _artifact.acc_meta_checksum = common.ChecksumURI( - "md5:844ce247db0844ad9f721430c80e7a21") + _artifact.meta_checksum = "md5:9882dbbf9cadc221019b712fd402bcbd" + _artifact.acc_meta_checksum = "md5:844ce247db0844ad9f721430c80e7a21" if self.depth > 3: for k, v in self.get_parts().items(): _artifact.parts[k] = v @@ -539,10 +521,8 @@ def get_chunks(self): _chunk.last_modified = common.get_current_ivoa_time() if self.caom_version >= 23: _chunk.max_last_modified = common.get_current_ivoa_time() - _chunk.meta_checksum = common.ChecksumURI( - "md5:9882dbbf9cadc221019b712fd402bcbd") - _chunk.acc_meta_checksum = common.ChecksumURI( - "md5:844ce247db0844ad9f721430c80e7a21") + _chunk.meta_checksum = "md5:9882dbbf9cadc221019b712fd402bcbd" + _chunk.acc_meta_checksum = "md5:844ce247db0844ad9f721430c80e7a21" if self.caom_version >= 24: _chunk.custom_axis = 3 _chunk.custom = self.get_custom_wcs() diff --git a/caom2/caom2/tests/test_artifact.py b/caom2/caom2/tests/test_artifact.py index 42e76176..4b63edaf 100644 --- a/caom2/caom2/tests/test_artifact.py +++ b/caom2/caom2/tests/test_artifact.py @@ -2,7 +2,7 @@ # ****************** CANADIAN ASTRONOMY DATA CENTRE ******************* # ************* CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** # -# (c) 2022. (c) 2022. +# (c) 2025. (c) 2025. # Government of Canada Gouvernement du Canada # National Research Council Conseil national de recherches # Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 @@ -73,7 +73,6 @@ from urllib.parse import urlparse from .. import artifact -from .. import common from .. import part @@ -129,7 +128,7 @@ def test_all(self): self.assertIsNone(test_artifact.content_checksum, "Default content checksum") - cs_uri = common.ChecksumURI("md5:e30580c1db513487f495fba09f64600e") + cs_uri = "md5:e30580c1db513487f495fba09f64600e" test_artifact.content_checksum = cs_uri self.assertEqual(test_artifact.content_checksum, cs_uri, "Content checksum") @@ -179,5 +178,5 @@ def test_all(self): # TODO re-enable when check enforced # with self.assertRaises(ValueError): - test_artifact.content_checksum = common.ChecksumURI('0x1234') - assert test_artifact.content_checksum.uri == '0x1234' + test_artifact.content_checksum = '0x1234' + assert test_artifact.content_checksum == '0x1234' diff --git a/caom2/caom2/tests/test_checksum.py b/caom2/caom2/tests/test_checksum.py index ba9dd7da..cdefd15c 100644 --- a/caom2/caom2/tests/test_checksum.py +++ b/caom2/caom2/tests/test_checksum.py @@ -119,14 +119,13 @@ def test_primitive_checksum(): update_checksum(md5, value, False) assert ('0fec383169e99d1a6bebd89d1cd8fad9' == md5.hexdigest()) md5 = hashlib.md5() - value = str2ivoa('2012-07-11T13:26:37.123') + value = str2ivoa('2012-07-11T13:26:37.123200') update_checksum(md5, value, False) - assert ('aedbcf5e27a17fc2daa5a0e0d7840009' == md5.hexdigest()) - # ensure that the milliseconds part is not part of checksum + assert ('9f8af3a440b6e1c8e2a7ea86d90685ac' == md5.hexdigest()) md5 = hashlib.md5() - value = str2ivoa('2012-07-11T13:26:37.000') + value = str2ivoa('2012-07-11T13:26:37.000000') update_checksum(md5, value, False) - assert ('aedbcf5e27a17fc2daa5a0e0d7840009' == md5.hexdigest()) + assert ('b35eea8d6e70a117ae7804f4e0f6cf58' == md5.hexdigest()) md5 = hashlib.md5() value = str('ad:file') update_checksum(md5, value, False) @@ -352,7 +351,7 @@ def atest_round_trip(): def test_checksum_diff(): for source_file_path in \ [os.path.join(THIS_DIR, TEST_DATA, x) for x in ['SampleDerived-CAOM-2.5.xml']]: - #['SampleDerived-CAOM-2.4.xml', 'SampleComposite-CAOM-2.3.xml']]: + # TODO Maybe ['SampleDerived-CAOM-2.4.xml', 'SampleComposite-CAOM-2.3.xml']]: logging.debug(source_file_path) output_file = tempfile.NamedTemporaryFile() sys.argv = 'caom2_checksum -d -o {} {}'.format( @@ -368,7 +367,7 @@ def test_checksum_diff(): assert 'plane' in output assert 'observation' in output - # original observation and the one outputed should be identical + # original observation and the one output should be identical reader = obs_reader_writer.ObservationReader() expected = reader.read(source_file_path) actual = reader.read(output_file.name) diff --git a/caom2/caom2/tests/test_common.py b/caom2/caom2/tests/test_common.py index cc298e23..b5217e57 100644 --- a/caom2/caom2/tests/test_common.py +++ b/caom2/caom2/tests/test_common.py @@ -2,7 +2,7 @@ # ****************** CANADIAN ASTRONOMY DATA CENTRE ******************* # ************* CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** # -# (c) 2022. (c) 2022. +# (c) 2025. (c) 2025. # Government of Canada Gouvernement du Canada # National Research Council Conseil national de recherches # Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 @@ -68,7 +68,6 @@ """ Defines TestCaom2IdGenerator class """ -import binascii import unittest from .. import artifact @@ -99,7 +98,7 @@ def test_all(self): test_part = part.Part("part") print(test_part._id, test_part._last_modified) - plane_uri = '{}/{}'.format(test_observation.uri.uri, "obs") + plane_uri = '{}/{}'.format(test_observation.uri, "obs") test_plane = plane.Plane(plane_uri) print(test_plane._id, test_plane._last_modified) @@ -111,10 +110,8 @@ def test_all(self): "acc_meta_checksum null") d1 = common.get_current_ivoa_time() d2 = common.get_current_ivoa_time() - cs_uri_meta = common.ChecksumURI( - "md5:e30580c1db513487f495fba09f64600e") - cs_uri_acc = common.ChecksumURI( - "sha1:7e2b74edf8ff7ddfda5ee3917dc65946b515b1f7") + cs_uri_meta = "md5:e30580c1db513487f495fba09f64600e" + cs_uri_acc = "sha1:7e2b74edf8ff7ddfda5ee3917dc65946b515b1f7" test_plane.last_modified = d1 test_plane.max_last_modified = d2 test_plane.meta_checksum = cs_uri_meta @@ -138,45 +135,45 @@ def test_all(self): with self.assertRaises(NotImplementedError): test_artifact.compute_meta_checksum() - -class TestObservationURI(unittest.TestCase): - def test_all(self): - obs_uri = observation.ObservationURI("caom:GEMINI/12345") - self.assertEqual("caom:GEMINI/12345", obs_uri.uri, "Observation URI") - self.assertEqual("GEMINI", obs_uri.collection, "Collection") - self.assertEqual("12345", obs_uri.observation_id, "Observation ID") - - obs_uri = observation.ObservationURI.get_observation_uri("CFHT", - "654321") - self.assertEqual("caom:CFHT/654321", obs_uri.uri, "Observation URI") - self.assertEqual("CFHT", obs_uri.collection, "Collection") - self.assertEqual("654321", obs_uri.observation_id, "Observation ID") - - exception = False - try: - obs_uri = observation.ObservationURI.get_observation_uri(None, - "123") - except TypeError: - exception = True - self.assertTrue(exception, "Missing exception") - - exception = False - try: - obs_uri = observation.ObservationURI.get_observation_uri("GEMINI", - None) - except TypeError: - exception = True - self.assertTrue(exception, "Missing exception") - - -class TestChecksumURI(unittest.TestCase): - def test_all(self): - cs_uri = common.ChecksumURI("md5:e30580c1db513487f495fba09f64600e") - self.assertEqual("md5:e30580c1db513487f495fba09f64600e", cs_uri.uri, - "Checksum URI") - self.assertEqual("md5", cs_uri.algorithm, "Algorithm") - self.assertEqual("e30580c1db513487f495fba09f64600e", cs_uri.checksum, - "Checksum") - self.assertEqual(binascii.hexlify( - bytearray.fromhex("e30580c1db513487f495fba09f64600e")), - binascii.hexlify(cs_uri.get_bytes()), "Round trip") +# +# class TestObservationURI(unittest.TestCase): +# def test_all(self): +# obs_uri = "caom:GEMINI/12345" +# self.assertEqual("caom:GEMINI/12345", obs_uri, "Observation URI") +# self.assertEqual("GEMINI", obs_uri.collection, "Collection") +# self.assertEqual("12345", obs_uri.observation_id, "Observation ID") +# +# obs_uri = observation.ObservationURI.get_observation_uri("CFHT", +# "654321") +# self.assertEqual("caom:CFHT/654321", obs_uri, "Observation URI") +# self.assertEqual("CFHT", obs_uri.collection, "Collection") +# self.assertEqual("654321", obs_uri.observation_id, "Observation ID") +# +# exception = False +# try: +# obs_uri = observation.ObservationURI.get_observation_uri(None, +# "123") +# except TypeError: +# exception = True +# self.assertTrue(exception, "Missing exception") +# +# exception = False +# try: +# obs_uri = observation.ObservationURI.get_observation_uri("GEMINI", +# None) +# except TypeError: +# exception = True +# self.assertTrue(exception, "Missing exception") +# +# +# class TestChecksumURI(unittest.TestCase): +# def test_all(self): +# cs_uri = "md5:e30580c1db513487f495fba09f64600e" +# self.assertEqual("md5:e30580c1db513487f495fba09f64600e", cs_uri, +# "Checksum URI") +# self.assertEqual("md5", cs_uri.algorithm, "Algorithm") +# self.assertEqual("e30580c1db513487f495fba09f64600e", cs_uri.checksum, +# "Checksum") +# self.assertEqual(binascii.hexlify( +# bytearray.fromhex("e30580c1db513487f495fba09f64600e")), +# binascii.hexlify(cs_uri.get_bytes()), "Round trip") diff --git a/caom2/caom2/tests/test_dali.py b/caom2/caom2/tests/test_dali.py index bfa0f79a..6f7e6279 100644 --- a/caom2/caom2/tests/test_dali.py +++ b/caom2/caom2/tests/test_dali.py @@ -66,11 +66,8 @@ # *********************************************************************** # -import math -import pytest import unittest -from .. import shape from .. import dali diff --git a/caom2/caom2/tests/test_diffs.py b/caom2/caom2/tests/test_diffs.py index 803d2dcc..df7b0cab 100644 --- a/caom2/caom2/tests/test_diffs.py +++ b/caom2/caom2/tests/test_diffs.py @@ -69,7 +69,7 @@ import os import unittest -from caom2 import Point, shape, SegmentType, Position, MultiShape +from caom2 import Point, shape, Position, MultiShape from .. import diff from .. import observation diff --git a/caom2/caom2/tests/test_obs_reader_writer.py b/caom2/caom2/tests/test_obs_reader_writer.py index 7b469029..7ffe1c02 100644 --- a/caom2/caom2/tests/test_obs_reader_writer.py +++ b/caom2/caom2/tests/test_obs_reader_writer.py @@ -77,7 +77,6 @@ from . import caom_test_instances from .xml_compare import xml_compare -from .. import caom_util from .. import dali from .. import obs_reader_writer from .. import observation @@ -170,7 +169,7 @@ def test_invalid_uuid(self): pass def test_complete_simple(self): - for version in (23, 24): # TODO 25 + for version in (23, 24, 25): for i in range(1, 6): print("Test Complete Simple {} version {}".format(i, version)) # CoordBounds2D as CoordCircle2D @@ -188,7 +187,7 @@ def test_complete_simple(self): def test_minimal_derived(self): # * composite for the pre-2.4 versions - for version in (23, 24): # TODO 25 + for version in (23, 24, 25): for i in range(1, 6): if version >= 24: print("Test Minimal Derived {} version {}". @@ -561,16 +560,16 @@ def compare_polarization(self, expected, actual): self.assertIsNone(expected, "polarization") else: self.assertEqual(expected.dimension, actual.dimension, "dimension") - if expected.polarization_states is None: - self.assertIsNone(actual.polarization_states, - "polarization_states") + if expected.states is None: + self.assertIsNone(actual.states, + "polarization.states") else: - self.assertEqual(len(expected.polarization_states), - len(actual.polarization_states), - "different number of polarization_states") - for index, state in enumerate(expected.polarization_states): - self.assertEqual(state, actual.polarization_states[index], - "polarization_state") + self.assertEqual(len(expected.states), + len(actual.states), + "different number of polarization.states") + for index, state in enumerate(expected.states): + self.assertEqual(state, actual.states[index], + "polarization state") def compare_custom(self, expected, actual): if expected is None: @@ -1073,7 +1072,7 @@ def test_roundtrip_floats(self): shape.Point(-0.00518884856598203, -0.00518884856598), 'test') # create empty energy - plane_uri = '{}/{}'.format(expected_obs.uri.uri, 'planeID') + plane_uri = '{}/{}'.format(expected_obs.uri, 'planeID') pl = plane.Plane(plane_uri) pl.energy = plane.Energy(dali.Interval(1.0, 2.0), [dali.Interval(1.0, 2.0)]) expected_obs.planes[pl.uri] = pl diff --git a/caom2/caom2/tests/test_observation.py b/caom2/caom2/tests/test_observation.py index d306dfcd..8b29aabf 100644 --- a/caom2/caom2/tests/test_observation.py +++ b/caom2/caom2/tests/test_observation.py @@ -119,7 +119,7 @@ def test_all(self): algorithm = observation.Algorithm("myAlg") obs = observation.Observation("GSA", "caom:GSA/A12345", algorithm) self.assertEqual("GSA", obs.collection, "Collection") - self.assertEqual(observation.ObservationURI("caom:GSA/A12345"), obs.uri, "Observation URI") + self.assertEqual("caom:GSA/A12345", obs.uri, "Observation URI") self.assertEqual(algorithm, obs.algorithm, "Algorithm") new_algorithm = observation.Algorithm("myNewAlg") @@ -205,7 +205,7 @@ def test_all(self): observation.Observation( obs.collection, - obs.uri.uri, + obs.uri, obs.algorithm, planes=obs.planes, sequence_number=obs.sequence_number, @@ -226,7 +226,7 @@ def test_all(self): observation.SimpleObservation._DEFAULT_ALGORITHM_NAME) obs = observation.SimpleObservation("GSA", "caom:GSA/A12345") self.assertEqual("GSA", obs.collection, "Collection") - self.assertEqual(observation.ObservationURI("caom:GSA/A12345"), obs.uri, "Observation URI") + self.assertEqual("caom:GSA/A12345", obs.uri, "Observation URI") self.assertEqual(algorithm, obs.algorithm, "Algorithm") obs.algorithm = algorithm @@ -337,7 +337,7 @@ def test_complete_init(self): self.assertEqual(collection, obs.collection, "Collection") self.assertIsNotNone(obs.uri, "Observation URI") - self.assertEqual(observation.ObservationURI(uri), obs.uri, "Observation URI") + self.assertEqual(uri, obs.uri, "Observation URI") self.assertIsNotNone(obs.algorithm, "Algorithm") self.assertEqual(algorithm, obs.algorithm, "Algorithm") @@ -378,7 +378,7 @@ def test_all(self): algorithm = observation.Algorithm("mozaic") obs = observation.CompositeObservation("GSA", "caom:GSA/A12345", algorithm) self.assertEqual("GSA", obs.collection, "Collection") - self.assertEqual(observation.ObservationURI("caom:GSA/A12345"), obs.uri, "Observation URI") + self.assertEqual("caom:GSA/A12345", obs.uri, "Observation URI") self.assertEqual(algorithm, obs.algorithm, "Algorithm") obs.algorithm = algorithm self.assertEqual(algorithm, obs.algorithm, "Algorithm") @@ -401,19 +401,19 @@ def test_all(self): self.assertTrue(exception, "Missing exception") self.assertEqual(0, len(obs.members), "Members") - observation_uri1 = observation.ObservationURI("caom:collection/obsID") + observation_uri1 = "caom:collection/obsID" obs.members.add(observation_uri1) self.assertEqual(1, len(obs.members), "Members") self.assertTrue(observation_uri1 in obs.members) - observation_uri2 = observation.ObservationURI("caom:collection/obsID2") + observation_uri2 = "caom:collection/obsID2" obs.members.add(observation_uri2) self.assertEqual(2, len(obs.members), "Members") self.assertTrue(observation_uri1 in obs.members) self.assertTrue(observation_uri2 in obs.members) # duplicates - observation_uri3 = observation.ObservationURI("caom:collection/obsID") + observation_uri3 = "caom:collection/obsID" obs.members.add(observation_uri3) self.assertEqual(2, len(obs.members), "Members") self.assertTrue(observation_uri1 in obs.members) @@ -518,7 +518,7 @@ def test_complete_init(self): self.assertEqual(collection, obs.collection, "Collection") self.assertIsNotNone(obs.uri, "Observation URI") - self.assertEqual(observation.ObservationURI(uri), obs.uri, "Observation URI") + self.assertEqual(uri, obs.uri, "Observation URI") self.assertIsNotNone(obs.algorithm, "Algorithm") self.assertEqual(algorithm, obs.algorithm, "Algorithm") diff --git a/caom2/caom2/tests/test_plane.py b/caom2/caom2/tests/test_plane.py index 4bd70fbf..47967f04 100644 --- a/caom2/caom2/tests/test_plane.py +++ b/caom2/caom2/tests/test_plane.py @@ -71,10 +71,8 @@ import unittest from datetime import datetime -from .. import artifact -from .. import caom_util +from .. import artifact, PolarizationState from .. import chunk -from .. import observation from .. import plane from .. import dali from .. import shape @@ -331,6 +329,7 @@ def test_all(self): # self.assertTrue(exception, "Missing exception") # + class TestDataQuality(unittest.TestCase): def test_all(self): self.assertRaises(TypeError, plane.DataQuality, "string") @@ -526,12 +525,12 @@ def test_setters(self): class TestPolarizaton(unittest.TestCase): def test_all(self): - polarization = plane.Polarization() - - self.assertIsNone(polarization.dimension, - "Default polarization dimension") + polarization = plane.Polarization(dimension=2, states=[PolarizationState.I, PolarizationState.Q]) - # TODO add test for state + self.assertEqual(2, polarization.dimension, "Default polarization dimension") + self.assertEqual(2, len(polarization.states), "Polarization states") + self.assertTrue(PolarizationState.I in polarization.states, "I state") + self.assertTrue(PolarizationState.Q in polarization.states, "Q state") class TestTime(unittest.TestCase): diff --git a/caom2/caom2/tests/test_shape.py b/caom2/caom2/tests/test_shape.py index f3a8ec2c..237dacb2 100644 --- a/caom2/caom2/tests/test_shape.py +++ b/caom2/caom2/tests/test_shape.py @@ -127,5 +127,3 @@ def test_all(self): point = shape.Point(1.0, 2.0) self.assertEqual(point.cval1, 1.0) self.assertEqual(point.cval2, 2.0) - - diff --git a/caom2/setup.cfg b/caom2/setup.cfg index 0d349833..8f1e2593 100644 --- a/caom2/setup.cfg +++ b/caom2/setup.cfg @@ -25,6 +25,9 @@ testpaths = caom2 [bdist_wheel] universal=1 +[flake8] +max-line-length = 120 + [metadata] package_name = caom2 description = CAOM-2.4 library From 9cf6e7292ca24b5b424d735117ec0b2c071633d3 Mon Sep 17 00:00:00 2001 From: Adrian Damian Date: Fri, 7 Feb 2025 22:07:24 -0800 Subject: [PATCH 5/6] Fixed caom2utils --- caom2/caom2/artifact.py | 2 +- caom2/caom2/common.py | 4 +- caom2repo/caom2repo/tests/test_core.py | 4 +- caom2utils/caom2utils/blueprints.py | 1 - caom2utils/caom2utils/caom2blueprint.py | 36 +- caom2utils/caom2utils/parsers.py | 28 +- caom2utils/caom2utils/polygonvalidator.py | 83 +---- .../tests/data/brite/HD36486/HD36486.py | 4 +- .../caom2utils/tests/test_collections.py | 12 +- .../caom2utils/tests/test_custom_axis_util.py | 212 ++++++----- .../caom2utils/tests/test_fits2caom2.py | 67 ++-- .../caom2utils/tests/test_obs_blueprint.py | 4 +- .../caom2utils/tests/test_polygonvalidator.py | 340 +++++++++--------- .../caom2utils/tests/test_wcsvalidator.py | 16 +- caom2utils/caom2utils/wcs_util.py | 30 +- 15 files changed, 394 insertions(+), 449 deletions(-) diff --git a/caom2/caom2/artifact.py b/caom2/caom2/artifact.py index 958cf2cc..d3edac54 100644 --- a/caom2/caom2/artifact.py +++ b/caom2/caom2/artifact.py @@ -258,7 +258,7 @@ def content_length(self, value): def content_checksum(self): """the checksum value for the artifact data - type: ChecksumURI + type: uri """ return self._content_checksum diff --git a/caom2/caom2/common.py b/caom2/caom2/common.py index 3e8d1673..a8f9cd42 100644 --- a/caom2/caom2/common.py +++ b/caom2/caom2/common.py @@ -246,7 +246,7 @@ def max_last_modified(self, value): def meta_checksum(self): """the meta checksum value - type: ChecksumURI + type: URI """ return self._meta_checksum @@ -263,7 +263,7 @@ def meta_checksum(self, value): def acc_meta_checksum(self): """the accumulated meta checksum value - type: ChecksumURI + type: URI """ return self._acc_meta_checksum diff --git a/caom2repo/caom2repo/tests/test_core.py b/caom2repo/caom2repo/tests/test_core.py index f7ba1303..df0b55d8 100644 --- a/caom2repo/caom2repo/tests/test_core.py +++ b/caom2repo/caom2repo/tests/test_core.py @@ -78,7 +78,7 @@ from cadcutils import util, exceptions from cadcutils.net import auth from caom2.obs_reader_writer import ObservationWriter -from caom2 import obs_reader_writer, ChecksumURI +from caom2 import obs_reader_writer from caom2.observation import SimpleObservation from unittest.mock import Mock, patch, MagicMock, ANY, call # TODO to be changed to io.BytesIO when caom2 is prepared for python3 @@ -577,7 +577,7 @@ def test_visit_retry_on_412(self): level = logging.DEBUG visitor = CAOM2RepoClient(auth.Subject(), level) observation = SimpleObservation('cfht', 'a') - observation.acc_meta_checksum = ChecksumURI('md5:abc') + observation.acc_meta_checksum = 'md5:abc' visitor.get_observation = MagicMock(side_effect=[observation, observation]) diff --git a/caom2utils/caom2utils/blueprints.py b/caom2utils/caom2utils/blueprints.py index 1044dc89..6839de01 100644 --- a/caom2utils/caom2utils/blueprints.py +++ b/caom2utils/caom2utils/blueprints.py @@ -378,7 +378,6 @@ def __init__( 'Plane.dataProductType': ([], DataProductType.IMAGE), 'Plane.metaRelease': (['RELEASE', 'REL_DATE'], None), 'Plane.dataRelease': (['RELEASE', 'REL_DATE'], None), - 'Plane.productID': (['RUNID'], None), 'Plane.provenance.name': (['XPRVNAME'], None), 'Plane.provenance.project': (['ADC_ARCH'], None), 'Plane.provenance.producer': (['ORIGIN'], None), diff --git a/caom2utils/caom2utils/caom2blueprint.py b/caom2utils/caom2utils/caom2blueprint.py index 0f1ed206..bf915685 100755 --- a/caom2utils/caom2utils/caom2blueprint.py +++ b/caom2utils/caom2utils/caom2blueprint.py @@ -2,7 +2,7 @@ # ****************** CANADIAN ASTRONOMY DATA CENTRE ******************* # ************* CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** # -# (c) 2016. (c) 2016. +# (c) 2025. (c) 2025. # Government of Canada Gouvernement du Canada # National Research Council Conseil national de recherches # Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 @@ -98,13 +98,12 @@ from caom2 import ( Artifact, Algorithm, - ChecksumURI, CompositeObservation, DerivedObservation, ObservationReader, ObservationWriter, Plane, - ProductType, + DataLinkSemantics, ReleaseType, SimpleObservation, ) @@ -263,9 +262,9 @@ def update_artifact_meta(artifact, file_info): ) if file_info.md5sum is not None: if file_info.md5sum.startswith('md5:'): - checksum = ChecksumURI(file_info.md5sum) + checksum = file_info.md5sum else: - checksum = ChecksumURI(f'md5:{file_info.md5sum}') + checksum = f'md5:{file_info.md5sum}' artifact.content_checksum = checksum artifact.content_length = _to_int(file_info.size) artifact.content_type = _to_str(file_info.file_type) @@ -354,13 +353,14 @@ def _augment( if dumpconfig: print(f'Blueprint for {uri}: {blueprint}') - if product_id not in obs.planes.keys(): - obs.planes.add(Plane(product_id=str(product_id))) + plane_uri = f'{obs.uri}/{product_id}' + if plane_uri not in obs.planes.keys(): + obs.planes[plane_uri] = Plane(uri=plane_uri) - plane = obs.planes[product_id] + plane = obs.planes[plane_uri] if uri not in plane.artifacts.keys(): - plane.artifacts.add(Artifact(uri=str(uri), product_type=ProductType.SCIENCE, release_type=ReleaseType.DATA)) + plane.artifacts.add(Artifact(uri=str(uri), product_type=DataLinkSemantics.SCIENCE, release_type=ReleaseType.DATA)) meta_uri = uri visit_local = None @@ -427,7 +427,10 @@ def _augment( result = None else: _get_and_update_artifact_meta(meta_uri, plane.artifacts[uri], subject, connected, client) - parser.augment_observation(observation=obs, artifact_uri=uri, product_id=plane.product_id) + product_id = None + if plane.uri: + product_id = plane.uri.split('/')[-1] + parser.augment_observation(observation=obs, artifact_uri=uri, product_id=product_id) result = _visit(plugin, parser, obs, visit_local, product_id, uri, subject, **kwargs) @@ -556,23 +559,26 @@ def _gen_obs(obs_blueprints, in_obs_xml, collection=None, obs_id=None): else: # determine the type of observation to create by looking for the the DerivedObservation.members in the # blueprints. If present in any of it assume derived + if not collection or not obs_id: + raise ValueError('collection and obs_id must be provided if no input observation is provided') + obs_uri = f'caom:{collection}/{obs_id}' for bp in obs_blueprints.values(): if bp._get('DerivedObservation.members') is not None: logging.debug('Build a DerivedObservation') obs = DerivedObservation( - collection=collection, observation_id=obs_id, algorithm=Algorithm('composite') + collection=collection, uri=obs_uri, algorithm=Algorithm('composite') ) break elif bp._get('CompositeObservation.members') is not None: logging.debug('Build a CompositeObservation with obs_id {}'.format(obs_id)) obs = CompositeObservation( - collection=collection, observation_id=obs_id, algorithm=Algorithm('composite') + collection=collection, uri=obs_uri, algorithm=Algorithm('composite') ) break if not obs: # build a simple observation - logging.debug(f'Build a SimpleObservation with obs_id {obs_id}') - obs = SimpleObservation(collection=collection, observation_id=obs_id, algorithm=Algorithm('exposure')) + logging.debug(f'Build a SimpleObservation with uri {obs_uri}') + obs = SimpleObservation(collection=collection, uri=obs_uri, algorithm=Algorithm('exposure')) return obs @@ -773,7 +779,7 @@ def _visit(plugin_name, parser, obs, visit_local, product_id=None, uri=None, sub if plugin_name is not None and len(plugin_name) > 0: # TODO make a check that's necessary under both calling conditions here logging.debug( - 'Begin plugin execution {!r} update method on ' 'observation {!r}'.format(plugin_name, obs.observation_id) + 'Begin plugin execution {!r} update method on ' 'observation {!r}'.format(plugin_name, obs.uri) ) plgin = _load_plugin(plugin_name) if isinstance(parser, FitsParser): diff --git a/caom2utils/caom2utils/parsers.py b/caom2utils/caom2utils/parsers.py index a5d839b6..8f4d0a65 100644 --- a/caom2utils/caom2utils/parsers.py +++ b/caom2utils/caom2utils/parsers.py @@ -159,13 +159,13 @@ def augment_observation(self, observation, artifact_uri, product_id=None): if product_id is None: raise ValueError('product ID required') - for ii in observation.planes: - if observation.planes[ii].product_id == product_id: - plane = observation.planes[product_id] - break - if plane is None: - plane = caom2.Plane(product_id=product_id) - observation.planes[product_id] = plane + plane_uri = f'{observation.uri}/{product_id}' + + if plane_uri in observation.planes: + plane = observation.planes[plane_uri] + else: + plane = caom2.Plane(uri=plane_uri) + observation.planes[plane_uri] = plane self.augment_plane(plane, artifact_uri) self.logger.debug(f'End CAOM2 observation augmentation for {artifact_uri}.') @@ -305,7 +305,7 @@ def _to_calibration_level(self, value): return self._to_enum_type(value, caom2.CalibrationLevel) def _to_product_type(self, value): - return self._to_enum_type(value, caom2.ProductType) + return self._to_enum_type(value, caom2.DataLinkSemantics) def _to_release_type(self, value): return self._to_enum_type(value, caom2.ReleaseType) @@ -551,7 +551,7 @@ def _get_provenance(self, current): prov.inputs.add(i) else: for i in inputs.split(): - prov.inputs.add(caom2.PlaneURI(str(i))) + prov.inputs.add(str(i)) else: if current is not None and len(current.inputs) > 0: # preserve the original value @@ -658,7 +658,7 @@ def augment_observation(self, observation, artifact_uri, product_id=None): observation.members.add(m) else: for m in members.split(): - observation.members.add(caom2.ObservationURI(m)) + observation.members.add(m) observation.algorithm = self._get_algorithm(observation) observation.sequence_number = _to_int(self._get_from_list('Observation.sequenceNumber', index=0)) @@ -910,7 +910,7 @@ def _get_proposal(self, current): 'Observation.proposal.id', index=0, current=None if current is None else current.id ) pi = self._get_from_list( - 'Observation.proposal.pi', index=0, current=None if current is None else current.pi_name + 'Observation.proposal.pi', index=0, current=None if current is None else current.pi ) project = self._get_from_list( 'Observation.proposal.project', index=0, current=None if current is None else current.project @@ -950,7 +950,7 @@ def _get_target(self, current): 'Observation.target.name', index=0, current=None if current is None else current.name ) target_type = self._get_from_list( - 'Observation.target.type', index=0, current=None if current is None else current.target_type + 'Observation.target.type', index=0, current=None if current is None else current.type ) standard = self._cast_as_bool( self._get_from_list( @@ -2094,7 +2094,7 @@ def _set_by_type(header, keyword, value): def _to_checksum_uri(value): if value is None: return None - elif isinstance(value, caom2.ChecksumURI): + elif isinstance(value, str): return value else: - return caom2.ChecksumURI(value) + raise TypeError(f'Expected a string, got {type(value)}') diff --git a/caom2utils/caom2utils/polygonvalidator.py b/caom2utils/caom2utils/polygonvalidator.py index 115e16d2..c9e2d1af 100644 --- a/caom2utils/caom2utils/polygonvalidator.py +++ b/caom2utils/caom2utils/polygonvalidator.py @@ -2,7 +2,7 @@ # ****************** CANADIAN ASTRONOMY DATA CENTRE ******************* # ************* CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** # -# (c) 2018. (c) 2018. +# (c) 2025. (c) 2025. # Government of Canada Gouvernement du Canada # National Research Council Conseil national de recherches # Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 @@ -68,10 +68,10 @@ import numpy as np from spherical_geometry import polygon, vector -from caom2 import Point, Polygon, MultiPolygon, SegmentType, Circle +from caom2 import Polygon, MultiShape, Circle -__all__ = ['validate_polygon', 'validate_multipolygon'] +__all__ = ['validate_polygon', 'validate_multishape'] def validate_polygon(poly): @@ -156,82 +156,19 @@ def _validate_self_intersection_and_direction(ras, decs): _validate_is_clockwise(ras, lon) -def validate_multipolygon(mp): +def validate_multishape(mp): """ Performs a basic validation of a multipolygon. An AssertionError is thrown if the multipolygon is invalid ie (invalid indexes, invalid polygons etc.) """ - + # TODO - not sure this is useful anymore if not mp: return - if not isinstance(mp, MultiPolygon): - raise ValueError(f'MultiPoligon expected in validation received {type(mp)}') - - _validate_size_and_end_vertices(mp) + if not isinstance(mp, MultiShape): + raise ValueError(f'MultiShape expected in validation received {type(mp)}') # perform a more detailed validation of this multipolygon object - mp_validator = MultiPolygonValidator() - for i in range(len(mp.vertices)): - mp_validator.validate(mp.vertices[i]) - - -def _validate_size_and_end_vertices(mp): - if len(mp.vertices) < 4: - # triangle - raise AssertionError('invalid polygon: {} vertices (min 4)'.format(len(mp.vertices))) - - if mp.vertices[0].type != SegmentType.MOVE: - raise AssertionError('invalid polygon: first vertex is not a MOVE vertex') - - if mp.vertices[-1].type != SegmentType.CLOSE: - raise AssertionError('invalid polygon: last vertex is not a CLOSE vertex') - - -class MultiPolygonValidator: - """ - A class to validate the sequencing of vertices in a polygon, as well as constructing and validating the polygon. - - An AssertionError is thrown if an incorrect polygon is detected. - """ - - def __init__(self): - self._lines = 0 - self._open_loop = False - self._polygon = Polygon() - - def validate(self, vertex): - if vertex.type == SegmentType.MOVE: - self._validate_move(vertex) - elif vertex.type == SegmentType.CLOSE: - self._validate_close(vertex) - else: - self._validate_line(vertex) - - def _validate_move(self, vertex): - if self._open_loop: - raise AssertionError('invalid polygon: MOVE vertex when loop open') - self._lines = 0 - self._open_loop = True - self._polygon.points.append(Point(vertex.cval1, vertex.cval2)) - - def _validate_close(self, vertex): - # close the polygon - if not self._open_loop: - raise AssertionError('invalid polygon: CLOSE vertex when loop close') - if self._lines < 2: - raise AssertionError('invalid polygon: minimum 2 lines required') - self._open_loop = False - # SphericalPolygon requires point[0] == point[-1] - point = self._polygon.points[0] - self._polygon.points.append(Point(point.cval1, point.cval2)) - # validate the polygons in the multipolygon - validate_polygon(self._polygon) - # instantiate a new Polygon for the next iteration - self._polygon = Polygon() - - def _validate_line(self, vertex): - if not self._open_loop: - raise AssertionError('invalid polygon: LINE vertex when loop close') - self._lines += 1 - self._polygon.points.append(Point(vertex.cval1, vertex.cval2)) + for shape in mp.shapes: + if shape not in [Circle, Polygon]: + raise ValueError(f'Invalid shape in MultiShape: {shape}') diff --git a/caom2utils/caom2utils/tests/data/brite/HD36486/HD36486.py b/caom2utils/caom2utils/tests/data/brite/HD36486/HD36486.py index 243fcc91..3c863b05 100644 --- a/caom2utils/caom2utils/tests/data/brite/HD36486/HD36486.py +++ b/caom2utils/caom2utils/tests/data/brite/HD36486/HD36486.py @@ -1,8 +1,8 @@ -from caom2 import ProductType +from caom2 import DataLinkSemantics def _get_artifact_product_type(uri): - return ProductType.SCIENCE + return DataLinkSemantics.SCIENCE def _get_time_axis_range_end_val(uri): diff --git a/caom2utils/caom2utils/tests/test_collections.py b/caom2utils/caom2utils/tests/test_collections.py index df2ea0fe..78610c1e 100644 --- a/caom2utils/caom2utils/tests/test_collections.py +++ b/caom2utils/caom2utils/tests/test_collections.py @@ -101,7 +101,8 @@ def test_differences(directory): assert len(expected_fname) == 1 expected = _read_observation(expected_fname[0]) # expected observation assert len(expected.planes) == 1 - prod_id = [p.product_id for p in expected.planes.values()][0] + plane_uri = [p.uri for p in expected.planes.values()][0] + prod_id = plane_uri.split('/')[-1] product_id = f'--productID {prod_id}' collection_id = expected.collection data_files = _get_files(['header', 'png', 'gif', 'cat', 'fits', 'h5', 'orig'], directory) @@ -203,10 +204,11 @@ def _header(fqn): header_mock.side_effect = _header temp = tempfile.NamedTemporaryFile() + observation_id = expected.uri.split('/')[-1] sys.argv = ( '{} -o {} --no_validate --observation {} {} {} {} ' '--resource-id ivo://cadc.nrc.ca/test'.format( - application, temp.name, expected.collection, expected.observation_id, inputs, cardinality + application, temp.name, expected.collection, observation_id, inputs, cardinality ) ).split() print(sys.argv) @@ -303,7 +305,7 @@ def _get_uris(collection, fnames, obs): id=a.uri, file_type=a.content_type, size=a.content_length, - md5sum=a.content_checksum.checksum, + md5sum=a.content_checksum, ) file_url = urlparse(a.uri) file_id = file_url.path.split('/')[-1] @@ -336,11 +338,11 @@ def _compare_observations(expected, actual, output_dir): result = get_differences(expected, actual, 'Observation') if result: tmp = '\n'.join([r for r in result]) - msg = f'Differences found observation {expected.observation_id} in ' f'{output_dir}\n{tmp}' + msg = f'Differences found observation {expected.uri} in ' f'{output_dir}\n{tmp}' _write_observation(actual) raise AssertionError(msg) else: - logging.info('Observation {} in {} match'.format(expected.observation_id, output_dir)) + logging.info('Observation {} in {} match'.format(expected.uri, output_dir)) def _read_observation(fname): diff --git a/caom2utils/caom2utils/tests/test_custom_axis_util.py b/caom2utils/caom2utils/tests/test_custom_axis_util.py index 59ba69de..7cf2d0c2 100644 --- a/caom2utils/caom2utils/tests/test_custom_axis_util.py +++ b/caom2utils/caom2utils/tests/test_custom_axis_util.py @@ -131,7 +131,6 @@ def test_function1d_to_interval_happy_path(self): expected_interval = Interval(-502.5, -2.5) self.assertEqual(expected_interval.lower, actual_interval.lower) self.assertEqual(expected_interval.upper, actual_interval.upper) - self.assertEqual(None, actual_interval.samples) # function_1d.delta == 0.0 && function_1d.naxis > 1 naxis = int(100) delta = 0.0 @@ -151,7 +150,6 @@ def test_range1d_to_interval(self): expected_interval = Interval(1.1, 11.1) self.assertEqual(expected_interval.lower, actual_interval.lower) self.assertEqual(expected_interval.upper, actual_interval.upper) - self.assertEqual(None, actual_interval.samples) # function_1d.delta == 0.0 && function_1d.naxis > 1 start = RefCoord(float(0.9), float(1.1)) end = RefCoord(float(10.9), float(1.1)) @@ -163,14 +161,14 @@ def test_range1d_to_interval(self): def test_compute_dimension_from_range_bounds(self): # user_chunk = False, matches is None test_chunk = Chunk() - test_chunk.product_type = chunk.ProductType.SCIENCE + test_chunk.product_type = chunk.DataLinkSemantics.SCIENCE test_chunk.custom = CustomTestUtil.good_wcs() test_chunks = TypedList(Chunk, test_chunk) part_name = "test_part" - part_product_type = chunk.ProductType.SCIENCE + part_product_type = chunk.DataLinkSemantics.SCIENCE part = Part(part_name, part_product_type, test_chunks) uri = 'mast:HST/product/test_file.jpg' - artifact_product_type = chunk.ProductType.SCIENCE + artifact_product_type = chunk.DataLinkSemantics.SCIENCE release_type = ReleaseType.DATA artifact = Artifact(uri, artifact_product_type, release_type) artifact.parts = TypedOrderedDict((Part), (part_name, part)) @@ -184,19 +182,19 @@ def test_compute_dimension_from_range_bounds(self): self.assertEqual(expected_num_pixels, actual_num_pixels) # user_chunk = False, ctype not match test_chunk = Chunk() - test_chunk.product_type = chunk.ProductType.SCIENCE + test_chunk.product_type = chunk.DataLinkSemantics.SCIENCE test_chunk.custom = CustomTestUtil.good_wcs() test_chunks = TypedList(Chunk, test_chunk) part_name = "test_part" - part_product_type = chunk.ProductType.SCIENCE + part_product_type = chunk.DataLinkSemantics.SCIENCE part = Part(part_name, part_product_type, test_chunks) uri = 'mast:HST/product/test_file.jpg' - artifact_product_type = chunk.ProductType.SCIENCE + artifact_product_type = chunk.DataLinkSemantics.SCIENCE release_type = ReleaseType.DATA artifact = Artifact(uri, artifact_product_type, release_type) artifact.parts = TypedOrderedDict((Part), (part_name, part)) artifacts = TypedList(Artifact, artifact) - product_type = chunk.ProductType.CALIBRATION + product_type = chunk.DataLinkSemantics.CALIBRATION expected_ctype = "RM" actual_num_pixels = wcs_util.CustomAxisUtil.compute_dimension_from_range_bounds( artifacts, product_type, expected_ctype @@ -205,19 +203,19 @@ def test_compute_dimension_from_range_bounds(self): self.assertEqual(expected_num_pixels, actual_num_pixels) # user_chunk = False, ptype not match test_chunk = Chunk() - test_chunk.product_type = chunk.ProductType.CALIBRATION + test_chunk.product_type = chunk.DataLinkSemantics.CALIBRATION test_chunk.custom = CustomTestUtil.good_wcs() test_chunks = TypedList(Chunk, test_chunk) part_name = "test_part" - part_product_type = chunk.ProductType.SCIENCE + part_product_type = chunk.DataLinkSemantics.SCIENCE part = Part(part_name, part_product_type, test_chunks) uri = 'mast:HST/product/test_file.jpg' - artifact_product_type = chunk.ProductType.SCIENCE + artifact_product_type = chunk.DataLinkSemantics.SCIENCE release_type = ReleaseType.DATA artifact = Artifact(uri, artifact_product_type, release_type) artifact.parts = TypedOrderedDict((Part), (part_name, part)) artifacts = TypedList(Artifact, artifact) - product_type = chunk.ProductType.CALIBRATION + product_type = chunk.DataLinkSemantics.CALIBRATION expected_ctype = "RM" actual_num_pixels = wcs_util.CustomAxisUtil.compute_dimension_from_range_bounds( artifacts, product_type, expected_ctype @@ -226,19 +224,19 @@ def test_compute_dimension_from_range_bounds(self): self.assertEqual(expected_num_pixels, actual_num_pixels) # user_chunk = False, atype not match test_chunk = Chunk() - test_chunk.product_type = chunk.ProductType.CALIBRATION + test_chunk.product_type = chunk.DataLinkSemantics.CALIBRATION test_chunk.custom = CustomTestUtil.good_wcs() test_chunks = TypedList(Chunk, test_chunk) part_name = "test_part" - part_product_type = chunk.ProductType.CALIBRATION + part_product_type = chunk.DataLinkSemantics.CALIBRATION part = Part(part_name, part_product_type, test_chunks) uri = 'mast:HST/product/test_file.jpg' - artifact_product_type = chunk.ProductType.SCIENCE + artifact_product_type = chunk.DataLinkSemantics.SCIENCE release_type = ReleaseType.DATA artifact = Artifact(uri, artifact_product_type, release_type) artifact.parts = TypedOrderedDict((Part), (part_name, part)) artifacts = TypedList(Artifact, artifact) - product_type = chunk.ProductType.CALIBRATION + product_type = chunk.DataLinkSemantics.CALIBRATION expected_ctype = "RM" actual_num_pixels = wcs_util.CustomAxisUtil.compute_dimension_from_range_bounds( artifacts, product_type, expected_ctype @@ -247,38 +245,38 @@ def test_compute_dimension_from_range_bounds(self): self.assertEqual(expected_num_pixels, actual_num_pixels) # user_chunk = True, current_type != expected_ctype test_chunk = Chunk() - test_chunk.product_type = chunk.ProductType.CALIBRATION + test_chunk.product_type = chunk.DataLinkSemantics.CALIBRATION test_chunk.custom = CustomTestUtil.good_wcs_with_function() test_chunks = TypedList(Chunk, test_chunk) part_name = "test_part" - part_product_type = chunk.ProductType.CALIBRATION + part_product_type = chunk.DataLinkSemantics.CALIBRATION part = Part(part_name, part_product_type, test_chunks) uri = 'mast:HST/product/test_file.jpg' - artifact_product_type = chunk.ProductType.CALIBRATION + artifact_product_type = chunk.DataLinkSemantics.CALIBRATION release_type = ReleaseType.DATA artifact = Artifact(uri, artifact_product_type, release_type) artifact.parts = TypedOrderedDict((Part), (part_name, part)) artifacts = TypedList(Artifact, artifact) - product_type = chunk.ProductType.CALIBRATION + product_type = chunk.DataLinkSemantics.CALIBRATION expected_ctype = "FARADAY" with pytest.raises(ValueError) as ex: wcs_util.CustomAxisUtil.compute_dimension_from_range_bounds(artifacts, product_type, expected_ctype) assert 'CTYPE must be the same across all Artifacts' in str(ex.value) # user_chunk = True, get_num_pixels: range is not None test_chunk = Chunk() - test_chunk.product_type = chunk.ProductType.CALIBRATION + test_chunk.product_type = chunk.DataLinkSemantics.CALIBRATION test_chunk.custom = CustomTestUtil.good_wcs_with_range() test_chunks = TypedList(Chunk, test_chunk) part_name = "test_part" - part_product_type = chunk.ProductType.CALIBRATION + part_product_type = chunk.DataLinkSemantics.CALIBRATION part = Part(part_name, part_product_type, test_chunks) uri = 'mast:HST/product/test_file.jpg' - artifact_product_type = chunk.ProductType.CALIBRATION + artifact_product_type = chunk.DataLinkSemantics.CALIBRATION release_type = ReleaseType.DATA artifact = Artifact(uri, artifact_product_type, release_type) artifact.parts = TypedOrderedDict((Part), (part_name, part)) artifacts = TypedList(Artifact, artifact) - product_type = chunk.ProductType.CALIBRATION + product_type = chunk.DataLinkSemantics.CALIBRATION expected_ctype = "RM" actual_num_pixels = wcs_util.CustomAxisUtil.compute_dimension_from_range_bounds( artifacts, product_type, expected_ctype @@ -288,19 +286,19 @@ def test_compute_dimension_from_range_bounds(self): # user_chunk = True, get_num_pixels: bounds with 3 samples that # traverses _merge_into_list completely test_chunk = Chunk() - test_chunk.product_type = chunk.ProductType.CALIBRATION + test_chunk.product_type = chunk.DataLinkSemantics.CALIBRATION test_chunk.custom = CustomTestUtil.good_wcs_with_bounds_3_samples() test_chunks = TypedList(Chunk, test_chunk) part_name = "test_part" - part_product_type = chunk.ProductType.CALIBRATION + part_product_type = chunk.DataLinkSemantics.CALIBRATION part = Part(part_name, part_product_type, test_chunks) uri = 'mast:HST/product/test_file.jpg' - artifact_product_type = chunk.ProductType.CALIBRATION + artifact_product_type = chunk.DataLinkSemantics.CALIBRATION release_type = ReleaseType.DATA artifact = Artifact(uri, artifact_product_type, release_type) artifact.parts = TypedOrderedDict((Part), (part_name, part)) artifacts = TypedList(Artifact, artifact) - product_type = chunk.ProductType.CALIBRATION + product_type = chunk.DataLinkSemantics.CALIBRATION expected_ctype = "RM" actual_num_pixels = wcs_util.CustomAxisUtil.compute_dimension_from_range_bounds( artifacts, product_type, expected_ctype @@ -310,19 +308,19 @@ def test_compute_dimension_from_range_bounds(self): # user_chunk = True, range = None, bounds = None, use_func and # function = None test_chunk = Chunk() - test_chunk.product_type = chunk.ProductType.CALIBRATION + test_chunk.product_type = chunk.DataLinkSemantics.CALIBRATION test_chunk.custom = CustomTestUtil.good_wcs() test_chunks = TypedList(Chunk, test_chunk) part_name = "test_part" - part_product_type = chunk.ProductType.CALIBRATION + part_product_type = chunk.DataLinkSemantics.CALIBRATION part = Part(part_name, part_product_type, test_chunks) uri = 'mast:HST/product/test_file.jpg' - artifact_product_type = chunk.ProductType.CALIBRATION + artifact_product_type = chunk.DataLinkSemantics.CALIBRATION release_type = ReleaseType.DATA artifact = Artifact(uri, artifact_product_type, release_type) artifact.parts = TypedOrderedDict((Part), (part_name, part)) artifacts = TypedList(Artifact, artifact) - product_type = chunk.ProductType.CALIBRATION + product_type = chunk.DataLinkSemantics.CALIBRATION expected_ctype = "RM" actual_num_pixels = wcs_util.CustomAxisUtil.compute_dimension_from_range_bounds( artifacts, product_type, expected_ctype @@ -344,19 +342,19 @@ def test_compute_dimension_from_wcs(self): # bounds is not None, user_chunk = True, current_type != expected_ctype bounds = Interval(1.1, 11.1) test_chunk = Chunk() - test_chunk.product_type = chunk.ProductType.CALIBRATION + test_chunk.product_type = chunk.DataLinkSemantics.CALIBRATION test_chunk.custom = CustomTestUtil.good_wcs_with_function() test_chunks = TypedList(Chunk, test_chunk) part_name = "test_part" - part_product_type = chunk.ProductType.CALIBRATION + part_product_type = chunk.DataLinkSemantics.CALIBRATION part = Part(part_name, part_product_type, test_chunks) uri = 'mast:HST/product/test_file.jpg' - artifact_product_type = chunk.ProductType.CALIBRATION + artifact_product_type = chunk.DataLinkSemantics.CALIBRATION release_type = ReleaseType.DATA artifact = Artifact(uri, artifact_product_type, release_type) artifact.parts = TypedOrderedDict((Part), (part_name, part)) artifacts = TypedList(Artifact, artifact) - product_type = chunk.ProductType.CALIBRATION + product_type = chunk.DataLinkSemantics.CALIBRATION expected_ctype = "FARADAY" with pytest.raises(ValueError) as ex: wcs_util.CustomAxisUtil.compute_dimension_from_wcs(bounds, artifacts, product_type, expected_ctype) @@ -365,19 +363,19 @@ def test_compute_dimension_from_wcs(self): # ss >= scale, num = 1 bounds = Interval(1.1, 11.1) test_chunk = Chunk() - test_chunk.product_type = chunk.ProductType.CALIBRATION + test_chunk.product_type = chunk.DataLinkSemantics.CALIBRATION test_chunk.custom = CustomTestUtil.good_wcs_with_negative_delta() test_chunks = TypedList(Chunk, test_chunk) part_name = "test_part" - part_product_type = chunk.ProductType.CALIBRATION + part_product_type = chunk.DataLinkSemantics.CALIBRATION part = Part(part_name, part_product_type, test_chunks) uri = 'mast:HST/product/test_file.jpg' - artifact_product_type = chunk.ProductType.CALIBRATION + artifact_product_type = chunk.DataLinkSemantics.CALIBRATION release_type = ReleaseType.DATA artifact = Artifact(uri, artifact_product_type, release_type) artifact.parts = TypedOrderedDict((Part), (part_name, part)) artifacts = TypedList(Artifact, artifact) - product_type = chunk.ProductType.CALIBRATION + product_type = chunk.DataLinkSemantics.CALIBRATION expected_ctype = "RM" actual_dimension = wcs_util.CustomAxisUtil.compute_dimension_from_wcs( bounds, artifacts, product_type, expected_ctype @@ -386,14 +384,14 @@ def test_compute_dimension_from_wcs(self): self.assertEqual(expected_dimension, actual_dimension) # bounds is not None, user_chunk = False, sw = None test_chunk = Chunk() - test_chunk.product_type = chunk.ProductType.SCIENCE + test_chunk.product_type = chunk.DataLinkSemantics.SCIENCE test_chunk.custom = CustomTestUtil.good_wcs() test_chunks = TypedList(Chunk, test_chunk) part_name = "test_part" - part_product_type = chunk.ProductType.SCIENCE + part_product_type = chunk.DataLinkSemantics.SCIENCE part = Part(part_name, part_product_type, test_chunks) uri = 'mast:HST/product/test_file.jpg' - artifact_product_type = chunk.ProductType.SCIENCE + artifact_product_type = chunk.DataLinkSemantics.SCIENCE release_type = ReleaseType.DATA artifact = Artifact(uri, artifact_product_type, release_type) artifact.parts = TypedOrderedDict((Part), (part_name, part)) @@ -409,22 +407,22 @@ def test_compute_dimension_from_wcs(self): # ss >= scale, num = 2 bounds = Interval(1.1, 11.1) test_chunk1 = Chunk() - test_chunk1.product_type = chunk.ProductType.CALIBRATION + test_chunk1.product_type = chunk.DataLinkSemantics.CALIBRATION test_chunk1.custom = CustomTestUtil.good_wcs_with_negative_delta() test_chunk2 = Chunk() - test_chunk2.product_type = chunk.ProductType.CALIBRATION + test_chunk2.product_type = chunk.DataLinkSemantics.CALIBRATION test_chunk2.custom = CustomTestUtil.good_wcs() test_chunks = TypedList(Chunk, test_chunk1, test_chunk2) part_name = "test_part" - part_product_type = chunk.ProductType.CALIBRATION + part_product_type = chunk.DataLinkSemantics.CALIBRATION part = Part(part_name, part_product_type, test_chunks) uri = 'mast:HST/product/test_file.jpg' - artifact_product_type = chunk.ProductType.CALIBRATION + artifact_product_type = chunk.DataLinkSemantics.CALIBRATION release_type = ReleaseType.DATA artifact = Artifact(uri, artifact_product_type, release_type) artifact.parts = TypedOrderedDict((Part), (part_name, part)) artifacts = TypedList(Artifact, artifact) - product_type = chunk.ProductType.CALIBRATION + product_type = chunk.DataLinkSemantics.CALIBRATION expected_ctype = "RM" actual_dimension = wcs_util.CustomAxisUtil.compute_dimension_from_wcs( bounds, artifacts, product_type, expected_ctype @@ -435,14 +433,14 @@ def test_compute_dimension_from_wcs(self): def test_compute_bounds(self): # user_chunk = False test_chunk = Chunk() - test_chunk.product_type = chunk.ProductType.SCIENCE + test_chunk.product_type = chunk.DataLinkSemantics.SCIENCE test_chunk.custom = CustomTestUtil.good_wcs() test_chunks = TypedList(Chunk, test_chunk) part_name = "test_part" - part_product_type = chunk.ProductType.SCIENCE + part_product_type = chunk.DataLinkSemantics.SCIENCE part = Part(part_name, part_product_type, test_chunks) uri = 'mast:HST/product/test_file.jpg' - artifact_product_type = chunk.ProductType.SCIENCE + artifact_product_type = chunk.DataLinkSemantics.SCIENCE release_type = ReleaseType.DATA artifact = Artifact(uri, artifact_product_type, release_type) artifact.parts = TypedOrderedDict((Part), (part_name, part)) @@ -454,38 +452,38 @@ def test_compute_bounds(self): self.assertEqual(expected_bounds, actual_bounds) # user_chunk = True, current_type != expected_ctype test_chunk = Chunk() - test_chunk.product_type = chunk.ProductType.CALIBRATION + test_chunk.product_type = chunk.DataLinkSemantics.CALIBRATION test_chunk.custom = CustomTestUtil.good_wcs_with_function() test_chunks = TypedList(Chunk, test_chunk) part_name = "test_part" - part_product_type = chunk.ProductType.CALIBRATION + part_product_type = chunk.DataLinkSemantics.CALIBRATION part = Part(part_name, part_product_type, test_chunks) uri = 'mast:HST/product/test_file.jpg' - artifact_product_type = chunk.ProductType.CALIBRATION + artifact_product_type = chunk.DataLinkSemantics.CALIBRATION release_type = ReleaseType.DATA artifact = Artifact(uri, artifact_product_type, release_type) artifact.parts = TypedOrderedDict((Part), (part_name, part)) artifacts = TypedList(Artifact, artifact) - product_type = chunk.ProductType.CALIBRATION + product_type = chunk.DataLinkSemantics.CALIBRATION expected_ctype = "FARADAY" with pytest.raises(ValueError) as ex: wcs_util.CustomAxisUtil.compute_bounds(artifacts, product_type, expected_ctype) assert 'CTYPE must be the same across all Artifacts' in str(ex.value) # user_chunk = True, range is not None test_chunk = Chunk() - test_chunk.product_type = chunk.ProductType.CALIBRATION + test_chunk.product_type = chunk.DataLinkSemantics.CALIBRATION test_chunk.custom = CustomTestUtil.good_wcs_with_range() test_chunks = TypedList(Chunk, test_chunk) part_name = "test_part" - part_product_type = chunk.ProductType.CALIBRATION + part_product_type = chunk.DataLinkSemantics.CALIBRATION part = Part(part_name, part_product_type, test_chunks) uri = 'mast:HST/product/test_file.jpg' - artifact_product_type = chunk.ProductType.CALIBRATION + artifact_product_type = chunk.DataLinkSemantics.CALIBRATION release_type = ReleaseType.DATA artifact = Artifact(uri, artifact_product_type, release_type) artifact.parts = TypedOrderedDict((Part), (part_name, part)) artifacts = TypedList(Artifact, artifact) - product_type = chunk.ProductType.CALIBRATION + product_type = chunk.DataLinkSemantics.CALIBRATION expected_ctype = "RM" actual_interval = wcs_util.CustomAxisUtil.compute_bounds(artifacts, product_type, expected_ctype) expected_interval = Interval(1.1, 11.1) @@ -493,19 +491,19 @@ def test_compute_bounds(self): self.assertEqual(expected_interval.upper, actual_interval.upper) # user_chunk = True, get_num_pixels: bounds with 3 samples that traverses _merge_into_list completely test_chunk = Chunk() - test_chunk.product_type = chunk.ProductType.CALIBRATION + test_chunk.product_type = chunk.DataLinkSemantics.CALIBRATION test_chunk.custom = CustomTestUtil.good_wcs_with_bounds_3_samples() test_chunks = TypedList(Chunk, test_chunk) part_name = "test_part" - part_product_type = chunk.ProductType.CALIBRATION + part_product_type = chunk.DataLinkSemantics.CALIBRATION part = Part(part_name, part_product_type, test_chunks) uri = 'mast:HST/product/test_file.jpg' - artifact_product_type = chunk.ProductType.CALIBRATION + artifact_product_type = chunk.DataLinkSemantics.CALIBRATION release_type = ReleaseType.DATA artifact = Artifact(uri, artifact_product_type, release_type) artifact.parts = TypedOrderedDict((Part), (part_name, part)) artifacts = TypedList(Artifact, artifact) - product_type = chunk.ProductType.CALIBRATION + product_type = chunk.DataLinkSemantics.CALIBRATION expected_ctype = "RM" actual_interval = wcs_util.CustomAxisUtil.compute_bounds(artifacts, product_type, expected_ctype) expected_interval = Interval(-1.2, 11.2) @@ -513,19 +511,19 @@ def test_compute_bounds(self): self.assertEqual(expected_interval.upper, actual_interval.upper) # user_chunk = True, function is not None test_chunk = Chunk() - test_chunk.product_type = chunk.ProductType.CALIBRATION + test_chunk.product_type = chunk.DataLinkSemantics.CALIBRATION test_chunk.custom = CustomTestUtil.good_wcs_with_function() test_chunks = TypedList(Chunk, test_chunk) part_name = "test_part" - part_product_type = chunk.ProductType.CALIBRATION + part_product_type = chunk.DataLinkSemantics.CALIBRATION part = Part(part_name, part_product_type, test_chunks) uri = 'mast:HST/product/test_file.jpg' - artifact_product_type = chunk.ProductType.CALIBRATION + artifact_product_type = chunk.DataLinkSemantics.CALIBRATION release_type = ReleaseType.DATA artifact = Artifact(uri, artifact_product_type, release_type) artifact.parts = TypedOrderedDict((Part), (part_name, part)) artifacts = TypedList(Artifact, artifact) - product_type = chunk.ProductType.CALIBRATION + product_type = chunk.DataLinkSemantics.CALIBRATION expected_ctype = "RM" actual_interval = wcs_util.CustomAxisUtil.compute_bounds(artifacts, product_type, expected_ctype) expected_interval = Interval(-49.5, 19950.5) @@ -535,14 +533,14 @@ def test_compute_bounds(self): def test_compute(self): # _choose_product returns Artifact.product (SCIENCE), user_chunk = False test_chunk = Chunk() - test_chunk.product_type = chunk.ProductType.CALIBRATION + test_chunk.product_type = chunk.DataLinkSemantics.CALIBRATION test_chunk.custom = CustomTestUtil.good_wcs() test_chunks = TypedList(Chunk, test_chunk) part_name = "test_part" - part_product_type = chunk.ProductType.CALIBRATION + part_product_type = chunk.DataLinkSemantics.CALIBRATION part = Part(part_name, part_product_type, test_chunks) uri = 'mast:HST/product/test_file.jpg' - artifact_product_type = chunk.ProductType.SCIENCE + artifact_product_type = chunk.DataLinkSemantics.SCIENCE release_type = ReleaseType.DATA artifact = Artifact(uri, artifact_product_type, release_type) artifact.parts = TypedOrderedDict((Part), (part_name, part)) @@ -552,14 +550,14 @@ def test_compute(self): self.assertEqual(expected_axis, actual_axis) # _choose_product returns Artifact.product (CALIBRATION), user_chunk = False test_chunk = Chunk() - test_chunk.product_type = chunk.ProductType.SCIENCE + test_chunk.product_type = chunk.DataLinkSemantics.SCIENCE test_chunk.custom = CustomTestUtil.good_wcs() test_chunks = TypedList(Chunk, test_chunk) part_name = "test_part" - part_product_type = chunk.ProductType.SCIENCE + part_product_type = chunk.DataLinkSemantics.SCIENCE part = Part(part_name, part_product_type, test_chunks) uri = 'mast:HST/product/test_file.jpg' - artifact_product_type = chunk.ProductType.CALIBRATION + artifact_product_type = chunk.DataLinkSemantics.CALIBRATION release_type = ReleaseType.DATA artifact = Artifact(uri, artifact_product_type, release_type) artifact.parts = TypedOrderedDict((Part), (part_name, part)) @@ -569,14 +567,14 @@ def test_compute(self): self.assertEqual(expected_axis, actual_axis) # _choose_product returns Part.product (SCIENCE), user_chunk = False test_chunk = Chunk() - test_chunk.product_type = chunk.ProductType.SCIENCE + test_chunk.product_type = chunk.DataLinkSemantics.SCIENCE test_chunk.custom = CustomTestUtil.good_wcs() test_chunks = TypedList(Chunk, test_chunk) part_name = "test_part" - part_product_type = chunk.ProductType.SCIENCE + part_product_type = chunk.DataLinkSemantics.SCIENCE part = Part(part_name, part_product_type, test_chunks) uri = 'mast:HST/product/test_file.jpg' - artifact_product_type = chunk.ProductType.PREVIEW + artifact_product_type = chunk.DataLinkSemantics.PREVIEW release_type = ReleaseType.DATA artifact = Artifact(uri, artifact_product_type, release_type) artifact.parts = TypedOrderedDict((Part), (part_name, part)) @@ -586,14 +584,14 @@ def test_compute(self): self.assertEqual(expected_axis, actual_axis) # _choose_product returns Part.product (CALIBRATION), user_chunk = False test_chunk = Chunk() - test_chunk.product_type = chunk.ProductType.SCIENCE + test_chunk.product_type = chunk.DataLinkSemantics.SCIENCE test_chunk.custom = CustomTestUtil.good_wcs() test_chunks = TypedList(Chunk, test_chunk) part_name = "test_part" - part_product_type = chunk.ProductType.CALIBRATION + part_product_type = chunk.DataLinkSemantics.CALIBRATION part = Part(part_name, part_product_type, test_chunks) uri = 'mast:HST/product/test_file.jpg' - artifact_product_type = chunk.ProductType.PREVIEW + artifact_product_type = chunk.DataLinkSemantics.PREVIEW release_type = ReleaseType.DATA artifact = Artifact(uri, artifact_product_type, release_type) artifact.parts = TypedOrderedDict((Part), (part_name, part)) @@ -603,14 +601,14 @@ def test_compute(self): self.assertEqual(expected_axis, actual_axis) # _choose_product returns Chunk.product (SCIENCE), user_chunk = False test_chunk = Chunk() - test_chunk.product_type = chunk.ProductType.SCIENCE + test_chunk.product_type = chunk.DataLinkSemantics.SCIENCE test_chunk.custom = CustomTestUtil.good_wcs() test_chunks = TypedList(Chunk, test_chunk) part_name = "test_part" - part_product_type = chunk.ProductType.PREVIEW + part_product_type = chunk.DataLinkSemantics.PREVIEW part = Part(part_name, part_product_type, test_chunks) uri = 'mast:HST/product/test_file.jpg' - artifact_product_type = chunk.ProductType.PREVIEW + artifact_product_type = chunk.DataLinkSemantics.PREVIEW release_type = ReleaseType.DATA artifact = Artifact(uri, artifact_product_type, release_type) artifact.parts = TypedOrderedDict((Part), (part_name, part)) @@ -620,14 +618,14 @@ def test_compute(self): self.assertEqual(expected_axis, actual_axis) # _choose_product returns Chunk.product (CALIBRATION), user_chunk = False test_chunk = Chunk() - test_chunk.product_type = chunk.ProductType.CALIBRATION + test_chunk.product_type = chunk.DataLinkSemantics.CALIBRATION test_chunk.custom = CustomTestUtil.good_wcs() test_chunks = TypedList(Chunk, test_chunk) part_name = "test_part" - part_product_type = chunk.ProductType.PREVIEW + part_product_type = chunk.DataLinkSemantics.PREVIEW part = Part(part_name, part_product_type, test_chunks) uri = 'mast:HST/product/test_file.jpg' - artifact_product_type = chunk.ProductType.PREVIEW + artifact_product_type = chunk.DataLinkSemantics.PREVIEW release_type = ReleaseType.DATA artifact = Artifact(uri, artifact_product_type, release_type) artifact.parts = TypedOrderedDict((Part), (part_name, part)) @@ -637,14 +635,14 @@ def test_compute(self): self.assertEqual(expected_axis, actual_axis) # _choose_product returns None, user_chunk = False test_chunk = Chunk() - test_chunk.product_type = chunk.ProductType.PREVIEW + test_chunk.product_type = chunk.DataLinkSemantics.PREVIEW test_chunk.custom = CustomTestUtil.good_wcs() test_chunks = TypedList(Chunk, test_chunk) part_name = "test_part" - part_product_type = chunk.ProductType.PREVIEW + part_product_type = chunk.DataLinkSemantics.PREVIEW part = Part(part_name, part_product_type, test_chunks) uri = 'mast:HST/product/test_file.jpg' - artifact_product_type = chunk.ProductType.PREVIEW + artifact_product_type = chunk.DataLinkSemantics.PREVIEW release_type = ReleaseType.DATA artifact = Artifact(uri, artifact_product_type, release_type) artifact.parts = TypedOrderedDict((Part), (part_name, part)) @@ -654,14 +652,14 @@ def test_compute(self): self.assertEqual(expected_axis, actual_axis) # _choose_product returns Artifact.product (SCIENCE), user_chunk = True, Chunk.custom is None test_chunk = Chunk() - test_chunk.product_type = chunk.ProductType.SCIENCE + test_chunk.product_type = chunk.DataLinkSemantics.SCIENCE test_chunk.custom = None test_chunks = TypedList(Chunk, test_chunk) part_name = "test_part" - part_product_type = chunk.ProductType.SCIENCE + part_product_type = chunk.DataLinkSemantics.SCIENCE part = Part(part_name, part_product_type, test_chunks) uri = 'mast:HST/product/test_file.jpg' - artifact_product_type = chunk.ProductType.SCIENCE + artifact_product_type = chunk.DataLinkSemantics.SCIENCE release_type = ReleaseType.DATA artifact = Artifact(uri, artifact_product_type, release_type) artifact.parts = TypedOrderedDict((Part), (part_name, part)) @@ -672,14 +670,14 @@ def test_compute(self): # _choose_product returns Artifact.product (SCIENCE), user_chunk = True, Chunk.custom is not None # bad Chunk.custom.axis.axis.ctype test_chunk = Chunk() - test_chunk.product_type = chunk.ProductType.SCIENCE + test_chunk.product_type = chunk.DataLinkSemantics.SCIENCE test_chunk.custom = CustomTestUtil.bad_ctype_wcs() test_chunks = TypedList(Chunk, test_chunk) part_name = "test_part" - part_product_type = chunk.ProductType.SCIENCE + part_product_type = chunk.DataLinkSemantics.SCIENCE part = Part(part_name, part_product_type, test_chunks) uri = 'mast:HST/product/test_file.jpg' - artifact_product_type = chunk.ProductType.SCIENCE + artifact_product_type = chunk.DataLinkSemantics.SCIENCE release_type = ReleaseType.DATA artifact = Artifact(uri, artifact_product_type, release_type) artifact.parts = TypedOrderedDict((Part), (part_name, part)) @@ -690,14 +688,14 @@ def test_compute(self): # _choose_product returns Artifact.product (SCIENCE), user_chunk = True, Chunk.custom is not None # first_ctype == Chunk.custom.axis.axis.ctype test_chunk = Chunk() - test_chunk.product_type = chunk.ProductType.SCIENCE + test_chunk.product_type = chunk.DataLinkSemantics.SCIENCE test_chunk.custom = CustomTestUtil.good_wcs_with_function() test_chunks = TypedList(Chunk, test_chunk) part_name = "test_part" - part_product_type = chunk.ProductType.SCIENCE + part_product_type = chunk.DataLinkSemantics.SCIENCE part = Part(part_name, part_product_type, test_chunks) uri = 'mast:HST/product/test_file.jpg' - artifact_product_type = chunk.ProductType.SCIENCE + artifact_product_type = chunk.DataLinkSemantics.SCIENCE release_type = ReleaseType.DATA artifact = Artifact(uri, artifact_product_type, release_type) artifact.parts = TypedOrderedDict((Part), (part_name, part)) @@ -705,31 +703,29 @@ def test_compute(self): expected_ctype = "RM" expected_sample = Interval(-49.5, 19950.5) expected_samples = [expected_sample] - expected_bounds = Interval(-49.5, 19950.5, expected_samples) + expected_bounds = Interval(-49.5, 19950.5) expected_dimension = 200 - expected_axis = plane.CustomAxis(expected_ctype, expected_bounds, expected_dimension) + expected_axis = plane.CustomAxis(expected_ctype, expected_bounds, expected_samples, expected_dimension) actual_axis = wcs_util.CustomAxisUtil.compute(artifacts) self.assertEqual(expected_axis.ctype, actual_axis.ctype) self.assertEqual(expected_axis.bounds.lower, actual_axis.bounds.lower) self.assertEqual(expected_axis.bounds.upper, actual_axis.bounds.upper) - self.assertEqual(expected_axis.bounds.samples[0].lower, actual_axis.bounds.samples[0].lower) - self.assertEqual(expected_axis.bounds.samples[0].upper, actual_axis.bounds.samples[0].upper) self.assertEqual(expected_axis.dimension, actual_axis.dimension) # _choose_product returns Artifact.product (SCIENCE), user_chunk = True, Chunk.custom is not None # first_ctype == Chunk.custom.axis.axis.ctype test_chunk_1 = Chunk() - test_chunk_1.product_type = chunk.ProductType.SCIENCE + test_chunk_1.product_type = chunk.DataLinkSemantics.SCIENCE test_chunk_1.custom = CustomTestUtil.good_wcs_with_function() test_chunk_2 = Chunk() - test_chunk_2.product_type = chunk.ProductType.SCIENCE + test_chunk_2.product_type = chunk.DataLinkSemantics.SCIENCE test_chunk_2.custom = CustomTestUtil.good_wcs_with_function() test_chunk_2.custom.axis.axis.ctype = "FARADAY" test_chunks = TypedList(Chunk, test_chunk_1, test_chunk_2) part_name = "test_part" - part_product_type = chunk.ProductType.SCIENCE + part_product_type = chunk.DataLinkSemantics.SCIENCE part = Part(part_name, part_product_type, test_chunks) uri = 'mast:HST/product/test_file.jpg' - artifact_product_type = chunk.ProductType.SCIENCE + artifact_product_type = chunk.DataLinkSemantics.SCIENCE release_type = ReleaseType.DATA artifact = Artifact(uri, artifact_product_type, release_type) artifact.parts = TypedOrderedDict((Part), (part_name, part)) @@ -737,9 +733,9 @@ def test_compute(self): expected_ctype = "RM" expected_sample = Interval(-49.5, 19950.5) expected_samples = [expected_sample] - expected_bounds = Interval(-49.5, 19950.5, expected_samples) + expected_bounds = Interval(-49.5, 19950.5) expected_dimension = 200 - expected_axis = plane.CustomAxis(expected_ctype, expected_bounds, expected_dimension) + expected_axis = plane.CustomAxis(expected_ctype, expected_bounds, expected_samples, expected_dimension) with pytest.raises(ValueError) as ex: actual_axis = wcs_util.CustomAxisUtil.compute(artifacts) assert 'CTYPE must be the same across all Artifacts' in str(ex.value) diff --git a/caom2utils/caom2utils/tests/test_fits2caom2.py b/caom2utils/caom2utils/tests/test_fits2caom2.py index 880121ec..80385da3 100755 --- a/caom2utils/caom2utils/tests/test_fits2caom2.py +++ b/caom2utils/caom2utils/tests/test_fits2caom2.py @@ -79,8 +79,8 @@ from caom2utils.caom2blueprint import _get_and_update_artifact_meta from caom2utils.wcs_parsers import FitsWcsParser, Hdf5WcsParser -from caom2 import ObservationWriter, SimpleObservation, Algorithm, Artifact, ProductType, ReleaseType, DataProductType -from caom2 import get_differences, obs_reader_writer, ObservationReader, Chunk, ObservationIntentType, ChecksumURI +from caom2 import ObservationWriter, SimpleObservation, Algorithm, Artifact, DataLinkSemantics, ReleaseType, DataProductType +from caom2 import get_differences, obs_reader_writer, ObservationReader, Chunk, ObservationIntentType from caom2 import CustomWCS, SpectralWCS, TemporalWCS, PolarizationWCS, SpatialWCS, Axis, CoordAxis1D, CoordAxis2D from caom2 import CalibrationLevel @@ -145,7 +145,7 @@ class MyExitError(Exception): def test_augment_energy(): bp = ObsBlueprint(energy_axis=1) test_fitsparser = FitsParser(sample_file_4axes, bp) - artifact = Artifact('ad:{}/{}'.format('TEST', sample_file_4axes), ProductType.SCIENCE, ReleaseType.DATA) + artifact = Artifact('ad:{}/{}'.format('TEST', sample_file_4axes), DataLinkSemantics.PREVIEW_IMAGE, ReleaseType.DATA) test_fitsparser.augment_artifact(artifact) energy = artifact.parts['0'].chunks[0].energy ex = _get_from_str_xml(EXPECTED_ENERGY_XML, ObservationReader()._get_spectral_wcs, 'energy') @@ -163,7 +163,7 @@ def test_hdf5_wcs_parser_set_wcs(): test_f_name = 'taos2_test.h5' test_uri = f'cadc:TEST/{test_f_name}' test_fqn = f'{TESTDATA_DIR}/taos_h5file/20220201T200117/{test_f_name}' - test_artifact = Artifact(test_uri, ProductType.SCIENCE, ReleaseType.DATA) + test_artifact = Artifact(test_uri, DataLinkSemantics.PREVIEW_IMAGE, ReleaseType.DATA) # check the error messages test_position_bp.configure_position_axes((4, 5)) @@ -197,7 +197,7 @@ def test_hdf5_wcs_parser_set_wcs(): def test_augment_failure(): bp = ObsBlueprint() test_fitsparser = FitsParser(sample_file_4axes, bp) - artifact = Artifact('ad:{}/{}'.format('TEST', sample_file_4axes), ProductType.SCIENCE, ReleaseType.DATA) + artifact = Artifact('ad:{}/{}'.format('TEST', sample_file_4axes), DataLinkSemantics.PREVIEW_IMAGE, ReleaseType.DATA) with pytest.raises(TypeError): test_fitsparser.augment_artifact(artifact) @@ -242,7 +242,7 @@ def test_augment_artifact_energy_from_blueprint(): def test_augment_polarization(): test_fitsparser = FitsParser(sample_file_4axes, ObsBlueprint(polarization_axis=1)) - artifact = Artifact('ad:{}/{}'.format('TEST', sample_file_4axes), ProductType.SCIENCE, ReleaseType.DATA) + artifact = Artifact('ad:{}/{}'.format('TEST', sample_file_4axes), DataLinkSemantics.PREVIEW_IMAGE, ReleaseType.DATA) test_fitsparser.augment_artifact(artifact) polarization = artifact.parts['0'].chunks[0].polarization ex = _get_from_str_xml(EXPECTED_POLARIZATION_XML, ObservationReader()._get_polarization_wcs, 'polarization') @@ -306,7 +306,7 @@ def test_augment_artifact_polarization_from_blueprint(): def test_augment_artifact(): test_blueprint = ObsBlueprint(position_axes=(1, 2)) test_fitsparser = FitsParser(sample_file_4axes, test_blueprint) - artifact = Artifact('ad:{}/{}'.format('TEST', sample_file_4axes), ProductType.SCIENCE, ReleaseType.DATA) + artifact = Artifact('ad:{}/{}'.format('TEST', sample_file_4axes), DataLinkSemantics.PREVIEW_IMAGE, ReleaseType.DATA) test_fitsparser.augment_artifact(artifact) assert artifact.parts is not None assert len(artifact.parts) == 1 @@ -375,7 +375,7 @@ def test_augment_artifact_position_from_blueprint(): def test_augment_artifact_time(): test_fitsparser = FitsParser(sample_file_time_axes, ObsBlueprint(time_axis=1)) - artifact = Artifact('ad:{}/{}'.format('TEST', sample_file_time_axes), ProductType.SCIENCE, ReleaseType.DATA) + artifact = Artifact('ad:{}/{}'.format('TEST', sample_file_time_axes), DataLinkSemantics.PREVIEW_IMAGE, ReleaseType.DATA) test_fitsparser.augment_artifact(artifact) assert artifact.parts is not None assert len(artifact.parts) == 6 @@ -427,7 +427,7 @@ def test_get_wcs_values(): def test_wcs_parser_augment_failures(): test_parser = FitsWcsParser(get_test_header(sample_file_4axes)[0].header, sample_file_4axes, 0) - test_obs = SimpleObservation('collection', 'MA1_DRAO-ST', Algorithm('exposure')) + test_obs = SimpleObservation(collection='collection', uri='caom:MA1_DRAO-ST', algorithm=Algorithm('exposure')) with pytest.raises(ValueError): test_parser.augment_custom(test_obs) @@ -697,12 +697,13 @@ def test_augment_observation(): test_obs_blueprint.set('Plane.calibrationLevel', '2') test_fitsparser = FitsParser(sample_file_4axes_obs, test_obs_blueprint) test_fitsparser.blueprint = test_obs_blueprint - test_obs = SimpleObservation('collection', 'MA1_DRAO-ST', Algorithm('exposure')) + test_obs = SimpleObservation('collection', 'caom:collection/MA1_DRAO-ST', Algorithm('exposure')) test_fitsparser.augment_observation(test_obs, sample_file_4axes_uri, product_id='HI-line') assert test_obs is not None assert test_obs.planes is not None assert len(test_obs.planes) == 1 - test_plane = test_obs.planes['HI-line'] + test_plane_uri = f'{test_obs.uri}/HI-line' + test_plane = test_obs.planes[test_plane_uri] assert test_plane.artifacts is not None assert len(test_plane.artifacts) == 1 test_artifact = test_plane.artifacts[sample_file_4axes_uri] @@ -725,9 +726,9 @@ def test_augment_value_errors(): ob = ObsBlueprint(position_axes=(1, 2)) ob.set('Plane.productID', None) test_parser = BlueprintParser(obs_blueprint=ob) - test_obs = SimpleObservation('collection', 'MA1_DRAO-ST', Algorithm('exposure')) + test_obs = SimpleObservation('collection', 'caom:MA1_DRAO-ST', Algorithm('exposure')) with pytest.raises(ValueError): - test_parser.augment_observation(test_obs, 'cadc:TEST/abc.fits.gz', product_id=None) + test_parser.augment_observation(test_obs, 'cadc:TEST/abc.fits.gz') with pytest.raises(ValueError): test_parser.augment_plane(test_obs, 'cadc:TEST/abc.fits.gz') @@ -1054,22 +1055,25 @@ def _get_obs(from_xml_string): EXPECTED_GENERIC_PARSER_FILE_SCHEME_XML = ( """ test_collection_id - test_observation_id + caom:test_collection_id/test_observation_id + 6df exposure - test_product_id + caom:test_collection_id/test_observation_id/test_product_id image 3 + ad:foo/bar0 + 109 thumbnail data text/plain @@ -1108,7 +1112,7 @@ def test_generic_parser(): java_config_file, '--override', text_override, - fname, + 'ad:foo/bar0', ] main_app() if stdout_mock.getvalue(): @@ -1352,7 +1356,7 @@ def test_visit_generic_parser(): test_plugin = __name__ kwargs = {} test_obs = SimpleObservation( - collection='test_collection', observation_id='test_obs_id', algorithm=Algorithm('exposure') + collection='test_collection', uri='caom:test_obs_id', algorithm=Algorithm('exposure') ) _visit(test_plugin, test_parser, test_obs, visit_local=None, **kwargs) except ImportError: @@ -1390,10 +1394,10 @@ def test_get_vos_meta(vos_mock): ) vos_mock.return_value.get_node.side_effect = _get_node test_uri = 'vos://cadc.nrc.ca!vospace/CAOMworkshop/Examples/DAO/' 'dao_c122_2016_012725.fits' - test_artifact = Artifact(test_uri, ProductType.SCIENCE, ReleaseType.DATA) + test_artifact = Artifact(test_uri, DataLinkSemantics.PREVIEW_IMAGE, ReleaseType.DATA) _get_and_update_artifact_meta(test_uri, test_artifact, subject=None) assert test_artifact is not None - assert test_artifact.content_checksum.uri == 'md5:5b00b00d4b06aba986c3663d09aa581f', 'checksum wrong' + assert test_artifact.content_checksum == 'md5:5b00b00d4b06aba986c3663d09aa581f', 'checksum wrong' assert test_artifact.content_length == 682560, 'length wrong' assert test_artifact.content_type == 'application/fits', 'content_type wrong' assert vos_mock.called, 'mock not called' @@ -1434,7 +1438,7 @@ def test_get_external_headers_fails(get_external_mock): test_product_id = 'TEST_PRODUCT_ID' test_blueprint = caom2utils.caom2blueprint.ObsBlueprint() test_observation = SimpleObservation( - collection=test_collection, observation_id=test_obs_id, algorithm=Algorithm(name='exposure') + collection=test_collection, uri=f'caom:{test_obs_id}', algorithm=Algorithm(name='exposure') ) test_result = caom2utils.caom2blueprint._augment( obs=test_observation, @@ -1446,7 +1450,8 @@ def test_get_external_headers_fails(get_external_mock): ) assert test_result is not None, 'expect a result' assert len(test_result.planes.values()) == 1, 'plane added to result' - test_plane = test_result.planes[test_product_id] + plane_uri = f'{test_result.uri}/{test_product_id}' + test_plane = test_result.planes[plane_uri] assert len(test_plane.artifacts.values()) == 1, 'artifact added to plane' assert test_uri in test_plane.artifacts.keys(), 'wrong artifact uri' @@ -1528,10 +1533,12 @@ def get_time_exposure(self, ext): test_blueprint.set('Artifact.releaseType', 'data') test_blueprint.set('Chunk.time.exposure', 'get_time_exposure()', 1) test_parser = FitsParser(src=[hdr1, hdr2], obs_blueprint=test_blueprint) - test_obs = SimpleObservation('collection', 'MA1_DRAO-ST', Algorithm('exposure')) + test_obs = SimpleObservation('collection', 'caom:MA1_DRAO-ST', Algorithm('exposure')) test_parser.augment_observation(test_obs, 'cadc:TEST/test_file_name.fits') - assert 'PRODUCT_ID' in test_obs.planes.keys(), 'expect plane' - test_plane = test_obs.planes['PRODUCT_ID'] + # TODO + #assert 'PRODUCT_ID' in test_obs.planes.keys(), 'expect plane' + plane_uri = f'{test_obs.uri}/PRODUCT_ID' + test_plane = test_obs.planes[plane_uri] assert 'cadc:TEST/test_file_name.fits' in test_plane.artifacts.keys(), 'expect artifact' test_artifact = test_plane.artifacts.pop('cadc:TEST/test_file_name.fits') test_part = test_artifact.parts.pop('1') @@ -1545,7 +1552,7 @@ def get_time_exposure(self, ext): test_blueprint2.set('Plane.calibrationLevel', 'getCalibrationLevel()') test_blueprint2.set('Plane.dataProductType', 'broken_function()') test_parser2 = BlueprintParser(obs_blueprint=test_blueprint2) - test_obs2 = SimpleObservation('collection', 'MA1_DRAO-ST', Algorithm('exposure')) + test_obs2 = SimpleObservation('collection', 'caom:MA1_DRAO-ST', Algorithm('exposure')) with pytest.raises(ValueError): test_parser2.augment_observation(test_obs2, 'cadc:TEST/abc.fits.gz') @@ -1573,7 +1580,7 @@ def test_apply_blueprint_execute_external(): def test_update_artifact_meta_errors(): test_uri = 'gemini:GEMINI/abc.jpg' - test_artifact = Artifact(uri=test_uri, product_type=ProductType.SCIENCE, release_type=ReleaseType.DATA) + test_artifact = Artifact(uri=test_uri, product_type=DataLinkSemantics.PREVIEW_IMAGE, release_type=ReleaseType.DATA) client_mock = Mock(autospec=True) client_mock.info.return_value = FileInfo(id=test_uri, file_type='application/octet', size=42, md5sum='md5:42') test_uri = 'gemini://test.fits' @@ -1584,13 +1591,13 @@ def test_update_artifact_meta_errors(): test_uri = 'gemini:GEMINI/abc.jpg' _get_and_update_artifact_meta(test_uri, test_artifact, client=client_mock) - assert test_artifact.content_checksum == ChecksumURI(uri='md5:42'), 'checksum' + assert test_artifact.content_checksum == 'md5:42', 'checksum' assert test_artifact.content_length == 42, 'length' assert test_artifact.content_type == 'application/octet', 'type' # TODO - does this increase coverage? test_uri = 'file:///test.fits.header' - test_artifact = Artifact(uri=test_uri, product_type=ProductType.SCIENCE, release_type=ReleaseType.DATA) + test_artifact = Artifact(uri=test_uri, product_type=DataLinkSemantics.PREVIEW_IMAGE, release_type=ReleaseType.DATA) client_mock.info.return_value = None _get_and_update_artifact_meta(test_uri, test_artifact, net.Subject(), client=client_mock) assert test_artifact.content_type is None, 'type' @@ -1628,7 +1635,7 @@ def test_gen_proc_failure(augment_mock, stdout_mock, cap_mock, client_mock): @patch('sys.stdout', new_callable=StringIO) @patch('caom2utils.caom2blueprint.Client') def test_parser_construction(vos_mock, stdout_mock): - vos_mock.get_node.side_effect = _get_node + vos_mock.return_value.get_node.side_effect = _get_node test_uri = 'vos:goliaths/abc.fits.gz' test_blueprint = ObsBlueprint() test_blueprint.set('Observation.instrument.keywords', 'instrument keyword') diff --git a/caom2utils/caom2utils/tests/test_obs_blueprint.py b/caom2utils/caom2utils/tests/test_obs_blueprint.py index a396309c..b16f3a5c 100644 --- a/caom2utils/caom2utils/tests/test_obs_blueprint.py +++ b/caom2utils/caom2utils/tests/test_obs_blueprint.py @@ -82,7 +82,7 @@ def test_obs_blueprint(): assert elems != ObsBlueprint.CAOM2_ELEMENTS # default config (one entry per row...) - assert str(ObsBlueprint()).count('\n') == 24 + assert str(ObsBlueprint()).count('\n') == 23 print(ObsBlueprint()) # default config with WCS info @@ -92,7 +92,7 @@ def test_obs_blueprint(): position_axes=(1, 2), energy_axis=3, polarization_axis=4, time_axis=5, obs_axis=6, custom_axis=7 ) ).count('\n') - == 90 + == 89 # TODO why? ) ob = ObsBlueprint() diff --git a/caom2utils/caom2utils/tests/test_polygonvalidator.py b/caom2utils/caom2utils/tests/test_polygonvalidator.py index f73e3253..ac8bd07d 100644 --- a/caom2utils/caom2utils/tests/test_polygonvalidator.py +++ b/caom2utils/caom2utils/tests/test_polygonvalidator.py @@ -70,7 +70,7 @@ import pytest from caom2 import shape -from caom2utils import validate_polygon, validate_multipolygon +from caom2utils import validate_polygon def test_open_polygon(): @@ -117,8 +117,6 @@ def test_open_polygon(): v12 = shape.Vertex(0.0, 0.0, shape.SegmentType.CLOSE) closed_vertices = [v0, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12] - # should detect that multipolygon is closed - validate_multipolygon(shape.MultiPolygon(closed_vertices)) def test_polygon_self_intersection(): @@ -171,171 +169,171 @@ def test_polygon_self_intersection(): assert 'self intersecting' in str(ex.value) -def test_open_multipolygon(): - # should detect that multipolygon is not closed - v0 = shape.Vertex(-126.210938, 67.991108, shape.SegmentType.MOVE) - v1 = shape.Vertex(-108.984375, 70.480896, shape.SegmentType.LINE) - v2 = shape.Vertex(-98.789063, 66.912834, shape.SegmentType.LINE) - v3 = shape.Vertex(-75.234375, 60.217991, shape.SegmentType.LINE) - v4 = shape.Vertex(-87.890625, 52.241256, shape.SegmentType.LINE) - v5 = shape.Vertex(-110.742188, 54.136696, shape.SegmentType.LINE) - v6 = shape.Vertex(0.0, 0.0, shape.SegmentType.CLOSE) - v7 = shape.Vertex(24.609375, 62.895218, shape.SegmentType.MOVE) - v8 = shape.Vertex(43.593750, 67.322924, shape.SegmentType.LINE) - v9 = shape.Vertex(55.898438, 62.734601, shape.SegmentType.LINE) - v10 = shape.Vertex(46.757813, 56.145550, shape.SegmentType.LINE) - v11 = shape.Vertex(26.015625, 55.354135, shape.SegmentType.LINE) - v12 = shape.Vertex(0.0, 0.0, shape.SegmentType.CLOSE) - no_vertices = [] - too_few_vertices = [v0, v1, v6] - two_moves_vertices = [v0, v1, v7, v2, v3, v4, v5, v6] - no_move_vertices = [v1, v2, v3, v4, v5, v6] - two_closes_vertices = [v0, v1, v2, v3, v4, v5, v7, v8, v9, v10, v11, v12] - no_close_vertices = [v0, v1, v2, v3, v4, v5] - min_closed_vertices = [v0, v1, v2, v6] - closed_vertices = [v0, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12] - rv0 = shape.Vertex(26.015625, 55.354135, shape.SegmentType.MOVE) - rv1 = shape.Vertex(46.757813, 56.145550, shape.SegmentType.LINE) - rv2 = shape.Vertex(55.898438, 62.734601, shape.SegmentType.LINE) - rv3 = shape.Vertex(43.593750, 67.322924, shape.SegmentType.LINE) - rv4 = shape.Vertex(24.609375, 62.895218, shape.SegmentType.LINE) - rv5 = shape.Vertex(0.0, 0.0, shape.SegmentType.CLOSE) - rv6 = shape.Vertex(-110.742188, 54.136696, shape.SegmentType.MOVE) - rv7 = shape.Vertex(-87.890625, 52.241256, shape.SegmentType.LINE) - rv8 = shape.Vertex(-75.234375, 60.217991, shape.SegmentType.LINE) - rv9 = shape.Vertex(-98.789063, 66.912834, shape.SegmentType.LINE) - rv10 = shape.Vertex(-108.984375, 70.480896, shape.SegmentType.LINE) - rv11 = shape.Vertex(-126.210938, 67.991108, shape.SegmentType.LINE) - rv12 = shape.Vertex(0.0, 0.0, shape.SegmentType.CLOSE) - counter_clockwise_vertices = [rv0, rv1, rv2, rv3, rv4, rv5, rv6, rv7, rv8, rv9, rv10, rv11, rv12] - # should detect that the polygons is not clockwise - with pytest.raises(AssertionError) as ex: - validate_multipolygon(shape.MultiPolygon(counter_clockwise_vertices)) - assert 'clockwise winding direction' in str(ex.value) - # should detect that there are not enough number of vertices to produce a multipolygon - with pytest.raises(AssertionError) as ex: - validate_multipolygon(shape.MultiPolygon(no_vertices)) - assert 'invalid polygon: 0 vertices' in str(ex.value) - with pytest.raises(AssertionError) as ex: - validate_multipolygon(shape.MultiPolygon(too_few_vertices)) - assert 'invalid polygon: 3 vertices' in str(ex.value) - # no close between two 'MOVE' - with pytest.raises(AssertionError) as ex: - validate_multipolygon(shape.MultiPolygon(two_moves_vertices)) - assert 'invalid polygon: MOVE vertex when loop open' in str(ex.value) - # no 'MOVE' before a 'CLOSE' - with pytest.raises(AssertionError) as ex: - validate_multipolygon(shape.MultiPolygon(no_move_vertices)) - assert 'invalid polygon: first vertex is not a MOVE' in str(ex.value) - # no 'MOVE' between two 'CLOSE' - with pytest.raises(AssertionError) as ex: - validate_multipolygon(shape.MultiPolygon(two_closes_vertices)) - assert 'invalid polygon: MOVE vertex when loop open' in str(ex.value) - # no 'CLOSE' after a 'MOVE' - with pytest.raises(AssertionError) as ex: - validate_multipolygon(shape.MultiPolygon(no_close_vertices)) - assert 'invalid polygon: last vertex is not a CLOSE' in str(ex.value) - # multipolygon default constructor -> too few vertices - with pytest.raises(AssertionError): - validate_multipolygon(shape.MultiPolygon(None)) - # should detect that multipolygon is closed - validate_multipolygon(shape.MultiPolygon(min_closed_vertices)) - # should detect that multipolygon is closed - validate_multipolygon(shape.MultiPolygon(closed_vertices)) - # instantiated multipolygon should contain the same vertices - p = shape.MultiPolygon(vertices=closed_vertices) - validate_multipolygon(p) - actual_vertices = p.vertices - assert actual_vertices[0].cval1 == closed_vertices[0].cval1 - assert actual_vertices[0].cval2 == closed_vertices[0].cval2 - assert actual_vertices[0].type == shape.SegmentType.MOVE - assert actual_vertices[1].cval1 == closed_vertices[1].cval1 - assert actual_vertices[1].cval2 == closed_vertices[1].cval2 - assert actual_vertices[1].type == shape.SegmentType.LINE - assert actual_vertices[2].cval1 == closed_vertices[2].cval1 - assert actual_vertices[2].cval2 == closed_vertices[2].cval2 - assert actual_vertices[2].type == shape.SegmentType.LINE - assert actual_vertices[3].cval1 == closed_vertices[3].cval1 - assert actual_vertices[3].cval2 == closed_vertices[3].cval2 - assert actual_vertices[3].type == shape.SegmentType.LINE - assert actual_vertices[4].cval1 == closed_vertices[4].cval1 - assert actual_vertices[4].cval2 == closed_vertices[4].cval2 - assert actual_vertices[4].type == shape.SegmentType.LINE - assert actual_vertices[5].cval1 == closed_vertices[5].cval1 - assert actual_vertices[5].cval2 == closed_vertices[5].cval2 - assert actual_vertices[5].type == shape.SegmentType.LINE - assert actual_vertices[6].cval1 == closed_vertices[6].cval1 - assert actual_vertices[6].cval2 == closed_vertices[6].cval2 - assert actual_vertices[6].type == shape.SegmentType.CLOSE - assert actual_vertices[7].cval1 == closed_vertices[7].cval1 - assert actual_vertices[7].cval2 == closed_vertices[7].cval2 - assert actual_vertices[7].type == shape.SegmentType.MOVE - assert actual_vertices[8].cval1 == closed_vertices[8].cval1 - assert actual_vertices[8].cval2 == closed_vertices[8].cval2 - assert actual_vertices[8].type == shape.SegmentType.LINE - assert actual_vertices[9].cval1 == closed_vertices[9].cval1 - assert actual_vertices[9].cval2 == closed_vertices[9].cval2 - assert actual_vertices[9].type == shape.SegmentType.LINE - assert actual_vertices[10].cval1 == closed_vertices[10].cval1 - assert actual_vertices[10].cval2 == closed_vertices[10].cval2 - assert actual_vertices[10].type == shape.SegmentType.LINE - assert actual_vertices[11].cval1 == closed_vertices[11].cval1 - assert actual_vertices[11].cval2 == closed_vertices[11].cval2 - assert actual_vertices[11].type == shape.SegmentType.LINE - assert actual_vertices[12].cval1 == closed_vertices[12].cval1 - assert actual_vertices[12].cval2 == closed_vertices[12].cval2 - assert actual_vertices[12].type == shape.SegmentType.CLOSE - - -def test_multipoly_self_intersect(): - # should detect self segment intersection of the multipolygon not near a Pole - v1 = shape.Vertex(-115.488281, 45.867063, shape.SegmentType.MOVE) - v2 = shape.Vertex(-91.230469, 36.075742, shape.SegmentType.LINE) - v3 = shape.Vertex(-95.800781, 54.807017, shape.SegmentType.LINE) - v4 = shape.Vertex(-108.457031, 39.951859, shape.SegmentType.LINE) - v5 = shape.Vertex(0.0, 0.0, shape.SegmentType.CLOSE) - points_with_self_intersecting_segments = [v1, v2, v3, v4, v5] - with pytest.raises(AssertionError) as ex: - validate_multipolygon(shape.MultiPolygon(points_with_self_intersecting_segments)) - assert 'self intersecting' in str(ex.value) - # should detect self segment intersection of the multipolygon near the South Pole, with the Pole outside the - # multipolygon - v1 = shape.Vertex(0.6128286003, -89.8967940441, shape.SegmentType.MOVE) - v2 = shape.Vertex(210.6391743183, -89.9073892376, shape.SegmentType.LINE) - v3 = shape.Vertex(90.6405151921, -89.8972874698, shape.SegmentType.LINE) - v4 = shape.Vertex(270.6114701911, -89.90689353, shape.SegmentType.LINE) - v5 = shape.Vertex(0.0, 0.0, shape.SegmentType.CLOSE) - points_with_self_intersecting_segments = [v1, v2, v3, v4, v5] - with pytest.raises(AssertionError) as ex: - validate_multipolygon(shape.MultiPolygon(points_with_self_intersecting_segments)) - assert 'self intersecting' in str(ex.value) - # should detect self segment intersection of the multipolygon near the South Pole, with the Pole inside the - # multipolygon - v1 = shape.Vertex(0.6128286003, -89.8967940441, shape.SegmentType.MOVE) - v2 = shape.Vertex(130.6391743183, -89.9073892376, shape.SegmentType.LINE) - v3 = shape.Vertex(90.6405151921, -89.8972874698, shape.SegmentType.LINE) - v4 = shape.Vertex(270.6114701911, -89.90689353, shape.SegmentType.LINE) - v5 = shape.Vertex(0.0, 0.0, shape.SegmentType.CLOSE) - points_with_self_intersecting_segments = [v1, v2, v3, v4, v5] - with pytest.raises(AssertionError) as ex: - validate_multipolygon(shape.MultiPolygon(points_with_self_intersecting_segments)) - assert 'self intersecting' in str(ex.value) - # should detect self segment intersection of the multipolygon which intersects with meridian = 0 - v1 = shape.Vertex(-7.910156, 13.293411, shape.SegmentType.MOVE) - v2 = shape.Vertex(4.042969, 7.068185, shape.SegmentType.LINE) - v3 = shape.Vertex(4.746094, 18.030975, shape.SegmentType.LINE) - v4 = shape.Vertex(-6.855469, 6.369894, shape.SegmentType.LINE) - v5 = shape.Vertex(0.0, 0.0, shape.SegmentType.CLOSE) - points_with_self_intersecting_segments = [v1, v2, v3, v4, v5] - with pytest.raises(AssertionError) as ex: - validate_multipolygon(shape.MultiPolygon(points_with_self_intersecting_segments)) - assert 'self intersecting' in str(ex.value) - - -def test_failures(): - # nothing happens - validate_multipolygon(None) - - test_object = type('', (), {})() - with pytest.raises(ValueError): - validate_multipolygon(test_object) +# def test_open_multipolygon(): +# # should detect that multipolygon is not closed +# v0 = shape.Vertex(-126.210938, 67.991108, shape.SegmentType.MOVE) +# v1 = shape.Vertex(-108.984375, 70.480896, shape.SegmentType.LINE) +# v2 = shape.Vertex(-98.789063, 66.912834, shape.SegmentType.LINE) +# v3 = shape.Vertex(-75.234375, 60.217991, shape.SegmentType.LINE) +# v4 = shape.Vertex(-87.890625, 52.241256, shape.SegmentType.LINE) +# v5 = shape.Vertex(-110.742188, 54.136696, shape.SegmentType.LINE) +# v6 = shape.Vertex(0.0, 0.0, shape.SegmentType.CLOSE) +# v7 = shape.Vertex(24.609375, 62.895218, shape.SegmentType.MOVE) +# v8 = shape.Vertex(43.593750, 67.322924, shape.SegmentType.LINE) +# v9 = shape.Vertex(55.898438, 62.734601, shape.SegmentType.LINE) +# v10 = shape.Vertex(46.757813, 56.145550, shape.SegmentType.LINE) +# v11 = shape.Vertex(26.015625, 55.354135, shape.SegmentType.LINE) +# v12 = shape.Vertex(0.0, 0.0, shape.SegmentType.CLOSE) +# no_vertices = [] +# too_few_vertices = [v0, v1, v6] +# two_moves_vertices = [v0, v1, v7, v2, v3, v4, v5, v6] +# no_move_vertices = [v1, v2, v3, v4, v5, v6] +# two_closes_vertices = [v0, v1, v2, v3, v4, v5, v7, v8, v9, v10, v11, v12] +# no_close_vertices = [v0, v1, v2, v3, v4, v5] +# min_closed_vertices = [v0, v1, v2, v6] +# closed_vertices = [v0, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12] +# rv0 = shape.Vertex(26.015625, 55.354135, shape.SegmentType.MOVE) +# rv1 = shape.Vertex(46.757813, 56.145550, shape.SegmentType.LINE) +# rv2 = shape.Vertex(55.898438, 62.734601, shape.SegmentType.LINE) +# rv3 = shape.Vertex(43.593750, 67.322924, shape.SegmentType.LINE) +# rv4 = shape.Vertex(24.609375, 62.895218, shape.SegmentType.LINE) +# rv5 = shape.Vertex(0.0, 0.0, shape.SegmentType.CLOSE) +# rv6 = shape.Vertex(-110.742188, 54.136696, shape.SegmentType.MOVE) +# rv7 = shape.Vertex(-87.890625, 52.241256, shape.SegmentType.LINE) +# rv8 = shape.Vertex(-75.234375, 60.217991, shape.SegmentType.LINE) +# rv9 = shape.Vertex(-98.789063, 66.912834, shape.SegmentType.LINE) +# rv10 = shape.Vertex(-108.984375, 70.480896, shape.SegmentType.LINE) +# rv11 = shape.Vertex(-126.210938, 67.991108, shape.SegmentType.LINE) +# rv12 = shape.Vertex(0.0, 0.0, shape.SegmentType.CLOSE) +# counter_clockwise_vertices = [rv0, rv1, rv2, rv3, rv4, rv5, rv6, rv7, rv8, rv9, rv10, rv11, rv12] +# # should detect that the polygons is not clockwise +# with pytest.raises(AssertionError) as ex: +# validate_multipolygon(shape.MultiPolygon(counter_clockwise_vertices)) +# assert 'clockwise winding direction' in str(ex.value) +# # should detect that there are not enough number of vertices to produce a multipolygon +# with pytest.raises(AssertionError) as ex: +# validate_multipolygon(shape.MultiPolygon(no_vertices)) +# assert 'invalid polygon: 0 vertices' in str(ex.value) +# with pytest.raises(AssertionError) as ex: +# validate_multipolygon(shape.MultiPolygon(too_few_vertices)) +# assert 'invalid polygon: 3 vertices' in str(ex.value) +# # no close between two 'MOVE' +# with pytest.raises(AssertionError) as ex: +# validate_multipolygon(shape.MultiPolygon(two_moves_vertices)) +# assert 'invalid polygon: MOVE vertex when loop open' in str(ex.value) +# # no 'MOVE' before a 'CLOSE' +# with pytest.raises(AssertionError) as ex: +# validate_multipolygon(shape.MultiPolygon(no_move_vertices)) +# assert 'invalid polygon: first vertex is not a MOVE' in str(ex.value) +# # no 'MOVE' between two 'CLOSE' +# with pytest.raises(AssertionError) as ex: +# validate_multipolygon(shape.MultiPolygon(two_closes_vertices)) +# assert 'invalid polygon: MOVE vertex when loop open' in str(ex.value) +# # no 'CLOSE' after a 'MOVE' +# with pytest.raises(AssertionError) as ex: +# validate_multipolygon(shape.MultiPolygon(no_close_vertices)) +# assert 'invalid polygon: last vertex is not a CLOSE' in str(ex.value) +# # multipolygon default constructor -> too few vertices +# with pytest.raises(AssertionError): +# validate_multipolygon(shape.MultiPolygon(None)) +# # should detect that multipolygon is closed +# validate_multipolygon(shape.MultiPolygon(min_closed_vertices)) +# # should detect that multipolygon is closed +# validate_multipolygon(shape.MultiPolygon(closed_vertices)) +# # instantiated multipolygon should contain the same vertices +# p = shape.MultiPolygon(vertices=closed_vertices) +# validate_multipolygon(p) +# actual_vertices = p.vertices +# assert actual_vertices[0].cval1 == closed_vertices[0].cval1 +# assert actual_vertices[0].cval2 == closed_vertices[0].cval2 +# assert actual_vertices[0].type == shape.SegmentType.MOVE +# assert actual_vertices[1].cval1 == closed_vertices[1].cval1 +# assert actual_vertices[1].cval2 == closed_vertices[1].cval2 +# assert actual_vertices[1].type == shape.SegmentType.LINE +# assert actual_vertices[2].cval1 == closed_vertices[2].cval1 +# assert actual_vertices[2].cval2 == closed_vertices[2].cval2 +# assert actual_vertices[2].type == shape.SegmentType.LINE +# assert actual_vertices[3].cval1 == closed_vertices[3].cval1 +# assert actual_vertices[3].cval2 == closed_vertices[3].cval2 +# assert actual_vertices[3].type == shape.SegmentType.LINE +# assert actual_vertices[4].cval1 == closed_vertices[4].cval1 +# assert actual_vertices[4].cval2 == closed_vertices[4].cval2 +# assert actual_vertices[4].type == shape.SegmentType.LINE +# assert actual_vertices[5].cval1 == closed_vertices[5].cval1 +# assert actual_vertices[5].cval2 == closed_vertices[5].cval2 +# assert actual_vertices[5].type == shape.SegmentType.LINE +# assert actual_vertices[6].cval1 == closed_vertices[6].cval1 +# assert actual_vertices[6].cval2 == closed_vertices[6].cval2 +# assert actual_vertices[6].type == shape.SegmentType.CLOSE +# assert actual_vertices[7].cval1 == closed_vertices[7].cval1 +# assert actual_vertices[7].cval2 == closed_vertices[7].cval2 +# assert actual_vertices[7].type == shape.SegmentType.MOVE +# assert actual_vertices[8].cval1 == closed_vertices[8].cval1 +# assert actual_vertices[8].cval2 == closed_vertices[8].cval2 +# assert actual_vertices[8].type == shape.SegmentType.LINE +# assert actual_vertices[9].cval1 == closed_vertices[9].cval1 +# assert actual_vertices[9].cval2 == closed_vertices[9].cval2 +# assert actual_vertices[9].type == shape.SegmentType.LINE +# assert actual_vertices[10].cval1 == closed_vertices[10].cval1 +# assert actual_vertices[10].cval2 == closed_vertices[10].cval2 +# assert actual_vertices[10].type == shape.SegmentType.LINE +# assert actual_vertices[11].cval1 == closed_vertices[11].cval1 +# assert actual_vertices[11].cval2 == closed_vertices[11].cval2 +# assert actual_vertices[11].type == shape.SegmentType.LINE +# assert actual_vertices[12].cval1 == closed_vertices[12].cval1 +# assert actual_vertices[12].cval2 == closed_vertices[12].cval2 +# assert actual_vertices[12].type == shape.SegmentType.CLOSE +# +# +# def test_multipoly_self_intersect(): +# # should detect self segment intersection of the multipolygon not near a Pole +# v1 = shape.Vertex(-115.488281, 45.867063, shape.SegmentType.MOVE) +# v2 = shape.Vertex(-91.230469, 36.075742, shape.SegmentType.LINE) +# v3 = shape.Vertex(-95.800781, 54.807017, shape.SegmentType.LINE) +# v4 = shape.Vertex(-108.457031, 39.951859, shape.SegmentType.LINE) +# v5 = shape.Vertex(0.0, 0.0, shape.SegmentType.CLOSE) +# points_with_self_intersecting_segments = [v1, v2, v3, v4, v5] +# with pytest.raises(AssertionError) as ex: +# validate_multipolygon(shape.MultiPolygon(points_with_self_intersecting_segments)) +# assert 'self intersecting' in str(ex.value) +# # should detect self segment intersection of the multipolygon near the South Pole, with the Pole outside the +# # multipolygon +# v1 = shape.Vertex(0.6128286003, -89.8967940441, shape.SegmentType.MOVE) +# v2 = shape.Vertex(210.6391743183, -89.9073892376, shape.SegmentType.LINE) +# v3 = shape.Vertex(90.6405151921, -89.8972874698, shape.SegmentType.LINE) +# v4 = shape.Vertex(270.6114701911, -89.90689353, shape.SegmentType.LINE) +# v5 = shape.Vertex(0.0, 0.0, shape.SegmentType.CLOSE) +# points_with_self_intersecting_segments = [v1, v2, v3, v4, v5] +# with pytest.raises(AssertionError) as ex: +# validate_multipolygon(shape.MultiPolygon(points_with_self_intersecting_segments)) +# assert 'self intersecting' in str(ex.value) +# # should detect self segment intersection of the multipolygon near the South Pole, with the Pole inside the +# # multipolygon +# v1 = shape.Vertex(0.6128286003, -89.8967940441, shape.SegmentType.MOVE) +# v2 = shape.Vertex(130.6391743183, -89.9073892376, shape.SegmentType.LINE) +# v3 = shape.Vertex(90.6405151921, -89.8972874698, shape.SegmentType.LINE) +# v4 = shape.Vertex(270.6114701911, -89.90689353, shape.SegmentType.LINE) +# v5 = shape.Vertex(0.0, 0.0, shape.SegmentType.CLOSE) +# points_with_self_intersecting_segments = [v1, v2, v3, v4, v5] +# with pytest.raises(AssertionError) as ex: +# validate_multipolygon(shape.MultiPolygon(points_with_self_intersecting_segments)) +# assert 'self intersecting' in str(ex.value) +# # should detect self segment intersection of the multipolygon which intersects with meridian = 0 +# v1 = shape.Vertex(-7.910156, 13.293411, shape.SegmentType.MOVE) +# v2 = shape.Vertex(4.042969, 7.068185, shape.SegmentType.LINE) +# v3 = shape.Vertex(4.746094, 18.030975, shape.SegmentType.LINE) +# v4 = shape.Vertex(-6.855469, 6.369894, shape.SegmentType.LINE) +# v5 = shape.Vertex(0.0, 0.0, shape.SegmentType.CLOSE) +# points_with_self_intersecting_segments = [v1, v2, v3, v4, v5] +# with pytest.raises(AssertionError) as ex: +# validate_multipolygon(shape.MultiPolygon(points_with_self_intersecting_segments)) +# assert 'self intersecting' in str(ex.value) +# +# +# def test_failures(): +# # nothing happens +# validate_multipolygon(None) +# +# test_object = type('', (), {})() +# with pytest.raises(ValueError): +# validate_multipolygon(test_object) diff --git a/caom2utils/caom2utils/tests/test_wcsvalidator.py b/caom2utils/caom2utils/tests/test_wcsvalidator.py index 5605575a..53099b4f 100644 --- a/caom2utils/caom2utils/tests/test_wcsvalidator.py +++ b/caom2utils/caom2utils/tests/test_wcsvalidator.py @@ -2,7 +2,7 @@ # ****************** CANADIAN ASTRONOMY DATA CENTRE ******************* # ************* CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** # -# (c) 2019. (c) 2019. +# (c) 2025. (c) 2025. # Government of Canada Gouvernement du Canada # National Research Council Conseil national de recherches # Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 @@ -203,14 +203,14 @@ def test_plane(self): def test_part(self): # CAOM2 entity is a Part pname = "part1" - product_type = chunk.ProductType.SCIENCE + product_type = chunk.DataLinkSemantics.SCIENCE part = PartTestUtil.get_test_part(pname, product_type) validate_wcs(part) def test_artifact(self): # CAOM2 entity is an Artifact auri = "uri:foo/bar" - product_type = chunk.ProductType.SCIENCE + product_type = chunk.DataLinkSemantics.SCIENCE # with valid wcs artifact = ArtifactTestUtil.get_test_artifact(auri, product_type) validate_wcs(artifact) @@ -218,7 +218,7 @@ def test_artifact(self): def test_artifact_with_null_wcs(self): # with null wcs auri = "uri:foo/bar" - product_type = chunk.ProductType.SCIENCE + product_type = chunk.DataLinkSemantics.SCIENCE artifact = ArtifactTestUtil.get_test_artifact(auri, product_type) validate_wcs(artifact) c = artifact.parts['test_part'].chunks[0] @@ -604,7 +604,7 @@ def get_test_plane(planeID): uri1 = 'uri:foo/bar1' uri2 = 'uri:foo/bar2' uri3 = 'uri:foo/bar3' - product_type = chunk.ProductType.SCIENCE + product_type = chunk.DataLinkSemantics.SCIENCE a1 = ArtifactTestUtil.get_test_artifact(uri1, product_type) a2 = ArtifactTestUtil.get_test_artifact(uri2, product_type) a3 = ArtifactTestUtil.get_test_artifact(uri3, product_type) @@ -632,9 +632,9 @@ def get_good_test_chunk(ptype): @staticmethod def get_test_artifact(uri, ptype): - # chunk.ProductType.SCIENCE is a common type + # chunk.DataLinkSemantics.SCIENCE is a common type if ptype is None: - ptype = chunk.ProductType.SCIENCE + ptype = chunk.DataLinkSemantics.SCIENCE test_artifact = artifact.Artifact(uri, ptype, artifact.ReleaseType.DATA) chunks = TypedList(chunk.Chunk) chunks.append(ArtifactTestUtil.get_good_test_chunk(ptype)) @@ -652,7 +652,7 @@ def __init__(self): @staticmethod def get_test_part(pname, ptype): - # chunk.ProductType.SCIENCE is a common type + # chunk.DataLinkSemantics.SCIENCE is a common type chunks = TypedList(chunk.Chunk) chunks.append(ArtifactTestUtil.get_good_test_chunk(ptype)) diff --git a/caom2utils/caom2utils/wcs_util.py b/caom2utils/caom2utils/wcs_util.py index 64ab1891..9db7cef1 100644 --- a/caom2utils/caom2utils/wcs_util.py +++ b/caom2utils/caom2utils/wcs_util.py @@ -343,26 +343,26 @@ def range1d_to_interval(wcs, r): def _chose_product_type(artifacts): ret = None for a in artifacts: - if chunk.ProductType.SCIENCE == a.product_type: - return chunk.ProductType.SCIENCE + if chunk.DataLinkSemantics.SCIENCE == a.product_type: + return chunk.DataLinkSemantics.SCIENCE - if chunk.ProductType.CALIBRATION == a.product_type: - return chunk.ProductType.CALIBRATION + if chunk.DataLinkSemantics.CALIBRATION == a.product_type: + return chunk.DataLinkSemantics.CALIBRATION for p_key in a.parts: p = a.parts[p_key] - if chunk.ProductType.SCIENCE == p.product_type: - return chunk.ProductType.SCIENCE + if chunk.DataLinkSemantics.SCIENCE == p.product_type: + return chunk.DataLinkSemantics.SCIENCE - if chunk.ProductType.CALIBRATION == p.product_type: - return chunk.ProductType.CALIBRATION + if chunk.DataLinkSemantics.CALIBRATION == p.product_type: + return chunk.DataLinkSemantics.CALIBRATION for c in p.chunks: - if chunk.ProductType.SCIENCE == c.product_type: - return chunk.ProductType.SCIENCE + if chunk.DataLinkSemantics.SCIENCE == c.product_type: + return chunk.DataLinkSemantics.SCIENCE - if chunk.ProductType.CALIBRATION == c.product_type: - ret = chunk.ProductType.CALIBRATION + if chunk.DataLinkSemantics.CALIBRATION == c.product_type: + ret = chunk.DataLinkSemantics.CALIBRATION return ret @@ -398,9 +398,9 @@ def compute(artifacts): product_type = CustomAxisUtil._chose_product_type(artifacts) axis_ctype = CustomAxisUtil._get_ctype(artifacts, product_type) if axis_ctype is not None: - c = plane.CustomAxis(axis_ctype) if product_type is not None: - c.bounds = CustomAxisUtil.compute_bounds(artifacts, product_type, axis_ctype) + bounds = CustomAxisUtil.compute_bounds(artifacts, product_type, axis_ctype) + c = plane.CustomAxis(axis_ctype, bounds, [bounds]) if c.dimension is None: c.dimension = CustomAxisUtil.compute_dimension_from_wcs( c.bounds, artifacts, product_type, axis_ctype @@ -466,7 +466,7 @@ def compute_bounds(artifacts, product_type, expected_ctype): lb = min(lb, sub.lower) ub = max(ub, sub.upper) - return shape.Interval(lb, ub, subs) + return shape.Interval(lb, ub) @staticmethod def compute_dimension_from_wcs(bounds, artifacts, product_type, expected_ctype): From a29f58831043ae7c9e0bb0785e553a283851d6d6 Mon Sep 17 00:00:00 2001 From: Adrian Damian Date: Fri, 7 Feb 2025 22:18:22 -0800 Subject: [PATCH 6/6] Fixed style in caom2utils --- caom2utils/caom2utils/caom2blueprint.py | 3 +- .../caom2utils/tests/test_fits2caom2.py | 9 +++--- .../caom2utils/tests/test_polygonvalidator.py | 29 +++++++++---------- 3 files changed, 21 insertions(+), 20 deletions(-) diff --git a/caom2utils/caom2utils/caom2blueprint.py b/caom2utils/caom2utils/caom2blueprint.py index bf915685..55047165 100755 --- a/caom2utils/caom2utils/caom2blueprint.py +++ b/caom2utils/caom2utils/caom2blueprint.py @@ -360,7 +360,8 @@ def _augment( plane = obs.planes[plane_uri] if uri not in plane.artifacts.keys(): - plane.artifacts.add(Artifact(uri=str(uri), product_type=DataLinkSemantics.SCIENCE, release_type=ReleaseType.DATA)) + plane.artifacts.add(Artifact(uri=str(uri), product_type=DataLinkSemantics.SCIENCE, + release_type=ReleaseType.DATA)) meta_uri = uri visit_local = None diff --git a/caom2utils/caom2utils/tests/test_fits2caom2.py b/caom2utils/caom2utils/tests/test_fits2caom2.py index 80385da3..e35d0f91 100755 --- a/caom2utils/caom2utils/tests/test_fits2caom2.py +++ b/caom2utils/caom2utils/tests/test_fits2caom2.py @@ -79,7 +79,8 @@ from caom2utils.caom2blueprint import _get_and_update_artifact_meta from caom2utils.wcs_parsers import FitsWcsParser, Hdf5WcsParser -from caom2 import ObservationWriter, SimpleObservation, Algorithm, Artifact, DataLinkSemantics, ReleaseType, DataProductType +from caom2 import (ObservationWriter, SimpleObservation, Algorithm, Artifact, + DataLinkSemantics, ReleaseType, DataProductType) from caom2 import get_differences, obs_reader_writer, ObservationReader, Chunk, ObservationIntentType from caom2 import CustomWCS, SpectralWCS, TemporalWCS, PolarizationWCS, SpatialWCS, Axis, CoordAxis1D, CoordAxis2D from caom2 import CalibrationLevel @@ -375,7 +376,8 @@ def test_augment_artifact_position_from_blueprint(): def test_augment_artifact_time(): test_fitsparser = FitsParser(sample_file_time_axes, ObsBlueprint(time_axis=1)) - artifact = Artifact('ad:{}/{}'.format('TEST', sample_file_time_axes), DataLinkSemantics.PREVIEW_IMAGE, ReleaseType.DATA) + artifact = Artifact('ad:{}/{}'.format('TEST', sample_file_time_axes), + DataLinkSemantics.PREVIEW_IMAGE, ReleaseType.DATA) test_fitsparser.augment_artifact(artifact) assert artifact.parts is not None assert len(artifact.parts) == 6 @@ -1535,9 +1537,8 @@ def get_time_exposure(self, ext): test_parser = FitsParser(src=[hdr1, hdr2], obs_blueprint=test_blueprint) test_obs = SimpleObservation('collection', 'caom:MA1_DRAO-ST', Algorithm('exposure')) test_parser.augment_observation(test_obs, 'cadc:TEST/test_file_name.fits') - # TODO - #assert 'PRODUCT_ID' in test_obs.planes.keys(), 'expect plane' plane_uri = f'{test_obs.uri}/PRODUCT_ID' + assert plane_uri in test_obs.planes.keys(), 'expect plane' test_plane = test_obs.planes[plane_uri] assert 'cadc:TEST/test_file_name.fits' in test_plane.artifacts.keys(), 'expect artifact' test_artifact = test_plane.artifacts.pop('cadc:TEST/test_file_name.fits') diff --git a/caom2utils/caom2utils/tests/test_polygonvalidator.py b/caom2utils/caom2utils/tests/test_polygonvalidator.py index ac8bd07d..eaf24148 100644 --- a/caom2utils/caom2utils/tests/test_polygonvalidator.py +++ b/caom2utils/caom2utils/tests/test_polygonvalidator.py @@ -102,21 +102,20 @@ def test_open_polygon(): validate_polygon(shape.Polygon(closed_points)) # should detect that multipolygon is not closed - v0 = shape.Vertex(-126.210938, 67.991108, shape.SegmentType.MOVE) - v1 = shape.Vertex(-108.984375, 70.480896, shape.SegmentType.LINE) - v2 = shape.Vertex(-98.789063, 66.912834, shape.SegmentType.LINE) - v3 = shape.Vertex(-75.234375, 60.217991, shape.SegmentType.LINE) - v4 = shape.Vertex(-87.890625, 52.241256, shape.SegmentType.LINE) - v5 = shape.Vertex(-110.742188, 54.136696, shape.SegmentType.LINE) - v6 = shape.Vertex(0.0, 0.0, shape.SegmentType.CLOSE) - v7 = shape.Vertex(24.609375, 62.895218, shape.SegmentType.MOVE) - v8 = shape.Vertex(43.593750, 67.322924, shape.SegmentType.LINE) - v9 = shape.Vertex(55.898438, 62.734601, shape.SegmentType.LINE) - v10 = shape.Vertex(46.757813, 56.145550, shape.SegmentType.LINE) - v11 = shape.Vertex(26.015625, 55.354135, shape.SegmentType.LINE) - v12 = shape.Vertex(0.0, 0.0, shape.SegmentType.CLOSE) - closed_vertices = [v0, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12] - + # v0 = shape.Vertex(-126.210938, 67.991108, shape.SegmentType.MOVE) + # v1 = shape.Vertex(-108.984375, 70.480896, shape.SegmentType.LINE) + # v2 = shape.Vertex(-98.789063, 66.912834, shape.SegmentType.LINE) + # v3 = shape.Vertex(-75.234375, 60.217991, shape.SegmentType.LINE) + # v4 = shape.Vertex(-87.890625, 52.241256, shape.SegmentType.LINE) + # v5 = shape.Vertex(-110.742188, 54.136696, shape.SegmentType.LINE) + # v6 = shape.Vertex(0.0, 0.0, shape.SegmentType.CLOSE) + # v7 = shape.Vertex(24.609375, 62.895218, shape.SegmentType.MOVE) + # v8 = shape.Vertex(43.593750, 67.322924, shape.SegmentType.LINE) + # v9 = shape.Vertex(55.898438, 62.734601, shape.SegmentType.LINE) + # v10 = shape.Vertex(46.757813, 56.145550, shape.SegmentType.LINE) + # v11 = shape.Vertex(26.015625, 55.354135, shape.SegmentType.LINE) + # v12 = shape.Vertex(0.0, 0.0, shape.SegmentType.CLOSE) + # closed_vertices = [v0, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12] def test_polygon_self_intersection():