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.gz |
+ NISP |
+ NIR_J |
+ {(1.56878970716861 , -1.15654637163971),(1.54566457897093 , -1.15654636650839),(1.54590379663453 , -1.14723910521964),(1.568550514417 , -1.14723911022404)} |
+ 89.2225775443200035 |
+ -66 |
+ null |
+ sedm |
+ mosaic |
+ DpdMerBksMosaic |
+ 3 |
+
+
+ |
+ |
+ EUC_NIR_W-CAL-IMAGE_H-65602-17_20230906T190444.924367Z.fits |
+ NISP |
+ NIR_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.498500000000007 |
+ 65602 |
+ sedm |
+ image |
+ DpdNirCalibratedFrame |
+ 2 |
+
+
+
+
+
+
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
^^^^^^^^^^^^^^^^