diff --git a/CHANGES.rst b/CHANGES.rst index 97f1d1fb0b..f18c727809 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -124,6 +124,11 @@ gaia - Added ``get_query_payload`` kwarg to return the ADQL query string. [#3539] +esa.euclid +^^^^^^^^^^ + +- New method get_sia to access the Simple Image Access Protocol (SIAP) v2.0 [#3569] + esa.hubble ^^^^^^^^^^ diff --git a/astroquery/esa/euclid/core.py b/astroquery/esa/euclid/core.py index 0d9ef7bb7f..ceac2d4a43 100644 --- a/astroquery/esa/euclid/core.py +++ b/astroquery/esa/euclid/core.py @@ -23,6 +23,8 @@ from astropy.table import unique from astropy.units import Quantity from astropy.utils import deprecated_renamed_argument +import astroquery.esa.utils.utils as esautils + from requests.exceptions import HTTPError from astroquery import log @@ -52,7 +54,7 @@ class EuclidClass(TapPlus): __regex_designation = re.compile(r"\s*(\S+)\s(-?\d+)\s*", flags=re.MULTILINE | re.UNICODE) def __init__(self, *, environment='PDR', tap_plus_conn_handler=None, datalink_handler=None, cutout_handler=None, - verbose=False, show_server_messages=True): + sia_handler=None, verbose=False, show_server_messages=True): """Constructor for EuclidClass. Parameters @@ -60,11 +62,13 @@ def __init__(self, *, environment='PDR', tap_plus_conn_handler=None, datalink_ha environment : str, mandatory if no tap, data or cutout hosts is specified, default 'PDR' The Euclid Science Archive environment: 'PDR', 'IDR', 'OTF' and 'REG' tap_plus_conn_handler : tap connection handler object, optional, default None - HTTP(s) connection hander (creator). If no handler is provided, a new one is created. - datalink_handler : dataliink connection handler object, optional, default None - HTTP(s) connection hander (creator). If no handler is provided, a new one is created. + HTTP(s) connection handler (creator). If no handler is provided, a new one is created. + datalink_handler : datalink connection handler object, optional, default None + HTTP(s) connection handler (creator). If no handler is provided, a new one is created. cutout_handler : cutout connection handler object, optional, default None - HTTP(s) connection hander (creator). If no handler is provided, a new one is created. + HTTP(s) connection handler (creator). If no handler is provided, a new one is created. + sia_handler : siap connection handler object, optional, default None + HTTP(s) connection handler (creator). If no handler is provided, a new one is created. verbose : bool, optional, default 'True' flag to display information about the process show_server_messages : bool, optional, default 'True' @@ -125,6 +129,20 @@ def __init__(self, *, environment='PDR', tap_plus_conn_handler=None, datalink_ha else: self.__euclidcutout = cutout_handler + if sia_handler is None: + self.__euclidsia = TapPlus(url=url_server, + server_context="sas-sia", + tap_context="tap-server", + upload_context="Upload", + table_edit_context="TableTool", + data_context="sia2/query", + datalink_context="datalink", + verbose=verbose, + client_id='ASTROQUERY', + use_names_over_ids=conf.USE_NAMES_OVER_IDS) + else: + self.__euclidsia = sia_handler + if show_server_messages: self.get_status_messages() @@ -667,6 +685,8 @@ def login(self, *, user=None, password=None, credentials_file=None, verbose=Fals self.__eucliddata.login(user=tap_user, password=tap_password, verbose=verbose) log.info(f"Login to Euclid cutout service: {self.__euclidcutout._TapPlus__getconnhandler().get_host_url()}") self.__euclidcutout.login(user=tap_user, password=tap_password, verbose=verbose) + log.info(f"Login to Euclid sia service: {self.__euclidsia._TapPlus__getconnhandler().get_host_url()}") + self.__euclidsia.login(user=tap_user, password=tap_password, verbose=verbose) except HTTPError as err: log.error('Error logging in data or cutout services: %s' % (str(err))) log.error("Logging out from TAP server") @@ -711,6 +731,14 @@ def login_gui(self, verbose=False): log.error("Logging out from TAP server") TapPlus.logout(self, verbose=verbose) + try: + log.info(f"Login to Euclid sia server: {self.__euclidsia._TapPlus__getconnhandler().get_host_url()}") + self.__euclidsia.login(user=tap_user, password=tap_password, verbose=verbose) + except HTTPError as err: + log.error('Error logging in sia server: %s' % (str(err))) + log.error("Logging out from TAP server") + TapPlus.logout(self, verbose=verbose) + def logout(self, verbose=False): """ Performs a logout @@ -746,6 +774,12 @@ def logout(self, verbose=False): except HTTPError as err: log.error('Error logging out cutout server: %s' % (str(err))) + try: + self.__euclidsia.logout(verbose=verbose) + log.info("Euclid sia server logout OK") + except HTTPError as err: + log.error('Error logging out sia server: %s' % (str(err))) + @staticmethod def __get_quantity_input(value, msg): if value is None: @@ -1087,7 +1121,7 @@ def get_product_list(self, *, observation_id=None, tile_index=None, product_type observation id for observations. It is not compatible with parameter tile_index. Searchable products by observation_id: 'dpdVisRawFrame', 'dpdNispRawFrame', - ,'DpdVisCalibratedQuadFrame','DpdVisCalibratedFrameCatalog', 'DpdVisStackedFrame', + 'DpdVisCalibratedQuadFrame','DpdVisCalibratedFrameCatalog', 'DpdVisStackedFrame', 'DpdVisStackedFrameCatalog', 'DpdNirCalibratedFrame', 'DpdNirCalibratedFrameCatalog', 'DpdNirStackedFrameCatalog', 'DpdNirStackedFrame', 'DpdMerSegmentationMap', 'dpdMerFinalCatalog', @@ -1482,6 +1516,115 @@ def __is_multiple(self, value): return not isinstance(value, int) and ((isinstance(value, (list, tuple)) and len(value) > 1) or ',' in value) + def get_sia(self, *, coordinates, radius=1.0, search_type='CIRCLE', calibration=2, instrument='ALL', band=None, + collection='sedm', dsr_part1=None, dsr_part2=None, dsr_part3=None, output_file=None, verbose=False): + """ + Query the Euclid Observation Images service through the IVOA SIAP 2.0 interface. + + The service provides access to public calibrated and stacked VIS and NISP images, MER mosaics, and Level 1 (raw) + VIS and NISP observations. + + Parameters + ---------- + coordinates: str or SkyCoord, mandatory, default None + Center of the search region. + radius: float or quantity, optional, default value 1.0 degree + Radius of the search region. If a numeric value is provided, it is interpreted in degrees. An + astropy.units.Quantity may also be supplied. If "BOX" uses this value as width + search_type : str, optional, default CIRCLE + Shape of the search region. Supported values are "CIRCLE" and "BOX". + calibration: int, optional, default 2 + Calibration level following the ObsCore data model: + - 1: raw instrumental data + - 2: instrumental data in a standard format + - : science-ready data + - 4: enhanced data products + instrument: str, optional, default ALL + Instrument to query. Supported values are "ALL", "VIS", and "NISP". + band: str, optional, default None + Filter name. This parameter is ignored when instrument="ALL". Valid values are: + - 1: "VIS" for the VIS instrument + - 2: "NIR_Y", "NIR_J", "NIR_H", or "NISP" for the NISP instrument + collection : str, optional, default sedm + Name of the data collection. + dsr_part1: str, optional, default None + First component of the dataset release identifier: for OTF environment, the activity code; for REG and IDR, + the target environment + dsr_part2: str, optional, default None + Second component of the dataset release identifier: for OTF environment, the patch id (a positive integer); + for REG and IDR, the activity code + dsr_part3: str, optional, default None + Third component of the dataset release identifier: for OTF, REG and IDR environment, the version (an integer + greater than 1) output_file : string, optional, default None + Output file where the query results are written. + verbose : bool, optional, default 'False' + flag to display information about the process + + Returns + ------- + astropy.table.Table or str + Query results. If ``output_file`` is specified, the results are also written as a VOTable. + """ + + valid_search_types = {'CIRCLE', 'BOX'} + valid_calibrations = {0: 'CALIB_ZERO', 1: 'CALIB_ONE', 2: 'CALIB_TWO', 3: 'CALIB_THREE'} + valid_instruments = {'ALL', 'VIS', 'NISP'} + valid_band_vis = {'VIS'} + valid_band_nisp = {'NIR_H', 'NIR_J', 'NIR_Y', 'NISP'} + + if search_type not in valid_search_types: + raise ValueError(f"Invalid search tyype {search_type}") + + if calibration is not None and calibration not in valid_calibrations: + raise ValueError(f"Invalid calibration {calibration}") + + if instrument not in valid_instruments: + raise ValueError(f"Invalid instrument {instrument}") + + if instrument == 'ALL' and band is not None: + raise ValueError(f"For instrument {instrument} band must be None") + + if instrument == 'VIS' and band is not None and band not in valid_band_vis: + raise ValueError(f"Invalid band {band} for instrument {instrument}") + + if instrument == 'NISP' and band is not None and band not in valid_band_nisp: + raise ValueError(f"Invalid band {band} for instrument {instrument}") + + if radius is not None: + coord = commons.parse_coordinates(coordinates=coordinates) + ra_deg = coord.ra.degree + dec_deg = coord.dec.degree + radius_deg = esautils.get_degree_radius(radius) + + if radius_deg <= 0.0: + raise ValueError(f"Search radius is zero or negative: {radius}") + else: + raise ValueError(f"Invalid coordinates or search radius: {coordinates}, {radius}") + + params_dict = dict() + params_dict['TAPCLIENT'] = 'ASTROQUERY' + params_dict['POS'] = f"{search_type},{ra_deg}, {dec_deg},{radius_deg}" + params_dict['INSTRUMENT'] = instrument + params_dict['COLLECTION'] = collection + + if calibration is not None: + params_dict['CALIB'] = valid_calibrations[calibration] + + if instrument != 'ALL' and band is not None: + params_dict['BAND'] = band + + if dsr_part1 is not None: + params_dict['DSP1'] = dsr_part1 + + if dsr_part2 is not None: + params_dict['DSP2'] = dsr_part2 + + if dsr_part3 is not None: + params_dict['DSP3'] = dsr_part3 + + return self.__euclidsia.load_data(params_dict=params_dict, output_file=output_file, http_method='GET', + verbose=verbose) + @deprecated_renamed_argument(('instrument', 'id'), (None, None), since='0.4.12') def get_cutout(self, *, file_path=None, coordinate, radius, output_file=None, verbose=False, instrument=None, id=None): diff --git a/astroquery/esa/euclid/tests/data/sia_test.vot b/astroquery/esa/euclid/tests/data/sia_test.vot new file mode 100644 index 0000000000..fab4a4d902 --- /dev/null +++ b/astroquery/esa/euclid/tests/data/sia_test.vot @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
EUC_MER_BGSUB-MOSAIC-VIS_TILE101007315-D84386_20230826T000856.482420Z_00.00.fits.gzNISPNIR_J{(1.56878970716861 , -1.15654637163971),(1.54566457897093 , -1.15654636650839),(1.54590379663453 , -1.14723910521964),(1.568550514417 , -1.14723911022404)}89.2225775443200035-66nullsedmmosaicDpdMerBksMosaic3
EUC_NIR_W-CAL-IMAGE_H-65602-17_20230906T190444.924367Z.fitsNISPNIR_H{(1.59364461496529 , -1.16117316244116),(1.5877646080787 , -1.16334176247736),(1.58179954392353 , -1.16549550886951),(1.5757606932609 , -1.16763184447562),(1.57013973108981 , -1.1695759346524),(1.5643381526667 , -1.16701623505239),(1.5587172508289 , -1.1645004743295),(1.55386389053321 , -1.16228607475105),(1.55304135179527 , -1.16190988595972),(1.54825944937331 , -1.15969489664624),(1.57153509266763 , -1.15142605710669)}90.0032000000000068-66.49850000000000765602sedmimageDpdNirCalibratedFrame2
+
+
diff --git a/astroquery/esa/euclid/tests/test_euclidtap.py b/astroquery/esa/euclid/tests/test_euclidtap.py index d6ec4c350b..4295e9fb6c 100644 --- a/astroquery/esa/euclid/tests/test_euclidtap.py +++ b/astroquery/esa/euclid/tests/test_euclidtap.py @@ -44,6 +44,8 @@ TABLE_FILE_NAME = get_pkg_data_filename(os.path.join("data", '1714556098855O-result.vot'), package=package) TABLE_DATA = Path(TABLE_FILE_NAME).read_text() +TABLE_SIA_FILE_NAME = get_pkg_data_filename(os.path.join("data", 'sia_test.vot'), package=package) + RADIUS = 1 * u.deg SKYCOORD = SkyCoord(ra=19 * u.deg, dec=20 * u.deg, frame="icrs") @@ -124,6 +126,8 @@ def mock_querier_async(): conn_handler = DummyConnHandler() tapplus = TapPlus(url="http://test:1111/tap", connhandler=conn_handler) cutout_handler = TapPlus(url="http://test:1111/tap", connhandler=conn_handler) + sia_handler = TapPlus(url="http://test:1111/tap", connhandler=conn_handler) + jobid = "12345" launch_response = DummyResponse(303) @@ -152,7 +156,7 @@ def mock_querier_async(): conn_handler.set_response("async/1479386030738O/results/result", results_response) return EuclidClass(tap_plus_conn_handler=conn_handler, datalink_handler=tapplus, cutout_handler=cutout_handler, - show_server_messages=False) + sia_handler=sia_handler, show_server_messages=False) @pytest.fixture(scope="module") @@ -191,7 +195,7 @@ def test_load_environments(): environment = 'WRONG' try: - tap = EuclidClass(environment='WRONG') + EuclidClass(environment='WRONG') except Exception as e: assert str(e).startswith(f"Invalid environment {environment}. Valid values: {list(conf.ENVIRONMENTS.keys())}") @@ -1107,6 +1111,8 @@ def test_get_observation_products(tmp_path_factory): assert result is not None + remove_temp_dir() + result = tap.get_observation_products(id='13', product_type='mosaic', filter='VIS', output_file=None) assert result is not None @@ -1125,6 +1131,8 @@ def test_get_observation_products(tmp_path_factory): assert result is not None + remove_temp_dir() + def test_get_observation_products_data_set_release(tmp_path_factory): conn_handler = DummyConnHandler() @@ -1143,6 +1151,8 @@ def test_get_observation_products_data_set_release(tmp_path_factory): assert result is not None + remove_temp_dir() + result = tap.get_observation_products(id='13', product_type='mosaic', filter='VIS', output_file=None, dsr_part1='CALBLOCK', dsr_part2='PV-023', dsr_part3=1) @@ -1163,6 +1173,8 @@ def test_get_observation_products_data_set_release(tmp_path_factory): assert result is not None + remove_temp_dir() + def test_get_observation_products_exceptions(): conn_handler = DummyConnHandler() @@ -1722,10 +1734,10 @@ def test_login(mock_login): tapplus = TapPlus(url="https://test:1111/tap", connhandler=conn_handler) tap = EuclidClass(tap_plus_conn_handler=conn_handler, datalink_handler=tapplus, show_server_messages=False) tap.login(user="user", password="password") - assert (mock_login.call_count == 3) + assert (mock_login.call_count == 4) mock_login.side_effect = HTTPError("Login error") tap.login(user="user", password="password") - assert (mock_login.call_count == 4) + assert (mock_login.call_count == 5) @patch.object(TapPlus, 'login_gui') @@ -1735,7 +1747,7 @@ def test_login_gui(mock_login_gui, mock_login): tapplus = TapPlus(url="http://test:1111/tap", connhandler=conn_handler) tap = EuclidClass(tap_plus_conn_handler=conn_handler, datalink_handler=tapplus, show_server_messages=False) tap.login_gui() - assert (mock_login_gui.call_count == 2) + assert (mock_login_gui.call_count == 3) mock_login_gui.side_effect = HTTPError("Login error") tap.login(user="user", password="password") assert (mock_login.call_count == 1) @@ -1747,10 +1759,10 @@ def test_logout(mock_logout): tapplus = TapPlus(url="http://test:1111/tap", connhandler=conn_handler) tap = EuclidClass(tap_plus_conn_handler=conn_handler, datalink_handler=tapplus, show_server_messages=False) tap.logout() - assert (mock_logout.call_count == 3) + assert (mock_logout.call_count == 4) mock_logout.side_effect = HTTPError("Login error") tap.logout() - assert (mock_logout.call_count == 4) + assert (mock_logout.call_count == 5) def test_get_datalinks(monkeypatch): @@ -1986,6 +1998,95 @@ def test_load_async_job(mock_querier_async): assert job.jobid == jobid +@pytest.mark.parametrize("verbose", [False, True]) +def test_get_sia(monkeypatch, verbose): + def load_data_monkeypatch(self, params_dict, output_file, http_method, verbose): + return Table.read(TABLE_SIA_FILE_NAME, format='votable') + + monkeypatch.setattr(TapPlus, "load_data", load_data_monkeypatch) + euclid = EuclidClass(show_server_messages=False) + + coords = SkyCoord(267.78, 65.53, frame="icrs", unit="deg") # NGC 6505 + + table = euclid.get_sia(coordinates=coords, radius=1.0, verbose=verbose) + assert isinstance(table, Table) + fn = 'file_name' + assert table[fn][0] == 'EUC_MER_BGSUB-MOSAIC-VIS_TILE101007315-D84386_20230826T000856.482420Z_00.00.fits.gz' + + table = euclid.get_sia(coordinates=coords, radius=1.0, dsr_part1='CALBLOCK', dsr_part2='PV-023', dsr_part3=1, + verbose=verbose) + assert isinstance(table, Table) + assert table[fn][0] == 'EUC_MER_BGSUB-MOSAIC-VIS_TILE101007315-D84386_20230826T000856.482420Z_00.00.fits.gz' + + # The coordinate is a string + + coords = "81.1238 17.4175" + + table = euclid.get_sia(coordinates=coords, radius=1.0, verbose=verbose) + assert isinstance(table, Table) + fn = 'file_name' + assert table[fn][0] == 'EUC_MER_BGSUB-MOSAIC-VIS_TILE101007315-D84386_20230826T000856.482420Z_00.00.fits.gz' + + table = euclid.get_sia(coordinates=coords, radius=1.0, dsr_part1='CALBLOCK', dsr_part2='PV-023', dsr_part3=1, + verbose=verbose) + assert isinstance(table, Table) + assert table[fn][0] == 'EUC_MER_BGSUB-MOSAIC-VIS_TILE101007315-D84386_20230826T000856.482420Z_00.00.fits.gz' + + # the search redius is a Quantity + + table = euclid.get_sia(coordinates=coords, radius=1.0, verbose=verbose) + assert isinstance(table, Table) + fn = 'file_name' + assert table[fn][0] == 'EUC_MER_BGSUB-MOSAIC-VIS_TILE101007315-D84386_20230826T000856.482420Z_00.00.fits.gz' + + table = euclid.get_sia(coordinates=coords, radius=u.Quantity(0.01, u.deg), dsr_part1='CALBLOCK', dsr_part2='PV-023', + dsr_part3=1, verbose=verbose) + assert isinstance(table, Table) + assert table[fn][0] == 'EUC_MER_BGSUB-MOSAIC-VIS_TILE101007315-D84386_20230826T000856.482420Z_00.00.fits.gz' + + +def test_get_sia_exceptions(monkeypatch): + def load_data_monkeypatch(self, params_dict, output_file, http_method, verbose): + return Table() + + monkeypatch.setattr(TapPlus, "load_data", load_data_monkeypatch) + euclid = EuclidClass(show_server_messages=False) + + coords = SkyCoord(267.78, 65.53, frame="icrs", unit="deg") # NGC 6505 + + error_message = "Invalid search tyype XX" + with pytest.raises(ValueError, match=error_message): + euclid.get_sia(search_type='XX', coordinates=coords, verbose=True) + + error_message = "Invalid instrument XX" + with pytest.raises(ValueError, match=error_message): + euclid.get_sia(instrument='XX', coordinates=coords, verbose=True) + + error_message = "For instrument ALL band must be None" + with pytest.raises(ValueError, match=error_message): + euclid.get_sia(instrument='ALL', band='XX', coordinates=coords, verbose=True) + + error_message = "Invalid band NIR_H for instrument VIS" + with pytest.raises(ValueError, match=error_message): + euclid.get_sia(instrument='VIS', band='NIR_H', coordinates=coords, verbose=True) + + error_message = "Invalid band VIS for instrument NISP" + with pytest.raises(ValueError, match=error_message): + euclid.get_sia(instrument='NISP', band='VIS', coordinates=coords, verbose=True) + + error_message = "Invalid calibration 5" + with pytest.raises(ValueError, match=error_message): + euclid.get_sia(calibration=5, coordinates=coords, verbose=True) + + error_message = "Search radius is zero or negative: -1.0" + with pytest.raises(ValueError, match=error_message): + euclid.get_sia(coordinates=coords, radius=-1.0, verbose=True) + + error_message = "Search radius is zero or negative: 0.0" + with pytest.raises(ValueError, match=error_message): + euclid.get_sia(coordinates=coords, radius=0.0, verbose=True) + + def remove_temp_dir(): dirs = glob.glob('./temp_*') for dir_path in dirs: diff --git a/astroquery/utils/tap/core.py b/astroquery/utils/tap/core.py index 3f9fe6f933..f113a94950 100755 --- a/astroquery/utils/tap/core.py +++ b/astroquery/utils/tap/core.py @@ -853,7 +853,7 @@ def load_tables(self, *, only_names=False, include_shared_tables=False, verbose= return self._Tap__load_tables(only_names=only_names, include_shared_tables=include_shared_tables, # noqa verbose=verbose) - def load_data(self, *, params_dict=None, output_file=None, verbose=False): + def load_data(self, *, params_dict=None, output_file=None, http_method='POST', verbose=False): """Loads the specified data Parameters @@ -863,6 +863,8 @@ def load_data(self, *, params_dict=None, output_file=None, verbose=False): output_file : string, optional, default None file where the results are saved. If it is not provided, the http response contents are returned. + http_method: string, optional, default POST + HTTP Request Method: POST or GET verbose : bool, optional, default 'False' flag to display information about the process @@ -879,12 +881,22 @@ def load_data(self, *, params_dict=None, output_file=None, verbose=False): data = urlencode(params_dict) if verbose: print(f"Data request: {data}") - response = connHandler.execute_datapost(data=data, verbose=verbose) + + if http_method == 'POST': + response = connHandler.execute_datapost(data=data, verbose=verbose) + elif http_method == 'GET': + response = connHandler.execute_dataget(query=data, verbose=verbose) + else: + raise ValueError(f"Invalid http_method, only GET or POST are allowed: {http_method}") + if verbose: print(response.status, response.reason) + connHandler.check_launch_response_status(response, verbose, 200) + if verbose: print("Reading...") + chunk = True if output_file is not None: with open(output_file, 'wb') as file: @@ -903,9 +915,12 @@ def load_data(self, *, params_dict=None, output_file=None, verbose=False): output_format = params_dict['FORMAT'].lower() else: output_format = "votable" + results = utils.read_http_response(response, output_format, use_names_over_ids=self.use_names_over_ids) + if verbose: print("Done.") + return results def load_groups(self, *, verbose=False): diff --git a/docs/esa/euclid/euclid.rst b/docs/esa/euclid/euclid.rst index cad80ee9c4..8261926a9c 100644 --- a/docs/esa/euclid/euclid.rst +++ b/docs/esa/euclid/euclid.rst @@ -356,6 +356,67 @@ and their sky coverage (in its "fov" field) is queried using ADQL_. Please note: :height: 500px :scale: 100% +The Euclid Archive provides the VO Simple Image Access Protocol (SIAP) v2.0 service for accessing Euclid observation images. This service returns public images from: + +1. Calibrated and stacked NISP and VIS images. +2. MER mosaics from VIS and NISP. + +The service returns a VOTable containing all the information required by the cutout function, including the path to the FITS files (file_name), the instrument, and the observation identifier (obs_id). + +The VOTable can also be saved for later use. + + >>> from astroquery.esa.euclid import Euclid + >>> from astropy.coordinates import SkyCoord + >>> import astropy.units as u + >>> coords = SkyCoord(267.78, 65.53, frame='icrs', unit="deg") # NGC 6505 + >>> radius = u.Quantity(0.01, u.deg) + >>> table = Euclid.get_sia(coordinates=coords, radius=radius, calibration=3) + >>> print(table.info) + + name dtype + ------------------- ------ + cutout_access_url object + facility_name object + file_name object + instrument_name object + filter object + s_region object + s_ra object + s_dec object + tile_index object + obs_collection object + dataproduct_type object + dataproduct_subtype object + calib_level object + + >>> print(table) + cutout_access_url ... + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ... + https://eas.esac.esa.int/sas-cutout/cutout?filepath=/euclid/repository_idr/iqr1/Q1_R1/MER/102158889/GPC/EUC_MER_BGSUB-MOSAIC-PANSTARRS-I_TILE102158889-5BC663_20241024T203110.498476Z_00.00.fits&collection=GPC&tileindex=102158889&POS=CIRCLE,267.78,65.53,0.01 ... + https://eas.esac.esa.int/sas-cutout/cutout?filepath=/euclid/repository_idr/iqr1/Q1_R1/MER/102158889/HSC/EUC_MER_BGSUB-MOSAIC-WISHES-G_TILE102158889-3DC3C3_20241024T205647.635112Z_00.00.fits&collection=HSC&tileindex=102158889&POS=CIRCLE,267.78,65.53,0.01 ... + https://eas.esac.esa.int/sas-cutout/cutout?filepath=/euclid/repository_idr/iqr1/Q1_R1/MER/102158889/HSC/EUC_MER_BGSUB-MOSAIC-WISHES-Z_TILE102158889-60A7E6_20241024T204143.931276Z_00.00.fits&collection=HSC&tileindex=102158889&POS=CIRCLE,267.78,65.53,0.01 ... + https://eas.esac.esa.int/sas-cutout/cutout?filepath=/euclid/repository_idr/iqr1/Q1_R1/MER/102158889/MEGACAM/EUC_MER_BGSUB-MOSAIC-CFIS-R_TILE102158889-4366B7_20241024T203624.450577Z_00.00.fits&collection=MEGACAM&tileindex=102158889&POS=CIRCLE,267.78,65.53,0.01 ... + https://eas.esac.esa.int/sas-cutout/cutout?filepath=/euclid/repository_idr/iqr1/Q1_R1/MER/102158889/MEGACAM/EUC_MER_BGSUB-MOSAIC-CFIS-U_TILE102158889-9E97F_20241024T204431.839748Z_00.00.fits&collection=MEGACAM&tileindex=102158889&POS=CIRCLE,267.78,65.53,0.01 ... + https://eas.esac.esa.int/sas-cutout/cutout?filepath=/euclid/repository_idr/iqr1/Q1_R1/MER/102158889/NISP/EUC_MER_BGSUB-MOSAIC-NIR-H_TILE102158889-ED035A_20241024T212936.705156Z_00.00.fits&collection=NISP&tileindex=102158889&POS=CIRCLE,267.78,65.53,0.01 ... + https://eas.esac.esa.int/sas-cutout/cutout?filepath=/euclid/repository_idr/iqr1/Q1_R1/MER/102158889/NISP/EUC_MER_BGSUB-MOSAIC-NIR-J_TILE102158889-B8D44B_20241024T215040.579149Z_00.00.fits&collection=NISP&tileindex=102158889&POS=CIRCLE,267.78,65.53,0.01 ... + https://eas.esac.esa.int/sas-cutout/cutout?filepath=/euclid/repository_idr/iqr1/Q1_R1/MER/102158889/NISP/EUC_MER_BGSUB-MOSAIC-NIR-Y_TILE102158889-AC6585_20241024T225321.344048Z_00.00.fits&collection=NISP&tileindex=102158889&POS=CIRCLE,267.78,65.53,0.01 ... + https://eas.esac.esa.int/sas-cutout/cutout?filepath=/euclid/repository_idr/iqr1/Q1_R1/MER/102158889/VIS/EUC_MER_BGSUB-MOSAIC-VIS_TILE102158889-F95D3B_20241025T024806.508980Z_00.00.fits&collection=VIS&tileindex=102158889&POS=CIRCLE,267.78,65.53,0.01 ... + +The path to the FITS file can be extracted from the response using a simple regular expression. + + >>> import re + >>> p = re.compile(r"filepath=(.*?)&") + >>> for i in [(p.search(t["cutout_access_url"]).group(1), t["instrument_name"]) for t in table]: + ... print(i) + ('/euclid/repository_idr/iqr1/Q1_R1/MER/102158889/GPC/EUC_MER_BGSUB-MOSAIC-PANSTARRS-I_TILE102158889-5BC663_20241024T203110.498476Z_00.00.fits', 'GPC') + ('/euclid/repository_idr/iqr1/Q1_R1/MER/102158889/HSC/EUC_MER_BGSUB-MOSAIC-WISHES-G_TILE102158889-3DC3C3_20241024T205647.635112Z_00.00.fits', 'HSC') + ('/euclid/repository_idr/iqr1/Q1_R1/MER/102158889/HSC/EUC_MER_BGSUB-MOSAIC-WISHES-Z_TILE102158889-60A7E6_20241024T204143.931276Z_00.00.fits', 'HSC') + ('/euclid/repository_idr/iqr1/Q1_R1/MER/102158889/MEGACAM/EUC_MER_BGSUB-MOSAIC-CFIS-R_TILE102158889-4366B7_20241024T203624.450577Z_00.00.fits', 'MEGACAM') + ('/euclid/repository_idr/iqr1/Q1_R1/MER/102158889/MEGACAM/EUC_MER_BGSUB-MOSAIC-CFIS-U_TILE102158889-9E97F_20241024T204431.839748Z_00.00.fits', 'MEGACAM') + ('/euclid/repository_idr/iqr1/Q1_R1/MER/102158889/NISP/EUC_MER_BGSUB-MOSAIC-NIR-H_TILE102158889-ED035A_20241024T212936.705156Z_00.00.fits', 'NISP') + ('/euclid/repository_idr/iqr1/Q1_R1/MER/102158889/NISP/EUC_MER_BGSUB-MOSAIC-NIR-J_TILE102158889-B8D44B_20241024T215040.579149Z_00.00.fits', 'NISP') + ('/euclid/repository_idr/iqr1/Q1_R1/MER/102158889/NISP/EUC_MER_BGSUB-MOSAIC-NIR-Y_TILE102158889-AC6585_20241024T225321.344048Z_00.00.fits', 'NISP') + ('/euclid/repository_idr/iqr1/Q1_R1/MER/102158889/VIS/EUC_MER_BGSUB-MOSAIC-VIS_TILE102158889-F95D3B_20241025T024806.508980Z_00.00.fits', 'VIS') 1.7. MER Cutouts ^^^^^^^^^^^^^^^^