caom2utils 1.7.3__tar.gz → 1.7.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {caom2utils-1.7.3/caom2utils.egg-info → caom2utils-1.7.4}/PKG-INFO +8 -3
- {caom2utils-1.7.3 → caom2utils-1.7.4}/caom2utils/blueprints.py +1 -3
- {caom2utils-1.7.3 → caom2utils-1.7.4}/caom2utils/caom2blueprint.py +19 -12
- {caom2utils-1.7.3 → caom2utils-1.7.4}/caom2utils/data_util.py +21 -10
- {caom2utils-1.7.3 → caom2utils-1.7.4}/caom2utils/legacy.py +13 -5
- {caom2utils-1.7.3 → caom2utils-1.7.4}/caom2utils/parsers.py +2 -1
- {caom2utils-1.7.3 → caom2utils-1.7.4}/caom2utils/tests/data/help.txt +3 -1
- caom2utils-1.7.4/caom2utils/tests/test_cli_parsers.py +146 -0
- {caom2utils-1.7.3 → caom2utils-1.7.4}/caom2utils/tests/test_collections.py +4 -1
- {caom2utils-1.7.3 → caom2utils-1.7.4}/caom2utils/tests/test_data_util.py +26 -5
- {caom2utils-1.7.3 → caom2utils-1.7.4}/caom2utils/tests/test_fits2caom2.py +20 -46
- caom2utils-1.7.4/caom2utils/version.py +1 -0
- {caom2utils-1.7.3 → caom2utils-1.7.4/caom2utils.egg-info}/PKG-INFO +8 -3
- {caom2utils-1.7.3 → caom2utils-1.7.4}/caom2utils.egg-info/SOURCES.txt +2 -4
- {caom2utils-1.7.3 → caom2utils-1.7.4}/caom2utils.egg-info/requires.txt +1 -1
- {caom2utils-1.7.3 → caom2utils-1.7.4}/setup.cfg +5 -5
- {caom2utils-1.7.3 → caom2utils-1.7.4}/setup.py +6 -1
- caom2utils-1.7.3/caom2utils/tests/data/missing_observation_help.txt +0 -8
- caom2utils-1.7.3/caom2utils/tests/data/missing_positional_argument_help.txt +0 -10
- caom2utils-1.7.3/caom2utils/tests/data/too_few_arguments_help.txt +0 -0
- caom2utils-1.7.3/caom2utils/version.py +0 -1
- {caom2utils-1.7.3 → caom2utils-1.7.4}/LICENSE +0 -0
- {caom2utils-1.7.3 → caom2utils-1.7.4}/MANIFEST.in +0 -0
- {caom2utils-1.7.3 → caom2utils-1.7.4}/README.rst +0 -0
- {caom2utils-1.7.3 → caom2utils-1.7.4}/caom2utils/__init__.py +0 -0
- {caom2utils-1.7.3 → caom2utils-1.7.4}/caom2utils/caomvalidator.py +0 -0
- {caom2utils-1.7.3 → caom2utils-1.7.4}/caom2utils/polygonvalidator.py +0 -0
- {caom2utils-1.7.3 → caom2utils-1.7.4}/caom2utils/tests/__init__.py +0 -0
- {caom2utils-1.7.3 → caom2utils-1.7.4}/caom2utils/tests/conftest.py +0 -0
- {caom2utils-1.7.3 → caom2utils-1.7.4}/caom2utils/tests/data/4axes.fits +0 -0
- {caom2utils-1.7.3 → caom2utils-1.7.4}/caom2utils/tests/data/4axes.override +0 -0
- {caom2utils-1.7.3 → caom2utils-1.7.4}/caom2utils/tests/data/4axes_obs.fits +0 -0
- {caom2utils-1.7.3 → caom2utils-1.7.4}/caom2utils/tests/data/SampleComposite-CAOM-2.3.xml +0 -0
- {caom2utils-1.7.3 → caom2utils-1.7.4}/caom2utils/tests/data/bad_product_id.txt +0 -0
- {caom2utils-1.7.3 → caom2utils-1.7.4}/caom2utils/tests/data/bad_product_id.xml +0 -0
- {caom2utils-1.7.3 → caom2utils-1.7.4}/caom2utils/tests/data/edge_case.blueprint +0 -0
- {caom2utils-1.7.3 → caom2utils-1.7.4}/caom2utils/tests/data/fits2caom2.config +0 -0
- {caom2utils-1.7.3 → caom2utils-1.7.4}/caom2utils/tests/data/java.config +0 -0
- {caom2utils-1.7.3 → caom2utils-1.7.4}/caom2utils/tests/data/missing_product_id.txt +0 -0
- {caom2utils-1.7.3 → caom2utils-1.7.4}/caom2utils/tests/data/nonconformant.py +0 -0
- {caom2utils-1.7.3 → caom2utils-1.7.4}/caom2utils/tests/data/test.override +0 -0
- {caom2utils-1.7.3 → caom2utils-1.7.4}/caom2utils/tests/data/test_plugin.py +0 -0
- {caom2utils-1.7.3 → caom2utils-1.7.4}/caom2utils/tests/data/test_plugin_class.py +0 -0
- {caom2utils-1.7.3 → caom2utils-1.7.4}/caom2utils/tests/data/text.override +0 -0
- {caom2utils-1.7.3 → caom2utils-1.7.4}/caom2utils/tests/data/time_axes.fits +0 -0
- {caom2utils-1.7.3 → caom2utils-1.7.4}/caom2utils/tests/test_caomvalidator.py +0 -0
- {caom2utils-1.7.3 → caom2utils-1.7.4}/caom2utils/tests/test_convert_from_java.py +0 -0
- {caom2utils-1.7.3 → caom2utils-1.7.4}/caom2utils/tests/test_custom_axis_util.py +0 -0
- {caom2utils-1.7.3 → caom2utils-1.7.4}/caom2utils/tests/test_obs_blueprint.py +0 -0
- {caom2utils-1.7.3 → caom2utils-1.7.4}/caom2utils/tests/test_polygonvalidator.py +0 -0
- {caom2utils-1.7.3 → caom2utils-1.7.4}/caom2utils/tests/test_si_uris.py +0 -0
- {caom2utils-1.7.3 → caom2utils-1.7.4}/caom2utils/tests/test_wcsvalidator.py +0 -0
- {caom2utils-1.7.3 → caom2utils-1.7.4}/caom2utils/wcs_parsers.py +0 -0
- {caom2utils-1.7.3 → caom2utils-1.7.4}/caom2utils/wcs_util.py +0 -0
- {caom2utils-1.7.3 → caom2utils-1.7.4}/caom2utils/wcsvalidator.py +0 -0
- {caom2utils-1.7.3 → caom2utils-1.7.4}/caom2utils.egg-info/dependency_links.txt +0 -0
- {caom2utils-1.7.3 → caom2utils-1.7.4}/caom2utils.egg-info/entry_points.txt +0 -0
- {caom2utils-1.7.3 → caom2utils-1.7.4}/caom2utils.egg-info/not-zip-safe +0 -0
- {caom2utils-1.7.3 → caom2utils-1.7.4}/caom2utils.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: caom2utils
|
|
3
|
-
Version: 1.7.
|
|
3
|
+
Version: 1.7.4
|
|
4
4
|
Summary: CAOM-2.4 utils
|
|
5
5
|
Home-page: https://www.cadc-ccda.hia-iha.nrc-cnrc.gc.ca/caom2
|
|
6
6
|
Author: Canadian Astronomy Data Centre
|
|
@@ -10,10 +10,15 @@ Classifier: Natural Language :: English
|
|
|
10
10
|
Classifier: License :: OSI Approved :: GNU Affero General Public License v3
|
|
11
11
|
Classifier: Programming Language :: Python
|
|
12
12
|
Classifier: Programming Language :: Python :: 3
|
|
13
|
-
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Requires-Python: >=3.10, <3.15
|
|
14
19
|
License-File: LICENSE
|
|
15
20
|
Requires-Dist: cadcdata>=2.5.2
|
|
16
|
-
Requires-Dist: caom2
|
|
21
|
+
Requires-Dist: caom2<3,>=2.6
|
|
17
22
|
Requires-Dist: astropy>=2.0
|
|
18
23
|
Requires-Dist: spherical-geometry>=1.2.11
|
|
19
24
|
Requires-Dist: vos>=3.1.1
|
|
@@ -982,9 +982,7 @@ class ObsBlueprint:
|
|
|
982
982
|
self._extensions[extension][caom2_element][1].insert(0, ttype_attribute)
|
|
983
983
|
else:
|
|
984
984
|
raise AttributeError(
|
|
985
|
-
|
|
986
|
-
extension, caom2_element
|
|
987
|
-
)
|
|
985
|
+
f'No TTYPE attributes in extension {extension} associated with keyword {caom2_element}'
|
|
988
986
|
)
|
|
989
987
|
else:
|
|
990
988
|
self._extensions[extension][caom2_element] = ('BINTABLE', [ttype_attribute], index)
|
|
@@ -467,7 +467,10 @@ def _load_module(module):
|
|
|
467
467
|
raise e
|
|
468
468
|
|
|
469
469
|
|
|
470
|
-
def
|
|
470
|
+
def build_caom2gen_parser():
|
|
471
|
+
"""
|
|
472
|
+
Build the ArgumentParser for caom2gen (without parsing argv).
|
|
473
|
+
"""
|
|
471
474
|
parser = get_gen_proc_arg_parser()
|
|
472
475
|
parser.add_argument(
|
|
473
476
|
'--blueprint',
|
|
@@ -479,6 +482,11 @@ def caom2gen():
|
|
|
479
482
|
'per lineage entry.'
|
|
480
483
|
),
|
|
481
484
|
)
|
|
485
|
+
return parser
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def caom2gen():
|
|
489
|
+
parser = build_caom2gen_parser()
|
|
482
490
|
|
|
483
491
|
if len(sys.argv) < 2:
|
|
484
492
|
parser.print_usage(file=sys.stderr)
|
|
@@ -528,10 +536,11 @@ def caom2gen():
|
|
|
528
536
|
try:
|
|
529
537
|
gen_proc(args, blueprints)
|
|
530
538
|
except Exception as e:
|
|
539
|
+
from cadcutils.util.cli_errors import format_user_error
|
|
531
540
|
logging.error('Failed caom2gen execution.')
|
|
532
|
-
logging.error(e)
|
|
533
|
-
|
|
534
|
-
|
|
541
|
+
logging.error(format_user_error(e))
|
|
542
|
+
if logging.getLogger().isEnabledFor(logging.DEBUG):
|
|
543
|
+
logging.debug(traceback.format_exc())
|
|
535
544
|
sys.exit(-1)
|
|
536
545
|
|
|
537
546
|
logging.debug(f'Done {APP_NAME} processing.')
|
|
@@ -711,7 +720,9 @@ def proc(args, obs_blueprints):
|
|
|
711
720
|
raise RuntimeError(msg)
|
|
712
721
|
|
|
713
722
|
subject = net.Subject.from_cmd_line_args(args)
|
|
714
|
-
client = data_util.StorageClientWrapper(
|
|
723
|
+
client = data_util.StorageClientWrapper(
|
|
724
|
+
subject, resource_id=args.resource_id, host=args.host,
|
|
725
|
+
insecure=args.insecure)
|
|
715
726
|
validate_wcs = True
|
|
716
727
|
if args.no_validate:
|
|
717
728
|
validate_wcs = False
|
|
@@ -831,13 +842,9 @@ def gen_proc(args, blueprints, **kwargs):
|
|
|
831
842
|
connected = False
|
|
832
843
|
else:
|
|
833
844
|
subject = net.Subject.from_cmd_line_args(args)
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
else:
|
|
838
|
-
# if the resource_id is defined, assume that the caller intends to use the Storage Inventory system, as
|
|
839
|
-
# it's the CADC storage client that depends on a resource_id
|
|
840
|
-
client = data_util.StorageClientWrapper(subject, resource_id=args.resource_id)
|
|
845
|
+
client = data_util.StorageClientWrapper(
|
|
846
|
+
subject, resource_id=args.resource_id, host=args.host,
|
|
847
|
+
insecure=args.insecure)
|
|
841
848
|
|
|
842
849
|
for ii, cardinality in enumerate(args.lineage):
|
|
843
850
|
product_id, uri = _extract_ids(cardinality)
|
|
@@ -95,7 +95,8 @@ class StorageClientWrapper:
|
|
|
95
95
|
Wrap the metrics collection with StorageInventoryClient.
|
|
96
96
|
"""
|
|
97
97
|
|
|
98
|
-
def __init__(self, subject, resource_id='ivo://cadc.nrc.ca/uvic/minoc', metrics=None
|
|
98
|
+
def __init__(self, subject, resource_id='ivo://cadc.nrc.ca/uvic/minoc', metrics=None,
|
|
99
|
+
host=None, insecure=False):
|
|
99
100
|
"""
|
|
100
101
|
:param subject: net.Subject instance for authentication and authorization
|
|
101
102
|
:param resource_id: str identifies the StorageInventoryClient endpoint. Defaults to the installation closest to
|
|
@@ -103,8 +104,11 @@ class StorageClientWrapper:
|
|
|
103
104
|
:param metrics: caom2pipe.manaage_composable.Metrics instance. If set, will track execution times, by action,
|
|
104
105
|
from the beginning of the method invocation to the end of the method invocation, success or failure.
|
|
105
106
|
Defaults to None, because fits2caom2 is a stand-alone application.
|
|
107
|
+
:param host: Host server for the storage inventory service
|
|
108
|
+
:param insecure: skip SSL server certificate verification (for testing only)
|
|
106
109
|
"""
|
|
107
|
-
self._cadc_client = StorageInventoryClient(
|
|
110
|
+
self._cadc_client = StorageInventoryClient(
|
|
111
|
+
subject=subject, resource_id=resource_id, host=host, insecure=insecure)
|
|
108
112
|
self._metrics = metrics
|
|
109
113
|
self._logger = logging.getLogger(self.__class__.__name__)
|
|
110
114
|
|
|
@@ -274,15 +278,22 @@ def _clean_headers(fits_header):
|
|
|
274
278
|
|
|
275
279
|
|
|
276
280
|
def get_local_headers_from_fits(fqn):
|
|
277
|
-
"""Create a list of fits.Header instances from a
|
|
278
|
-
|
|
279
|
-
|
|
281
|
+
"""Create a list of fits.Header instances from a FITS file or plain-text
|
|
282
|
+
header file.
|
|
283
|
+
|
|
284
|
+
When the file is not a FITS binary (e.g. a ``*.fits.header`` text file),
|
|
285
|
+
headers are parsed from text instead.
|
|
286
|
+
|
|
287
|
+
:param fqn: fully-qualified name of the file on disk
|
|
288
|
+
:return: list of fits.Header instances
|
|
280
289
|
"""
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
290
|
+
try:
|
|
291
|
+
with fits.open(fqn, memmap=True, lazy_load_hdus=True) as hdulist:
|
|
292
|
+
hdulist.verify('fix')
|
|
293
|
+
return [h.header for h in hdulist]
|
|
294
|
+
except OSError:
|
|
295
|
+
with open(fqn, encoding='utf-8', errors='replace') as f:
|
|
296
|
+
return make_headers_from_string(f.read())
|
|
286
297
|
|
|
287
298
|
|
|
288
299
|
def get_local_file_headers(fqn):
|
|
@@ -457,10 +457,12 @@ def update_blueprint(obs_blueprint, artifact_uri=None, config=None, defaults=Non
|
|
|
457
457
|
return None
|
|
458
458
|
|
|
459
459
|
|
|
460
|
-
def
|
|
460
|
+
def build_fits2caom2_parser():
|
|
461
|
+
"""
|
|
462
|
+
Build the ArgumentParser for fits2caom2 (without parsing argv).
|
|
463
|
+
"""
|
|
461
464
|
parser = caom2blueprint.get_arg_parser()
|
|
462
465
|
|
|
463
|
-
# add legacy fits2caom2 arguments
|
|
464
466
|
parser.add_argument(
|
|
465
467
|
'--config',
|
|
466
468
|
required=False,
|
|
@@ -469,6 +471,11 @@ def main_app():
|
|
|
469
471
|
|
|
470
472
|
parser.add_argument('--default', help='file with default values for keywords')
|
|
471
473
|
parser.add_argument('--override', help='file with override values for keywords')
|
|
474
|
+
return parser
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def main_app():
|
|
478
|
+
parser = build_fits2caom2_parser()
|
|
472
479
|
|
|
473
480
|
if len(sys.argv) < 2:
|
|
474
481
|
# correct error message when running python3
|
|
@@ -507,9 +514,10 @@ def main_app():
|
|
|
507
514
|
try:
|
|
508
515
|
caom2blueprint.proc(args, obs_blueprint)
|
|
509
516
|
except Exception as e:
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
logging.
|
|
517
|
+
from cadcutils.util.cli_errors import format_user_error
|
|
518
|
+
logging.error(format_user_error(e))
|
|
519
|
+
if logging.getLogger().isEnabledFor(logging.DEBUG):
|
|
520
|
+
logging.debug(traceback.format_exc())
|
|
513
521
|
sys.exit(-1)
|
|
514
522
|
|
|
515
523
|
logging.info("DONE")
|
|
@@ -1899,7 +1899,8 @@ class Hdf5Parser(ContentParser):
|
|
|
1899
1899
|
Retrieve metadata from file, cache in the blueprint.
|
|
1900
1900
|
"""
|
|
1901
1901
|
self.logger.debug('Begin apply_blueprint_from_file')
|
|
1902
|
-
# h5py is an extra in this package since most collections do not
|
|
1902
|
+
# h5py is an extra in this package since most collections do not
|
|
1903
|
+
# require it
|
|
1903
1904
|
import h5py
|
|
1904
1905
|
|
|
1905
1906
|
individual, multi, attributes, candidate_extensions = self._extract_path_names_from_blueprint()
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
usage: fits2caom2 [-h]
|
|
1
|
+
usage: fits2caom2 [-h]
|
|
2
|
+
[--cert CERT | -n | --netrc-file NETRC_FILE | -u USER | --token TOKEN]
|
|
2
3
|
[--resource-id RESOURCE_ID] [-q | -v] [-V] [--dumpconfig]
|
|
3
4
|
[--not_connected] [--no_validate] [-o OUT_OBS_XML]
|
|
4
5
|
(-i IN_OBS_XML | --observation collection observationID)
|
|
@@ -41,6 +42,7 @@ optional arguments:
|
|
|
41
42
|
--resource-id RESOURCE_ID
|
|
42
43
|
resource identifier (default
|
|
43
44
|
ivo://cadc.nrc.ca/global/raven)
|
|
45
|
+
--token TOKEN authentication token to use.
|
|
44
46
|
-u, --user USER name of user to authenticate. Note: application
|
|
45
47
|
prompts for the corresponding password!
|
|
46
48
|
-v, --verbose verbose messages
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# ***********************************************************************
|
|
2
|
+
# ****************** CANADIAN ASTRONOMY DATA CENTRE *******************
|
|
3
|
+
# ************* CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES **************
|
|
4
|
+
#
|
|
5
|
+
# (c) 2026. (c) 2026.
|
|
6
|
+
# Government of Canada Gouvernement du Canada
|
|
7
|
+
# National Research Council Conseil national de recherches
|
|
8
|
+
# Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6
|
|
9
|
+
# All rights reserved Tous droits réservés
|
|
10
|
+
#
|
|
11
|
+
# NRC disclaims any warranties, Le CNRC dénie toute garantie
|
|
12
|
+
# expressed, implied, or énoncée, implicite ou légale,
|
|
13
|
+
# statutory, of any kind with de quelque nature que ce
|
|
14
|
+
# respect to the software, soit, concernant le logiciel,
|
|
15
|
+
# including without limitation y compris sans restriction
|
|
16
|
+
# any warranty of merchantability toute garantie de valeur
|
|
17
|
+
# or fitness for a particular marchande ou de pertinence
|
|
18
|
+
# purpose. NRC shall not be pour un usage particulier.
|
|
19
|
+
# liable in any event for any Le CNRC ne pourra en aucun cas
|
|
20
|
+
# damages, whether direct or être tenu responsable de tout
|
|
21
|
+
# indirect, special or general, dommage, direct ou indirect,
|
|
22
|
+
# consequential or incidental, particulier ou général,
|
|
23
|
+
# arising from the use of the accessoire ou fortuit, résultant
|
|
24
|
+
# software. Neither the name de l'utilisation du logiciel. Ni
|
|
25
|
+
# of the National Research le nom du Conseil National de
|
|
26
|
+
# Council of Canada nor the Recherches du Canada ni les noms
|
|
27
|
+
# names of its contributors may de ses participants ne peuvent
|
|
28
|
+
# be used to endorse or promote être utilisés pour approuver ou
|
|
29
|
+
# products derived from this promouvoir les produits dérivés
|
|
30
|
+
# software without specific prior de ce logiciel sans autorisation
|
|
31
|
+
# written permission. préalable et particulière
|
|
32
|
+
# par écrit.
|
|
33
|
+
#
|
|
34
|
+
# This file is part of the Ce fichier fait partie du projet
|
|
35
|
+
# OpenCADC project. OpenCADC.
|
|
36
|
+
#
|
|
37
|
+
# OpenCADC is free software: OpenCADC est un logiciel libre ;
|
|
38
|
+
# you can redistribute it and/or vous pouvez le redistribuer ou le
|
|
39
|
+
# modify it under the terms of modifier suivant les termes de
|
|
40
|
+
# the GNU Affero General Public la “GNU Affero General Public
|
|
41
|
+
# License as published by the License” telle que publiée
|
|
42
|
+
# Free Software Foundation, par la Free Software Foundation
|
|
43
|
+
# either version 3 of the : soit la version 3 de cette
|
|
44
|
+
# License, or (at your option) licence, soit (à votre gré)
|
|
45
|
+
# any later version. toute version ultérieure.
|
|
46
|
+
#
|
|
47
|
+
# OpenCADC is distributed in the OpenCADC est distribué
|
|
48
|
+
# hope that it will be useful, dans l’espoir qu’il vous
|
|
49
|
+
# but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE
|
|
50
|
+
# without even the implied GARANTIE : sans même la garantie
|
|
51
|
+
# warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ
|
|
52
|
+
# or FITNESS FOR A PARTICULAR ni d’ADÉQUATION À UN OBJECTIF
|
|
53
|
+
# PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence
|
|
54
|
+
# General Public License for Générale Publique GNU Affero pour
|
|
55
|
+
# more details. plus de détails.
|
|
56
|
+
#
|
|
57
|
+
# You should have received Vous devriez avoir reçu une
|
|
58
|
+
# a copy of the GNU Affero copie de la Licence Générale
|
|
59
|
+
# General Public License along Publique GNU Affero avec
|
|
60
|
+
# with OpenCADC. If not, see OpenCADC ; si ce n’est pas le cas,
|
|
61
|
+
# <http://www.gnu.org/licenses/>. consultez :
|
|
62
|
+
# <http://www.gnu.org/licenses/>.
|
|
63
|
+
#
|
|
64
|
+
# ***********************************************************************
|
|
65
|
+
|
|
66
|
+
"""Contract tests for fits2caom2 and caom2gen CLI parsers."""
|
|
67
|
+
|
|
68
|
+
import os
|
|
69
|
+
import sys
|
|
70
|
+
from unittest.mock import patch
|
|
71
|
+
|
|
72
|
+
import pytest
|
|
73
|
+
|
|
74
|
+
from caom2utils import caom2blueprint
|
|
75
|
+
from caom2utils.caom2blueprint import (
|
|
76
|
+
GLOBAL_STORAGE_RESOURCE_ID, build_caom2gen_parser, get_arg_parser,
|
|
77
|
+
)
|
|
78
|
+
from caom2utils.legacy import build_fits2caom2_parser
|
|
79
|
+
from cadcutils.util.tests.parser_helpers import (
|
|
80
|
+
assert_has_base_dests, assert_has_dests, assert_help_contains,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
THIS_DIR = os.path.dirname(os.path.realpath(__file__))
|
|
84
|
+
TESTDATA_DIR = os.path.join(THIS_DIR, 'data')
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class MyExitError(Exception):
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
_COMMON_DESTS = (
|
|
92
|
+
'dumpconfig', 'not_connected', 'no_validate', 'out_obs_xml',
|
|
93
|
+
'in_obs_xml', 'observation', 'local',
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_fits2caom2_base_parser_contract():
|
|
98
|
+
parser = get_arg_parser()
|
|
99
|
+
assert_has_base_dests(parser)
|
|
100
|
+
assert_has_dests(parser, *_COMMON_DESTS, 'productID', 'fileURI')
|
|
101
|
+
assert_help_contains(
|
|
102
|
+
parser,
|
|
103
|
+
GLOBAL_STORAGE_RESOURCE_ID,
|
|
104
|
+
'Augments an observation',
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_fits2caom2_legacy_parser_contract():
|
|
109
|
+
parser = build_fits2caom2_parser()
|
|
110
|
+
assert_has_base_dests(parser)
|
|
111
|
+
assert_has_dests(
|
|
112
|
+
parser, *_COMMON_DESTS, 'productID', 'fileURI',
|
|
113
|
+
'config', 'default', 'override',
|
|
114
|
+
)
|
|
115
|
+
assert_help_contains(parser, 'utype to keyword config')
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_caom2gen_parser_contract():
|
|
119
|
+
parser = build_caom2gen_parser()
|
|
120
|
+
assert_has_base_dests(parser)
|
|
121
|
+
assert_has_dests(
|
|
122
|
+
parser, *_COMMON_DESTS, 'external_url', 'module', 'plugin',
|
|
123
|
+
'lineage', 'use_blueprint_parser', 'blueprint',
|
|
124
|
+
)
|
|
125
|
+
assert_help_contains(
|
|
126
|
+
parser,
|
|
127
|
+
GLOBAL_STORAGE_RESOURCE_ID,
|
|
128
|
+
'productID/artifactURI',
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@patch('caom2utils.caom2blueprint.gen_proc', side_effect=RuntimeError('boom'))
|
|
133
|
+
@patch('sys.exit', side_effect=MyExitError)
|
|
134
|
+
def test_caom2gen_logs_debug_traceback_on_error(exit_mock, gen_mock):
|
|
135
|
+
blueprint = os.path.join(TESTDATA_DIR, 'si', 'si.blueprint')
|
|
136
|
+
sys.argv = (
|
|
137
|
+
'caom2gen --debug -o /tmp/out.xml --no_validate '
|
|
138
|
+
'--resource-id ivo://cadc.nrc.ca/test '
|
|
139
|
+
'--observation TEST_COLLECTION TEST_OBS_ID '
|
|
140
|
+
'--lineage test_product_id/cadc:TEST/test_file.fits '
|
|
141
|
+
'--blueprint {}'.format(blueprint)
|
|
142
|
+
).split()
|
|
143
|
+
with pytest.raises(MyExitError):
|
|
144
|
+
caom2blueprint.caom2gen()
|
|
145
|
+
gen_mock.assert_called_once()
|
|
146
|
+
exit_mock.assert_called_once_with(-1)
|
|
@@ -350,5 +350,8 @@ def _read_observation(fname):
|
|
|
350
350
|
|
|
351
351
|
|
|
352
352
|
def _write_observation(obs):
|
|
353
|
+
fd, path = tempfile.mkstemp(suffix='.xml', prefix='caom2utils_failed_obs_')
|
|
354
|
+
os.close(fd)
|
|
353
355
|
writer = ObservationWriter(True, False, 'caom2', 'http://www.opencadc.org/caom2/xml/v2.4')
|
|
354
|
-
writer.write(obs,
|
|
356
|
+
writer.write(obs, path)
|
|
357
|
+
return path
|
|
@@ -66,7 +66,6 @@
|
|
|
66
66
|
# ***********************************************************************
|
|
67
67
|
#
|
|
68
68
|
|
|
69
|
-
from pathlib import Path
|
|
70
69
|
|
|
71
70
|
from astropy.io import fits
|
|
72
71
|
from cadcdata import FileInfo
|
|
@@ -104,7 +103,7 @@ def test_get_file_type():
|
|
|
104
103
|
|
|
105
104
|
|
|
106
105
|
@patch('caom2utils.data_util.StorageInventoryClient')
|
|
107
|
-
def test_storage_inventory_client(cadc_client_mock):
|
|
106
|
+
def test_storage_inventory_client(cadc_client_mock, tmp_path):
|
|
108
107
|
test_subject = Mock(autospec=True)
|
|
109
108
|
test_uri = 'cadc:TEST/test_file.fits'
|
|
110
109
|
|
|
@@ -118,10 +117,11 @@ def test_storage_inventory_client(cadc_client_mock):
|
|
|
118
117
|
else:
|
|
119
118
|
test_fqn.write_text('StorageInventoryClient')
|
|
120
119
|
|
|
121
|
-
for
|
|
120
|
+
for subdir in (None, 'nested'):
|
|
121
|
+
test_working_directory = tmp_path / subdir if subdir else tmp_path
|
|
122
|
+
if subdir:
|
|
123
|
+
test_working_directory.mkdir()
|
|
122
124
|
test_fqn = test_working_directory / 'test_file.fits'
|
|
123
|
-
if test_fqn.exists():
|
|
124
|
-
test_fqn.unlink()
|
|
125
125
|
|
|
126
126
|
cadc_client_mock.return_value.cadcinfo.side_effect = info_si_mock
|
|
127
127
|
cadc_client_mock.return_value.cadcget.side_effect = get_si_mock
|
|
@@ -227,6 +227,27 @@ def test_unicode_decode_error():
|
|
|
227
227
|
assert result is not None, 'expect retry using a different method'
|
|
228
228
|
|
|
229
229
|
|
|
230
|
+
def test_get_local_headers_from_text_header_file():
|
|
231
|
+
"""Plain-text header files should be readable (issue #135)."""
|
|
232
|
+
test_fqn = join(
|
|
233
|
+
test_fits2caom2.TESTDATA_DIR,
|
|
234
|
+
'dao/dao_c122_2016_007830/dao_c122_2016_007830.fits.header',
|
|
235
|
+
)
|
|
236
|
+
result = data_util.get_local_headers_from_fits(test_fqn)
|
|
237
|
+
assert result is not None
|
|
238
|
+
assert len(result) == 1
|
|
239
|
+
assert result[0].get('OBJECT') == 'FLAT'
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def test_get_local_headers_from_multi_extension_text_header():
|
|
243
|
+
test_fqn = join(
|
|
244
|
+
test_fits2caom2.TESTDATA_DIR,
|
|
245
|
+
'gemini/N20250101M0624/N20250101M0624.fits.header',
|
|
246
|
+
)
|
|
247
|
+
result = data_util.get_local_headers_from_fits(test_fqn)
|
|
248
|
+
assert len(result) == 3
|
|
249
|
+
|
|
250
|
+
|
|
230
251
|
def test_get_file_encoding():
|
|
231
252
|
test_subjects = {'abc.fits': None, 'abc.fits.gz': 'gzip', 'abc.fits.fz': 'x-fits'}
|
|
232
253
|
for test_subject in test_subjects.keys():
|
|
@@ -93,6 +93,7 @@ from unittest.mock import Mock, patch
|
|
|
93
93
|
from io import StringIO, BytesIO
|
|
94
94
|
|
|
95
95
|
import importlib
|
|
96
|
+
import logging
|
|
96
97
|
import os
|
|
97
98
|
import sys
|
|
98
99
|
|
|
@@ -536,52 +537,30 @@ def _get_from_str_xml(string_xml, get_func, element_tag):
|
|
|
536
537
|
return act_obj
|
|
537
538
|
|
|
538
539
|
|
|
539
|
-
@patch('sys.exit', Mock(side_effect=[MyExitError, MyExitError, MyExitError, MyExitError
|
|
540
|
-
def
|
|
541
|
-
"""Tests
|
|
540
|
+
@patch('sys.exit', Mock(side_effect=[MyExitError, MyExitError, MyExitError, MyExitError]))
|
|
541
|
+
def test_cli_errors():
|
|
542
|
+
"""Tests CLI validation and error messages."""
|
|
542
543
|
|
|
543
|
-
# expected helper messages
|
|
544
544
|
with open(os.path.join(TESTDATA_DIR, 'bad_product_id.txt')) as myfile:
|
|
545
545
|
bad_product_id = myfile.read().strip()
|
|
546
546
|
with open(os.path.join(TESTDATA_DIR, 'missing_product_id.txt')) as myfile:
|
|
547
547
|
missing_product_id = myfile.read()
|
|
548
|
-
with open(os.path.join(TESTDATA_DIR, 'too_few_arguments_help.txt')) as myfile:
|
|
549
|
-
too_few_arguments_usage = myfile.read()
|
|
550
|
-
with open(os.path.join(TESTDATA_DIR, 'help.txt')) as myfile:
|
|
551
|
-
usage = myfile.read()
|
|
552
|
-
with open(os.path.join(TESTDATA_DIR, 'missing_observation_help.txt')) as myfile:
|
|
553
|
-
myfile.read()
|
|
554
|
-
with open(os.path.join(TESTDATA_DIR, 'missing_positional_argument_help.txt')) as myfile:
|
|
555
|
-
myfile.read()
|
|
556
|
-
|
|
557
|
-
# too few arguments error message when running python3
|
|
558
|
-
with patch('sys.stdout', new_callable=StringIO) as stdout_mock:
|
|
559
|
-
sys.argv = ["fits2caom2"]
|
|
560
|
-
with pytest.raises(MyExitError):
|
|
561
|
-
main_app()
|
|
562
|
-
if stdout_mock.getvalue():
|
|
563
|
-
assert too_few_arguments_usage == stdout_mock.getvalue()
|
|
564
548
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
sys.argv = ["fits2caom2", "-h"]
|
|
549
|
+
with patch('sys.stderr', new_callable=StringIO) as stderr_mock:
|
|
550
|
+
sys.argv = ["fits2caom2"]
|
|
568
551
|
with pytest.raises(MyExitError):
|
|
569
552
|
main_app()
|
|
570
|
-
|
|
571
|
-
assert usage.strip('\n') == expected
|
|
553
|
+
assert 'too few arguments' in stderr_mock.getvalue()
|
|
572
554
|
|
|
573
|
-
# missing productID when plane count is wrong
|
|
574
555
|
with patch('sys.stderr', new_callable=StringIO) as stderr_mock:
|
|
575
556
|
with patch('sys.stdout', new_callable=StringIO) as stdout_mock:
|
|
576
557
|
bad_product_file = os.path.join(TESTDATA_DIR, 'bad_product_id.xml')
|
|
577
558
|
sys.argv = ["fits2caom2", "--in", bad_product_file, "ad:CGPS/CGPS_MA1_HI_line_image.fits"]
|
|
578
559
|
with pytest.raises(MyExitError):
|
|
579
560
|
main_app()
|
|
580
|
-
# inconsistencies between Python 3.7 and later versions. this should be on stderr_mmock only
|
|
581
561
|
result = stderr_mock.getvalue() + stdout_mock.getvalue()
|
|
582
562
|
assert bad_product_id in result, result
|
|
583
563
|
|
|
584
|
-
# missing productID when blueprint doesn't have one either
|
|
585
564
|
with patch('sys.stdout', new_callable=StringIO) as stdout_mock:
|
|
586
565
|
with patch('sys.stderr', new_callable=StringIO) as stderr_mock, patch(
|
|
587
566
|
'caom2utils.data_util.StorageClientWrapper'
|
|
@@ -597,27 +576,22 @@ def test_help():
|
|
|
597
576
|
]
|
|
598
577
|
with pytest.raises(MyExitError):
|
|
599
578
|
main_app()
|
|
600
|
-
# inconsistencies between Python 3.7 and later versions. this should be on stderr_mmock only
|
|
601
579
|
result = stderr_mock.getvalue() + stdout_mock.getvalue()
|
|
602
580
|
assert missing_product_id.strip() in result, result
|
|
603
581
|
|
|
604
|
-
# missing required --observation
|
|
605
|
-
"""
|
|
606
|
-
TODO: fix the tests
|
|
607
|
-
with patch('sys.stderr', new_callable=StringIO) as stdout_mock:
|
|
608
|
-
sys.argv = ["fits2caom2", "testProductID", "testpathto/testFileURI"]
|
|
609
|
-
with pytest.raises(MyExitError):
|
|
610
|
-
main_app()
|
|
611
|
-
assert(missing_observation_usage == stdout_mock.getvalue())
|
|
612
582
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
583
|
+
@patch('caom2utils.caom2blueprint.proc', side_effect=RuntimeError('fail'))
|
|
584
|
+
@patch('sys.exit', side_effect=MyExitError)
|
|
585
|
+
def test_main_app_logs_debug_traceback_on_error(exit_mock, proc_mock):
|
|
586
|
+
logging.getLogger().setLevel(logging.DEBUG)
|
|
587
|
+
sys.argv = [
|
|
588
|
+
"fits2caom2", "--debug", "--observation", "cfht", "7000000o",
|
|
589
|
+
"ad:CGPS/CGPS_MA1_HI_line_image.fits",
|
|
590
|
+
]
|
|
591
|
+
with pytest.raises(MyExitError):
|
|
592
|
+
main_app()
|
|
593
|
+
proc_mock.assert_called_once()
|
|
594
|
+
exit_mock.assert_called_once_with(-1)
|
|
621
595
|
|
|
622
596
|
|
|
623
597
|
EXPECTED_OBS_XML = (
|
|
@@ -1073,7 +1047,7 @@ EXPECTED_GENERIC_PARSER_FILE_SCHEME_XML = (
|
|
|
1073
1047
|
<caom2:productType>thumbnail</caom2:productType>
|
|
1074
1048
|
<caom2:releaseType>data</caom2:releaseType>
|
|
1075
1049
|
<caom2:contentType>text/plain</caom2:contentType>
|
|
1076
|
-
<caom2:contentLength>
|
|
1050
|
+
<caom2:contentLength>2573</caom2:contentLength>
|
|
1077
1051
|
<caom2:contentChecksum>md5:e6c08f3b8309f05a5a3330e27e3b44eb</caom2:contentChecksum>
|
|
1078
1052
|
<caom2:uri>file://"""
|
|
1079
1053
|
+ text_file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
version = '1.7.4'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: caom2utils
|
|
3
|
-
Version: 1.7.
|
|
3
|
+
Version: 1.7.4
|
|
4
4
|
Summary: CAOM-2.4 utils
|
|
5
5
|
Home-page: https://www.cadc-ccda.hia-iha.nrc-cnrc.gc.ca/caom2
|
|
6
6
|
Author: Canadian Astronomy Data Centre
|
|
@@ -10,10 +10,15 @@ Classifier: Natural Language :: English
|
|
|
10
10
|
Classifier: License :: OSI Approved :: GNU Affero General Public License v3
|
|
11
11
|
Classifier: Programming Language :: Python
|
|
12
12
|
Classifier: Programming Language :: Python :: 3
|
|
13
|
-
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Requires-Python: >=3.10, <3.15
|
|
14
19
|
License-File: LICENSE
|
|
15
20
|
Requires-Dist: cadcdata>=2.5.2
|
|
16
|
-
Requires-Dist: caom2
|
|
21
|
+
Requires-Dist: caom2<3,>=2.6
|
|
17
22
|
Requires-Dist: astropy>=2.0
|
|
18
23
|
Requires-Dist: spherical-geometry>=1.2.11
|
|
19
24
|
Requires-Dist: vos>=3.1.1
|
|
@@ -25,6 +25,7 @@ caom2utils.egg-info/top_level.txt
|
|
|
25
25
|
caom2utils/tests/__init__.py
|
|
26
26
|
caom2utils/tests/conftest.py
|
|
27
27
|
caom2utils/tests/test_caomvalidator.py
|
|
28
|
+
caom2utils/tests/test_cli_parsers.py
|
|
28
29
|
caom2utils/tests/test_collections.py
|
|
29
30
|
caom2utils/tests/test_convert_from_java.py
|
|
30
31
|
caom2utils/tests/test_custom_axis_util.py
|
|
@@ -44,13 +45,10 @@ caom2utils/tests/data/edge_case.blueprint
|
|
|
44
45
|
caom2utils/tests/data/fits2caom2.config
|
|
45
46
|
caom2utils/tests/data/help.txt
|
|
46
47
|
caom2utils/tests/data/java.config
|
|
47
|
-
caom2utils/tests/data/missing_observation_help.txt
|
|
48
|
-
caom2utils/tests/data/missing_positional_argument_help.txt
|
|
49
48
|
caom2utils/tests/data/missing_product_id.txt
|
|
50
49
|
caom2utils/tests/data/nonconformant.py
|
|
51
50
|
caom2utils/tests/data/test.override
|
|
52
51
|
caom2utils/tests/data/test_plugin.py
|
|
53
52
|
caom2utils/tests/data/test_plugin_class.py
|
|
54
53
|
caom2utils/tests/data/text.override
|
|
55
|
-
caom2utils/tests/data/time_axes.fits
|
|
56
|
-
caom2utils/tests/data/too_few_arguments_help.txt
|
|
54
|
+
caom2utils/tests/data/time_axes.fits
|
|
@@ -23,6 +23,9 @@ norecursedirs = build docs/_build
|
|
|
23
23
|
doctest_plus = enabled
|
|
24
24
|
testpaths = caom2utils
|
|
25
25
|
|
|
26
|
+
[flake8]
|
|
27
|
+
max-line-length = 120
|
|
28
|
+
|
|
26
29
|
[metadata]
|
|
27
30
|
package_name = caom2utils
|
|
28
31
|
description = CAOM-2.4 utils
|
|
@@ -32,12 +35,12 @@ license = AGPLv3
|
|
|
32
35
|
url = https://www.cadc-ccda.hia-iha.nrc-cnrc.gc.ca/caom2
|
|
33
36
|
edit_on_github = False
|
|
34
37
|
github_project = opencadc/caom2tools
|
|
35
|
-
version = 1.7.
|
|
38
|
+
version = 1.7.4
|
|
36
39
|
|
|
37
40
|
[options]
|
|
38
41
|
install_requires =
|
|
39
42
|
cadcdata>=2.5.2
|
|
40
|
-
caom2>=2.6
|
|
43
|
+
caom2>=2.6,<3
|
|
41
44
|
astropy>=2.0
|
|
42
45
|
spherical-geometry>=1.2.11
|
|
43
46
|
vos>=3.1.1
|
|
@@ -51,9 +54,6 @@ test =
|
|
|
51
54
|
flake8>=3.9
|
|
52
55
|
xml-compare>=1.0.5
|
|
53
56
|
|
|
54
|
-
[flake8]
|
|
55
|
-
max-line-length = 120
|
|
56
|
-
|
|
57
57
|
[entry_points]
|
|
58
58
|
fits2caom2 = caom2utils.legacy:main_app
|
|
59
59
|
caom2gen = caom2utils.caom2blueprint:caom2gen
|
|
@@ -106,7 +106,7 @@ setup(name=PACKAGENAME,
|
|
|
106
106
|
use_2to3=False,
|
|
107
107
|
setup_requires=['pytest-runner'],
|
|
108
108
|
entry_points=entry_points,
|
|
109
|
-
python_requires='>=3.
|
|
109
|
+
python_requires='>=3.10, <3.15',
|
|
110
110
|
packages=find_packages(),
|
|
111
111
|
package_data={PACKAGENAME: ['data/*', 'tests/data/*', '*/data/*', '*/tests/data/*']},
|
|
112
112
|
classifiers=[
|
|
@@ -114,6 +114,11 @@ setup(name=PACKAGENAME,
|
|
|
114
114
|
'License :: OSI Approved :: GNU Affero General Public License v3',
|
|
115
115
|
'Programming Language :: Python',
|
|
116
116
|
'Programming Language :: Python :: 3',
|
|
117
|
+
'Programming Language :: Python :: 3.10',
|
|
118
|
+
'Programming Language :: Python :: 3.11',
|
|
119
|
+
'Programming Language :: Python :: 3.12',
|
|
120
|
+
'Programming Language :: Python :: 3.13',
|
|
121
|
+
'Programming Language :: Python :: 3.14',
|
|
117
122
|
],
|
|
118
123
|
cmdclass={
|
|
119
124
|
'coverage': PyTest,
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
usage: fits2caom2 [-h] [-V] [-d | -q | -v] [--dumpconfig] [--ignorePartialWCS]
|
|
2
|
-
[-o OUT_OBS_XML]
|
|
3
|
-
(-i IN_OBS_XML | --observation collection observationID)
|
|
4
|
-
[--config CONFIG] [--default DEFAULT] [--override OVERRIDE]
|
|
5
|
-
[--local LOCAL [LOCAL ...]] [--log LOG] [--keep] [--test]
|
|
6
|
-
[--cert CERT]
|
|
7
|
-
productID fileURI [fileURI ...]
|
|
8
|
-
fits2caom2: error: one of the arguments -i/--in --observation is required
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
usage: fits2caom2 [-h] [-V] [-d | -q | -v] [--dumpconfig] [--ignorePartialWCS]
|
|
2
|
-
[-o OUT_OBS_XML]
|
|
3
|
-
(-i IN_OBS_XML | --observation collection observationID)
|
|
4
|
-
[--config CONFIG] [--default DEFAULT] [--override OVERRIDE]
|
|
5
|
-
[--local LOCAL [LOCAL ...]] [--log LOG] [--keep] [--test]
|
|
6
|
-
[--cert CERT]
|
|
7
|
-
productID fileURI [fileURI ...]
|
|
8
|
-
fits2caom2: error: too few arguments
|
|
9
|
-
|
|
10
|
-
|
|
File without changes
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
version = '1.7.3'
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|