diff --git a/.github/workflows/ci_tests.yml b/.github/workflows/ci_tests.yml index 428589e099..a20ed7910b 100644 --- a/.github/workflows/ci_tests.yml +++ b/.github/workflows/ci_tests.yml @@ -42,18 +42,25 @@ jobs: toxenv: py39-test-oldestdeps-alldeps toxargs: -v - - name: Python 3.10 with all optional dependencies (MacOS X) + - name: OSX, py310, all optional dependencies os: macos-latest python: "3.10" toxenv: py310-test-alldeps toxargs: -v - - name: Python 3.11 with mandatory dependencies (Windows) + - name: Windows, py311, mandatory dependencies only os: windows-latest python: "3.11" toxenv: py311-test toxargs: -v + - name: Linux ARM, py312, all optional dependencies + os: ubuntu-24.04-arm + python: "3.12" + toxenv: py312-test-alldeps + toxargs: -v + + steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 diff --git a/CHANGES.rst b/CHANGES.rst index 00915e7c4f..d237727522 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,7 +3,9 @@ New Tools and Services ---------------------- - +esa.integral +^^^^^^^^^^^^ +- New module to access the ESA Integral Science Legacy Archive. [#3154] Service fixes and enhancements ------------------------------ diff --git a/astroquery/esa/integral/__init__.py b/astroquery/esa/integral/__init__.py new file mode 100644 index 0000000000..cb6bbcbef6 --- /dev/null +++ b/astroquery/esa/integral/__init__.py @@ -0,0 +1,60 @@ +""" +========= +ISLA Init +========= + +European Space Astronomy Centre (ESAC) +European Space Agency (ESA) + +""" + +from astropy import config as _config + +ISLA_DOMAIN = 'https://isla.esac.esa.int/tap/' +ISLA_TAP_URL = ISLA_DOMAIN + 'tap' + + +class Conf(_config.ConfigNamespace): + """ + Configuration parameters for `astroquery.esa.integral`. + """ + ISLA_TAP_SERVER = _config.ConfigItem(ISLA_TAP_URL, "ISLA TAP Server") + ISLA_DATA_SERVER = _config.ConfigItem(ISLA_DOMAIN + 'data?', "ISLA Data Server") + ISLA_LOGIN_SERVER = _config.ConfigItem(ISLA_DOMAIN + 'login', "ISLA Login Server") + ISLA_LOGOUT_SERVER = _config.ConfigItem(ISLA_DOMAIN + 'logout', "ISLA Logout Server") + ISLA_SERVLET = _config.ConfigItem(ISLA_TAP_URL + "/sync/?PHASE=RUN", + "ISLA Sync Request") + ISLA_TARGET_RESOLVER = _config.ConfigItem(ISLA_DOMAIN + "servlet/target-resolver?TARGET_NAME={}" + "&RESOLVER_TYPE={}&FORMAT=json", + "ISLA Target Resolver Request") + + ISLA_INSTRUMENT_BAND_QUERY = _config.ConfigItem('select i.name as instrument, b."name" as band, ' + 'i.instrument_oid, b.band_oid from ila.instrument i join ' + 'ila.band b using(instrument_oid);', + "ISLA Instrument Band Query") + ISLA_EPOCH_TARGET_QUERY = _config.ConfigItem("select distinct epoch from ila.epoch where source_id = '{}' and " + "(instrument_oid = {} or band_oid = {})", + "ISLA Epoch Query") + ISLA_EPOCH_QUERY = _config.ConfigItem("select distinct epoch from ila.epoch where " + "(instrument_oid = {} or band_oid = {})", + "ISLA Epoch Query") + ISLA_OBSERVATION_BASE_QUERY = _config.ConfigItem("select * from ila.cons_pub_obs", + "ISLA Observation Base Query") + ISLA_TARGET_CONDITION = _config.ConfigItem("select distinct src.name, src.ra, src.dec, src.source_id from " + "ila.v_cat_source src where " + "src.name ilike '%{}%' order by src.name asc", + "ISLA Target Condition") + ISLA_CONE_TARGET_CONDITION = _config.ConfigItem("select distinct src.name, src.ra, src.dec, " + "src.source_id from ila.v_cat_source src where " + "1=CONTAINS(POINT('ICRS',src.ra,src.dec),CIRCLE('ICRS',{},{},{}))", + "ISLA Target Condition") + ISLA_COORDINATE_CONDITION = _config.ConfigItem("1=CONTAINS(POINT('ICRS',ra,dec),CIRCLE('ICRS',{},{},{}))", + "ISLA Coordinate Condition") + TIMEOUT = 60 + + +conf = Conf() + +from .core import Integral, IntegralClass + +__all__ = ['Integral', 'IntegralClass', 'Conf', 'conf'] diff --git a/astroquery/esa/integral/core.py b/astroquery/esa/integral/core.py new file mode 100644 index 0000000000..3f1a388ca8 --- /dev/null +++ b/astroquery/esa/integral/core.py @@ -0,0 +1,856 @@ +# Licensed under a 3-clause BSD style license - see LICENSE.rst +""" +====================== +ISLA Astroquery Module +====================== + +European Space Astronomy Centre (ESAC) +European Space Agency (ESA) + +""" +from astropy.table import Table +from astroquery.query import BaseQuery, BaseVOQuery +from astroquery import log +from astroquery.utils import commons +import pyvo +from requests import HTTPError + +from . import conf +import time +import astroquery.esa.utils.utils as esautils +from datetime import datetime + +__all__ = ['Integral', 'IntegralClass'] + + +class IntegralClass(BaseVOQuery, BaseQuery): + """ + Class to init ESA Integral Module and communicate with isla + """ + + def __init__(self, auth_session=None): + super().__init__() + + # Checks if auth session has been defined. If not, create a new session + if auth_session: + self._auth_session = auth_session + else: + self._auth_session = esautils.ESAAuthSession() + + self._auth_session.timeout = conf.TIMEOUT + self._tap = None + self._tap_url = conf.ISLA_TAP_SERVER + + self.instruments = [] + self.bands = [] + self.instrument_band_map = {} + + @property + def tap(self) -> pyvo.dal.TAPService: + if self._tap is None: + self._tap = pyvo.dal.TAPService( + conf.ISLA_TAP_SERVER, session=self._auth_session) + # Retrieve the instruments and bands available within ISLA Archive + self.get_instrument_band_map() + + return self._tap + + def get_tables(self, *, only_names=False): + """ + Gets all public tables within ISLA TAP + + Parameters + ---------- + only_names : bool, optional, default False + True to load table names only + + Returns + ------- + A list of table objects + """ + table_set = self.tap.tables + if only_names: + return list(table_set.keys()) + else: + return list(table_set.values()) + + def get_table(self, table): + """ + Gets the specified table from ISLA TAP + + Parameters + ---------- + table : str, mandatory + full qualified table name (i.e. schema name + table name) + + Returns + ------- + A table object + """ + tables = self.get_tables() + for t in tables: + if table == t.name: + return t + + def get_job(self, jobid): + """ + Returns the job corresponding to an ID. Note that the caller must be able to see + the job in the current security context. + + Parameters + ---------- + jobid : str, mandatory + ID of the job to view + + Returns + ------- + JobSummary corresponding to the job ID + """ + + return self.tap.get_job(job_id=jobid) + + def get_job_list(self, *, phases=None, after=None, last=None, + short_description=True): + """ + Returns all the asynchronous jobs + + Parameters + ---------- + phases : list of str + Union of job phases to filter the results by. + after : datetime + Return only jobs created after this datetime + last : int + Return only the most recent number of jobs + short_description : flag - True or False + If True, the jobs in the list will contain only the information + corresponding to the TAP ShortJobDescription object (job ID, phase, + run ID, owner ID and creation ID) whereas if False, a separate GET + call to each job is performed for the complete job description + + Returns + ------- + A list of Job objects + """ + + return self.tap.get_job_list(phases=phases, after=after, last=last, + short_description=short_description) + + def login(self, *, user=None, password=None): + """ + Performs a login. + TAP+ only + User and password shall be used + + Parameters + ---------- + user : str, mandatory, default None + Username. If no value is provided, a prompt to type it will appear + password : str, mandatory, default None + User password. If no value is provided, a prompt to type it will appear + """ + self.tap._session.login(login_url=conf.ISLA_LOGIN_SERVER, user=user, password=password) + + def logout(self): + """ + Performs a logout. + TAP+ only + """ + self.tap._session.logout(logout_url=conf.ISLA_LOGOUT_SERVER) + + def query_tap(self, query, *, async_job=False, output_file=None, output_format='votable'): + """Launches a synchronous or asynchronous job to query the ISLA tap + + Parameters + ---------- + query : str, mandatory + query (adql) to be executed + async_job : bool, optional, default 'False' + executes the query (job) in asynchronous/synchronous mode (default + synchronous) + output_file : str, optional, default None + file name where the results are saved if dumpToFile is True. + If this parameter is not provided, the jobid is used instead + output_format : str, optional, default 'votable' + results format + + Returns + ------- + An astropy.table object containing the results + """ + if async_job: + job = self.tap.submit_job(query) + job.run() + while job.phase == 'EXECUTING': + time.sleep(3) + result = job.fetch_result().to_table() + else: + result = self.tap.search(query).to_table() + + if output_file: + esautils.download_table(result, output_file, output_format) + + return result + + def get_sources(self, target_name, *, async_job=False, output_file=None, output_format=None): + """Retrieve the coordinates of an INTEGRAL source + + Parameters + ---------- + target_name : str, mandatory + target name to be requested, mandatory + async_job : bool, optional, default 'False' + executes the query (job) in asynchronous/synchronous mode (default + synchronous) + output_file : str, optional, default None + file name where the results are saved if dumpToFile is True. + If this parameter is not provided, the jobid is used instead + output_format : str, optional, default 'votable' + results format + + Returns + ------- + An astropy.table object containing the results + """ + + # First attempt, resolve the name in the source catalogue + query = conf.ISLA_TARGET_CONDITION.format(target_name) + result = self.query_tap(query=query, async_job=async_job, output_file=output_file, output_format=output_format) + + if len(result) > 0: + return result + + # Second attempt, resolve using a Resolver Service and cone search to the source catalogue + try: + coordinates = esautils.resolve_target(conf.ISLA_TARGET_RESOLVER, self.tap._session, target_name, 'ALL') + if coordinates: + query = conf.ISLA_CONE_TARGET_CONDITION.format(coordinates.ra.degree, coordinates.dec.degree, 0.0833) + result = self.query_tap(query=query, async_job=async_job, output_file=output_file, + output_format=output_format) + + if len(result) > 0: + return result[0] + + raise ValueError(f"Target {target_name} cannot be resolved for ISLA") + except ValueError: + raise ValueError(f"Target {target_name} cannot be resolved for ISLA") + + def get_observations(self, *, target_name=None, coordinates=None, radius=14.0, start_time=None, end_time=None, + start_revno=None, end_revno=None, async_job=False, output_file=None, output_format=None, + verbose=False): + """Retrieve the INTEGRAL observations associated to target name, time range and/or revolution + + Parameters + ---------- + target_name: str, optional + target name to be requested + coordinates: str or SkyCoord, optional + coordinates of the center in the cone search + radius: float or quantity, optional, default value 14 degrees + radius in degrees (int, float) or quantity of the cone_search + start_time: str in UTC or datetime, optional + start time of the observation + end_time: str in UTC or datetime, optional + end time of the observation + start_revno: string, optional + start revolution number, as a four-digit string with leading zeros + e.g. 0352 + end_revno: string, optional + end revolution number, as a four-digit string with leading zeros + e.g. 0353 + async_job : bool, optional, default 'False' + executes the query (job) in asynchronous/synchronous mode (default + synchronous) + output_file : str, optional, default None + file name where the results are saved if dumpToFile is True. + If this parameter is not provided, the jobid is used instead + output_format : str, optional, default 'votable' + results format + verbose : bool, optional, default 'False' + flag to display information about the process + + Returns + ------- + An astropy.table object containing the results + """ + base_query = conf.ISLA_OBSERVATION_BASE_QUERY + query = base_query + conditions = [] + + # Target name/Coordinates + radius condition + if target_name and coordinates: + raise TypeError("Please use only target or coordinates as " + "parameter.") + # Radius in degrees + if radius: + radius = esautils.get_degree_radius(radius) + + # Resolve target or coordinates to get coordinates + if target_name: + coord = self.get_sources(target_name=target_name) + ra = coord['ra'][0] + dec = coord['dec'][0] + conditions.append(conf.ISLA_COORDINATE_CONDITION.format(ra, dec, radius)) + elif coordinates: + coord = commons.parse_coordinates(coordinates=coordinates) + ra = coord.ra.degree + dec = coord.dec.degree + conditions.append(conf.ISLA_COORDINATE_CONDITION.format(ra, dec, radius)) + + # Start/End time conditions + if start_time: + parsed_start = datetime.fromisoformat(start_time.replace('Z', '+00:00')) + conditions.append(f"endtime >= '{parsed_start}'") + + if end_time: + parsed_end = datetime.fromisoformat(end_time.replace('Z', '+00:00')) + conditions.append(f"starttime <= '{parsed_end}'") + + # Revolution Number conditions + if start_revno and self.__validate_revno(start_revno): + conditions.append(f"end_revno >= '{start_revno}'") + + if end_revno and self.__validate_revno(end_revno): + conditions.append(f"start_revno <= '{end_revno}'") + + # Create final query + if conditions: + query = f"{query} where {' AND '.join(conditions)}" + + query = f"{query} order by obsid" + if verbose: + return query + else: + return self.query_tap(query=query, async_job=async_job, output_file=output_file, + output_format=output_format) + + def download_science_windows(self, *, science_windows=None, observation_id=None, revolution=None, proposal=None, + output_file=None, cache=False, read_fits=True): + """Method to download science windows associated to one of these parameters: + science_windows, observation_id, revolution or proposal + + Parameters + ---------- + science_windows : list of str, optional + Science Windows to download + observation_id: str, optional + Observation ID associated to science windows + revolution: str, optional + Revolution associated to science windows + proposal: str, optional + Proposal ID associated to science windows + output_file: str, optional + File name and path for the downloaded file + cache: bool, optional, default False + Flag to determine if the file is stored in the cache or not + read_fits: bool, optional, default True + Open the downloaded file and parse the existing FITS files + + Returns + ------- + If read_fits=True, a list with objects containing filename, path and FITS file opened with the + science windows. If read_fits=False, the path of the downloaded file + """ + + # Validate and retrieve the correct value + params = self.__get_science_window_parameter(science_windows, observation_id, revolution, proposal) + params['RETRIEVAL_TYPE'] = 'SCW' + try: + + downloaded_file = esautils.download_file(url=conf.ISLA_DATA_SERVER, session=self.tap._session, + filename=output_file, params=params, + cache=cache, cache_folder=self.cache_location, verbose=True) + if read_fits: + return esautils.read_downloaded_fits([downloaded_file]) + else: + return downloaded_file + + except Exception as e: + log.error('No science windows have been found with these inputs. {}'.format(e)) + + def get_timeline(self, coordinates, *, radius=14): + """Retrieve the INTEGRAL timeline associated to coordinates and radius + + Parameters + ---------- + coordinates: str or SkyCoord, mandatory + RA and Dec of the source + radius: float or quantity, optional, default value 14 degrees + radius in degrees (int, float) or quantity of the cone_search + + Returns + ------- + An object containing: + totalItems: a counter for the number of items retrieved + fraFC: + totEffExpo: + timeline: An astropy.table object containing the results for scwExpo, scwRevs, scwTimes and scwOffAxis + """ + + if radius: + radius = esautils.get_degree_radius(radius) + + c = commons.parse_coordinates(coordinates=coordinates) + + query_params = { + 'REQUEST': 'timelines', + "ra": c.ra.degree, + "dec": c.dec.degree, + "radius": radius + } + + try: + # Execute the request to the servlet + request_result = esautils.execute_servlet_request(url=conf.ISLA_SERVLET, + tap=self.tap, + query_params=query_params) + total_items = request_result['totalItems'] + data = request_result['data'] + fraFC = data['fraFC'] + totEffExpo = data['totEffExpo'] + timeline = Table({ + "scwExpo": data["scwExpo"], + "scwRevs": data["scwRevs"], + "scwTimes": [datetime.fromtimestamp(scwTime / 1000) for scwTime in data["scwTimes"]], + "scwOffAxis": data["scwOffAxis"] + }) + return {'total_items': total_items, 'fraFC': fraFC, 'totEffExpo': totEffExpo, 'timeline': timeline} + except HTTPError as e: + if 'None science windows have been selected' in e.response.text: + raise ValueError('No timeline is available for the current coordinates and radius.') + else: + raise e + + def get_epochs(self, *, target_name=None, instrument=None, band=None): + """Retrieve the INTEGRAL epochs associated to a target and an instrument or a band + + Parameters + ---------- + target_name : str, optional + target name to be requested, mandatory + instrument : str, optional + Possible values are in isla.instruments object + band : str, optional + Possible values are in isla.bandsobject + + Returns + ------- + An astropy.table object containing the available epochs + """ + + value = self.__get_instrument_or_band(instrument=instrument, band=band) + instrument_oid, band_oid = self.__get_oids(value) + if target_name: + query = conf.ISLA_EPOCH_TARGET_QUERY.format(target_name, instrument_oid, band_oid) + else: + query = conf.ISLA_EPOCH_QUERY.format(instrument_oid, band_oid) + return self.query_tap(query) + + def get_long_term_timeseries(self, target_name, *, instrument=None, band=None, path='', filename=None, + cache=False, read_fits=True): + """Method to download long term timeseries associated to an epoch and instrument or band + + Parameters + ---------- + target_name : str, mandatory + target name to be requested, mandatory + instrument : str + Possible values are in isla.instruments object + band : str + Possible values are in isla.bandsobject + path: str, optional + Path for the downloaded file + filename: str, optional + Filename for the downloaded file + cache: bool, optional, default False + Flag to determine if the file is stored in the cache or not + read_fits: bool, optional, default True + Open the downloaded file and parse the existing FITS files + + Returns + ------- + If read_fits=True, a list with objects containing filename, path and FITS file opened with long + term timeseries. If read_fits=False, the path of the downloaded file + """ + + value = self.__get_instrument_or_band(instrument=instrument, band=band) + + params = {'RETRIEVAL_TYPE': 'long_timeseries', + 'source': target_name, + 'instrument_oid': self.instrument_band_map[value]['instrument_oid']} + try: + downloaded_file = esautils.download_file(url=conf.ISLA_DATA_SERVER, session=self.tap._session, + params=params, path=path, filename=filename, + cache=cache, cache_folder=self.cache_location, verbose=True) + if read_fits: + return esautils.read_downloaded_fits([downloaded_file]) + else: + return downloaded_file + except HTTPError as err: + log.error('No long term timeseries have been found with these inputs. {}'.format(err)) + except Exception as e: + log.error('Problem when retrieving long term timeseries. {}'.format(e)) + + def get_short_term_timeseries(self, target_name, epoch, instrument=None, band=None, + path='', filename=None, cache=False, read_fits=True): + """Method to download short term timeseries associated to an epoch and instrument or band + + Parameters + ---------- + target_name : str, mandatory + target name to be requested, mandatory + epoch : str, mandatory + reference epoch for the short term timeseries + instrument : str, optional + Possible values are in isla.instruments object + band : str, optional + Possible values are in isla.bandsobject + path: str, optional + Path for the downloaded file + filename: str, optional + Filename for the downloaded file + cache: bool, optional, default False + Flag to determine if the file is stored in the cache or not + read_fits: bool, optional, default True + Open the downloaded file and parse the existing FITS files + + Returns + ------- + If read_fits=True, a list with objects containing filename, path and FITS file opened with short + term timeseries. If read_fits=False, the path of the downloaded file + + """ + + value = self.__get_instrument_or_band(instrument=instrument, band=band) + self.__validate_epoch(target_name=target_name, epoch=epoch, + instrument=instrument, band=band) + + params = {'RETRIEVAL_TYPE': 'short_timeseries', + 'source': target_name, + 'band_oid': self.instrument_band_map[value]['band_oid'], + 'epoch': epoch} + + try: + downloaded_file = esautils.download_file(url=conf.ISLA_DATA_SERVER, session=self.tap._session, + params=params, path=path, filename=filename, + cache=cache, cache_folder=self.cache_location, verbose=True) + + if read_fits: + return esautils.read_downloaded_fits([downloaded_file]) + else: + return downloaded_file + except HTTPError as err: + log.error('No short term timeseries have been found with these inputs. {}'.format(err)) + except Exception as e: + log.error('Problem when retrieving short term timeseries. {}'.format(e)) + + def get_spectra(self, target_name, epoch, instrument=None, band=None, *, path='', filename=None, + cache=False, read_fits=True): + """Method to download mosaics associated to an epoch and instrument or band + + Parameters + ---------- + target_name : str, mandatory + target name to be requested, mandatory + epoch : str, mandatory + reference epoch for the short term timeseries + instrument : str + Possible values are in isla.instruments object + band : str + Possible values are in isla.bandsobject + path: str, optional + Path for the downloaded file + filename: str, optional + Filename for the downloaded file + cache: bool, optional, default False + Flag to determine if the file is stored in the cache or not + read_fits: bool, optional, default True + Open the downloaded file and parse the existing FITS files + + Returns + ------- + If read_fits=True, a list with objects containing filename, path and FITS file opened with spectra. + If read_fits=False, a list of paths of the downloaded files + """ + + value = self.__get_instrument_or_band(instrument=instrument, band=band) + self.__validate_epoch(target_name=target_name, epoch=epoch, + instrument=instrument, band=band) + query_params = { + 'REQUEST': 'spectra', + "source": target_name, + "instrument_oid": self.instrument_band_map[value]['instrument_oid'], + "epoch": epoch + } + + try: + # Execute the request to the servlet + request_result = esautils.execute_servlet_request(url=conf.ISLA_SERVLET, + tap=self.tap, + query_params=query_params) + + if len(request_result) == 0: + raise ValueError('Please try with different input parameters.') + + # Parse the spectrum + downloaded_files = [] + for element in request_result: + params = {'RETRIEVAL_TYPE': 'spectras', + 'spectra_oid': element['spectraOid']} + downloaded_files.append( + esautils.download_file(url=conf.ISLA_DATA_SERVER, session=self.tap._session, + params=params, path=path, filename=filename, + cache=cache, cache_folder=self.cache_location, verbose=True)) + + if read_fits: + return esautils.read_downloaded_fits(downloaded_files) + else: + return downloaded_files + except ValueError as err: + log.error('Spectra are not available with these inputs. {}'.format(err)) + except Exception as e: + log.error('Problem when retrieving spectra. {}'.format(e)) + + def get_mosaic(self, epoch, instrument=None, band=None, *, path='', filename=None, cache=False, read_fits=True): + """Method to download mosaics associated to an epoch and instrument or band + + Parameters + ---------- + epoch : str, mandatory + reference epoch for the short term timeseries + instrument : str + Possible values are in isla.instruments object + band : str + Possible values are in isla.bandsobject + cache: bool, optional, default False + Flag to determine if the file is stored in the cache or not + path: str, optional + Path for the downloaded file + filename: str, optional + Filename for the downloaded file + read_fits: bool, optional, default True + Open the downloaded file and parse the existing FITS files + + Returns + ------- + If read_fits=True, a list with objects containing filename, path and FITS file opened with mosaics. + If read_fits=False, a list of paths of the downloaded files + """ + + self.__validate_epoch(epoch=epoch, + instrument=instrument, band=band) + + value = self.__get_instrument_or_band(instrument=instrument, band=band) + + query_params = { + 'REQUEST': 'mosaics', + "band_oid": self.instrument_band_map[value]['band_oid'], + "epoch": epoch + } + + try: + # Execute the request to the servlet + request_result = esautils.execute_servlet_request(url=conf.ISLA_SERVLET, + tap=self.tap, + query_params=query_params) + + if len(request_result) == 0: + raise ValueError('Please try with different input parameters.') + + downloaded_files = [] + for element in request_result: + params = {'RETRIEVAL_TYPE': 'mosaics', + 'mosaic_oid': element['mosaicOid']} + downloaded_files.append( + esautils.download_file(url=conf.ISLA_DATA_SERVER, session=self.tap._session, + params=params, path=path, filename=filename, + cache=cache, cache_folder=self.cache_location, verbose=True)) + if read_fits: + return esautils.read_downloaded_fits(downloaded_files) + else: + return downloaded_files + except ValueError as err: + log.error('Mosaics are not available for these inputs. {}'.format(err)) + except Exception as e: + log.error('Problem when retrieving mosaics. {}'.format(e)) + + def get_source_metadata(self, target_name): + """Retrieve the metadata associated to an INTEGRAL target + + Parameters + ---------- + target_name : str, mandatory + target name to be requested, mandatory + + Returns + ------- + An object containing te metadata from the target + """ + query_params = { + 'REQUEST': 'sources', + "SOURCE": target_name + } + try: + return esautils.execute_servlet_request(url=conf.ISLA_SERVLET, + tap=self.tap, + query_params=query_params) + except HTTPError as e: + if 'Source not found in the database' in e.response.text: + raise ValueError(f"Target {target_name} cannot be resolved for ISLA") + else: + raise e + + def get_instrument_band_map(self): + """ + Maps the bands and instruments included in ISLA + """ + + if len(self.instrument_band_map) == 0: + instrument_band_table = self.query_tap(conf.ISLA_INSTRUMENT_BAND_QUERY) + instrument_band_map = {} + + for row in instrument_band_table: + instrument_band_map[row['instrument']] = {'band': row['band'], + 'instrument_oid': row['instrument_oid'], + 'band_oid': row['band_oid']} + instrument_band_map[row['band']] = {'instrument': row['instrument'], + 'instrument_oid': row['instrument_oid'], + 'band_oid': row['band_oid']} + + instruments = instrument_band_table['instrument'] + bands = instrument_band_table['band'] + + self.instruments = instruments + self.bands = bands + self.instrument_band_map = instrument_band_map + + def get_instruments(self): + """ + Get the instruments available in ISLA + """ + self.get_instrument_band_map() + return self.instruments + + def get_bands(self): + """ + Get the bands available in ISLA + """ + self.get_instrument_band_map() + return self.bands + + def __get_instrument_or_band(self, instrument, band): + if instrument and band: + raise TypeError("Please use only instrument or band as " + "parameter.") + + if instrument is None and band is None: + raise TypeError("Please use at least one parameter, instrument or band.") + + if instrument: + value = instrument + else: + value = band + + # Retrieve the available instruments or bands if not loaded yet + self.get_instrument_band_map() + + # Validate the value is in the list of allowed ones + if value in self.instrument_band_map: + return value + + raise ValueError(f"This is not a valid value for instrument or band. Valid values are:\n" + f"Instruments: {self.get_instruments()}\n" + f"Bands: {self.get_bands()}") + + def __get_oids(self, value): + """ + Retrieves the band_oid and instrument_oid associated to a band or instrument + Parameters + ---------- + value: str + value to check + """ + + return self.instrument_band_map[value]['instrument_oid'], self.instrument_band_map[value]['band_oid'] + + def __validate_revno(self, rev_no): + """ + Verifies if the format for revolution number is correct + + Parameters + ---------- + rev_no: str + revolution number + """ + if len(rev_no) == 4: + return True + raise ValueError(f"Revolution number {rev_no} is not correct. It must be a four-digit number as a string, " + f"with leading zeros to complete the four digits") + + def __validate_epoch(self, epoch, *, target_name=None, instrument=None, band=None): + """ + Validate if the epoch is available for the target name and instrument or band + + Parameters + ---------- + epoch : str, mandatory + reference epoch for the short term timeseries + target_name : str, optional + target name to be requested, mandatory + instrument : str, optional + Possible values are in isla.instruments object + band : str, optional + Possible values are in isla.bandsobject + """ + available_epochs = self.get_epochs(target_name=target_name, instrument=instrument, band=band) + + if epoch not in available_epochs['epoch']: + raise ValueError(f"Epoch {epoch} is not available for this target and instrument/band.") + + def __get_science_window_parameter(self, science_windows, observation_id, revolution, proposal): + """ + Verifies if only one parameter is not null and return its value + + Parameters + ---------- + science_windows : list of str or str, mandatory + Science Windows to download + observation_id: str, optional + Observation ID associated to science windows + revolution: str, optional + Revolution associated to science windows + proposal: str, optional + Proposal ID associated to science windows + + Returns + ------- + The correct parameter for the science windows + """ + params = [science_windows, observation_id, revolution, proposal] + + # Count how many are not None + non_none_count = sum(p is not None for p in params) + + # Ensure only one parameter is provided + if non_none_count > 1: + raise ValueError("Only one parameter can be provided at a time.") + + if science_windows is not None: + if isinstance(science_windows, str): + return {'scwid': science_windows} + elif isinstance(science_windows, list): + return {'scwid': ','.join(science_windows)} + + if observation_id is not None and isinstance(observation_id, str): + return {'obsid': observation_id} + + if revolution is not None and isinstance(revolution, str): + return {'REVID': revolution} + + if proposal is not None and isinstance(proposal, str): + return {'PROPID': proposal} + + raise ValueError("Input parameters are wrong") + + +Integral = IntegralClass() diff --git a/astroquery/esa/integral/tests/__init__.py b/astroquery/esa/integral/tests/__init__.py new file mode 100644 index 0000000000..a60d631950 --- /dev/null +++ b/astroquery/esa/integral/tests/__init__.py @@ -0,0 +1,10 @@ +# Licensed under a 3-clause BSD style license - see LICENSE.rst +""" +============== +ISLA TEST Init +============== + +European Space Astronomy Centre (ESAC) +European Space Agency (ESA) + +""" diff --git a/astroquery/esa/integral/tests/data/tar_file.tar b/astroquery/esa/integral/tests/data/tar_file.tar new file mode 100644 index 0000000000..21fdb7b311 Binary files /dev/null and b/astroquery/esa/integral/tests/data/tar_file.tar differ diff --git a/astroquery/esa/integral/tests/data/tar_gz_file.gz b/astroquery/esa/integral/tests/data/tar_gz_file.gz new file mode 100644 index 0000000000..6879c325e3 Binary files /dev/null and b/astroquery/esa/integral/tests/data/tar_gz_file.gz differ diff --git a/astroquery/esa/integral/tests/data/zip_file.zip b/astroquery/esa/integral/tests/data/zip_file.zip new file mode 100644 index 0000000000..81fd39338b Binary files /dev/null and b/astroquery/esa/integral/tests/data/zip_file.zip differ diff --git a/astroquery/esa/integral/tests/mocks.py b/astroquery/esa/integral/tests/mocks.py new file mode 100644 index 0000000000..dc5e15ff86 --- /dev/null +++ b/astroquery/esa/integral/tests/mocks.py @@ -0,0 +1,120 @@ +# Licensed under a 3-clause BSD style license - see LICENSE.rst +""" +================ +ISLA Mocks tests +================ + +European Space Astronomy Centre (ESAC) +European Space Agency (ESA) + +""" +from unittest.mock import Mock +from requests import HTTPError + +from pyvo.dal import DALResults +from astropy.io.votable.tree import VOTableFile +from astropy.table import Table + +instruments_value = ["i1"] +bands_value = ["b1"] +instrument_band_mock_value = {"i1": {'instrument_oid': 'id1', 'band_oid': 'id2'}, + "b1": {'instrument_oid': 'id3', 'band_oid': 'id4'}} + + +def get_instrument_bands(): + return instruments_value, bands_value, instrument_band_mock_value + + +def get_dal_table(): + data = { + "source_id": [123456789012345, 234567890123456, 345678901234567], + "ra": [266.404997, 266.424512, 266.439127], + "dec": [-28.936173, -28.925636, -28.917813] + } + + # Create an Astropy Table with the mock data + astropy_table = Table(data) + + return DALResults(VOTableFile.from_table(astropy_table)) + + +def get_empty_table(): + data = { + "source_id": [], + } + + # Create an Astropy Table with the mock data + return Table(data) + + +def get_sources_table(): + data = { + "name": ['Crab'], + "ra": [83.63320922851562], + "dec": [22.01447105407715], + "source_id": ['J053432.0+220052'] + } + + # Create an Astropy Table with the mock data + astropy_table = Table(data) + + return DALResults(VOTableFile.from_table(astropy_table)) + + +def get_mock_timeline(): + return {'totalItems': 1, + 'data': { + 'fraFC': 1, + 'totEffExpo': 1, + 'scwExpo': [1, 2], + 'scwRevs': [1, 2], + 'scwTimes': [1, 2], + 'scwOffAxis': [1, 2] + }} + + +def get_mock_timeseries(): + return {'aggregationValue': 1, + 'aggregationUnit': 'm', + 'detectors': ['d1', 'd2'], + 'sourceId': 'target', + 'totalItems': 2, + 'time': [['2024-12-18T12:00:00', '2024-12-19T12:00:00'], ['2024-12-20T12:00:00', '2024-12-18T21:00:00']], + 'rates': [[1, 2], [3, 4]], + 'ratesError': [[1, 2], [3, 4]] + } + + +def get_mock_spectra(): + return [{'spectraOid': 1, + 'fileName': 'm', + 'detector': ['d1', 'd2'], + 'metadata': 'meta', + 'dateStart': 'target', + 'dateStop': 2, + 'time': ['2024-12-18T12:00:00', '2024-12-19T12:00:00', '2024-12-20T12:00:00', '2024-12-18T21:00:00'], + 'energy': [1, 2, 3, 4], + 'energyError': [1, 2, 3, 4], + 'rate': [1, 2, 3, 4], + 'rateError': [1, 2, 3, 4] + }] + + +def get_mock_mosaic(): + return [{'mosaicOid': 1, + 'fileName': 'm', + 'height': 2, + 'width': 2, + 'minZScale': 1, + 'maxZScale': 3, + 'ra': [[1, 2], [3, 4]], + 'dec': [[1, 2], [3, 4]], + 'data': [[1, 2], [3, 4]] + }] + + +def get_mock_response(): + error_message = "Mocked HTTP error" + mock_response = Mock() + mock_response.raise_for_status.side_effect = HTTPError(error_message) + return mock_response diff --git a/astroquery/esa/integral/tests/setup_package.py b/astroquery/esa/integral/tests/setup_package.py new file mode 100644 index 0000000000..448d2cf2b6 --- /dev/null +++ b/astroquery/esa/integral/tests/setup_package.py @@ -0,0 +1,18 @@ +# Licensed under a 3-clause BSD style license - see LICENSE.rst +import os + + +# setup paths to the test data +# can specify a single file or a list of files +def get_package_data(): + paths = [os.path.join('data', '*.vot'), + os.path.join('data', '*.xml'), + os.path.join('data', '*.zip'), + os.path.join('data', '*.gz'), + os.path.join('data', '*.tar'), + os.path.join('data', '*.fits'), + os.path.join('data', '*.txt'), + ] # etc, add other extensions + # you can also enlist files individually by names + # finally construct and return a dict for the sub module + return {'astroquery.esa.integral.tests': paths} diff --git a/astroquery/esa/integral/tests/test_isla_remote.py b/astroquery/esa/integral/tests/test_isla_remote.py new file mode 100644 index 0000000000..bf9d54aaff --- /dev/null +++ b/astroquery/esa/integral/tests/test_isla_remote.py @@ -0,0 +1,287 @@ +# Licensed under a 3-clause BSD style license - see LICENSE.rst +""" +============== +ISLA TAP tests +============== + +European Space Astronomy Centre (ESAC) +European Space Agency (ESA) + +""" +import os +import tempfile + +from astropy.coordinates import SkyCoord +from astroquery.esa.integral import IntegralClass +import pytest +from pyvo import DALQueryError + + +def data_path(filename): + data_dir = os.path.join(os.path.dirname(__file__), 'data') + return os.path.join(data_dir, filename) + + +def close_file(file): + file.close() + + +def close_files(file_list): + for file in file_list: + close_file(file['fits']) + + +def create_temp_folder(): + return tempfile.TemporaryDirectory() + + +@pytest.mark.remote_data +class TestIntegralRemote: + + def test_get_table(self): + isla = IntegralClass() + + obscore = isla.get_table('ivoa.obscore') + assert obscore is not None + + ehst = isla.get_table('ehst.archive') + assert ehst is None + + def test_query_tap(self): + isla = IntegralClass() + + # Query an existing table + result = isla.query_tap('select top 10 * from ivoa.obscore;') + assert len(result) > 0 + + # Query a table that does not exist + with pytest.raises(DALQueryError) as err: + isla.query_tap('select top 10 * from schema.table;') + assert 'Unknown table' in err.value.args[0] + + # Store the result in a file + temp_folder = create_temp_folder() + filename = os.path.join(temp_folder.name, 'query_tap.votable') + isla.query_tap('select top 10 * from ivoa.obscore;', output_file=filename) + assert os.path.exists(filename) + + temp_folder.cleanup() + + def test_get_sources(self): + isla = IntegralClass() + sources = isla.get_sources(target_name='crab') + + assert len(sources) == 1 + assert sources[0]['name'] == 'Crab' + + # Query a target that does not exist + with pytest.raises(ValueError) as err: + isla.get_sources(target_name='star') + assert 'Target star cannot be resolved for ISLA' in err.value.args[0] + + def test_get_source_metadata(self): + isla = IntegralClass() + metadata = isla.get_source_metadata(target_name='crab') + + assert len(metadata) >= 1 + assert metadata[0]['name'] == 'Integral' + assert metadata[0]['metadata'][0]['value'] == 'Crab' + + # Query a target that does not exist + with pytest.raises(ValueError) as err: + isla.get_source_metadata(target_name='star') + assert 'Target star cannot be resolved for ISLA' in err.value.args[0] + + def test_get_observations(self): + isla = IntegralClass() + observations = isla.get_observations(target_name='crab', radius=12.0, start_time='2005-01-01T00:00:00Z', + end_time='2005-12-31T00:00:00Z', start_revno='0290', end_revno='0599') + assert len(observations) > 1 + + # Query returned + query = isla.get_observations(target_name='crab', radius=12.0, start_time='2005-01-01T00:00:00Z', + end_time='2005-12-31T00:00:00Z', start_revno='0290', end_revno='0599', + verbose=True) + + assert type(query) is str + assert 'select * from ila.cons_pub_obs' in query + + # Invalid date format + with pytest.raises(ValueError) as err: + isla.get_observations(target_name='crab', radius=12.0, start_time='2005/01/01T00:00:00Z', + end_time='2005/12/31T00:00:00Z', start_revno='0290', end_revno='0599') + assert 'Invalid isoformat' in err.value.args[0] + + # Invalid revno + with pytest.raises(ValueError) as err: + isla.get_observations(target_name='crab', radius=12.0, start_time='2005-01-01T00:00:00Z', + end_time='2005-12-31T00:00:00Z', start_revno='290', end_revno='0599') + assert 'Revolution number 290 is not correct' in err.value.args[0] + + # No observations available + with pytest.raises(ValueError) as err: + isla.get_observations(target_name='star', radius=12.0, start_time='2005-01-01T00:00:00Z', + end_time='2005-12-31T00:00:00Z', start_revno='0290', end_revno='0599') + assert 'Target star cannot be resolved for ISLA' in err.value.args[0] + + def test_download_science_windows(self): + # Simple download + isla = IntegralClass() + temp_folder = create_temp_folder() + output_file = os.path.join(temp_folder.name, 'sc') + sc = isla.download_science_windows(science_windows='008100430010', output_file=output_file) + assert len(sc) > 1 + + close_files(sc) + + # Only one parameter is allowed + with pytest.raises(ValueError) as err: + isla.download_science_windows(science_windows='008100430010', observation_id='03200490001') + assert 'Only one parameter can be provided at a time.' in err.value.args[0] + + # Correct parameters are set + with pytest.raises(ValueError) as err: + isla.download_science_windows(revolution=12) + assert 'Input parameters are wrong' in err.value.args[0] + + temp_folder.cleanup() + + def test_get_timeline(self): + isla = IntegralClass() + coords = SkyCoord(ra=83.63320922851562, dec=22.01447105407715, unit="deg") + timeline = isla.get_timeline(coordinates=coords) + + assert timeline is not None + assert 'timeline' in timeline + assert timeline['total_items'] == len(timeline['timeline']) + + # No timeline has been found + zero_coords = SkyCoord(ra=0, dec=0, unit="deg") + with pytest.raises(ValueError) as err: + isla.get_timeline(coordinates=zero_coords, radius=0.8) + assert 'No timeline is available for the current coordinates and radius.' in err.value.args[0] + + def test_get_epochs(self): + # Nominal request + isla = IntegralClass() + epochs = isla.get_epochs(target_name='J011705.1-732636', band='28_40') + + assert len(epochs) > 0 + assert epochs['epoch'] is not None + + # Error when band and instrument are set + with pytest.raises(TypeError) as err: + isla.get_epochs(target_name='J011705.1-732636', instrument='jem-x', band='28_40') + assert 'Please use only instrument or band as parameter.' in err.value.args[0] + + # Error when band and instrument are not set + with pytest.raises(TypeError) as err: + isla.get_epochs(target_name='J011705.1-732636') + assert 'Please use at least one parameter, instrument or band.' in err.value.args[0] + + # No epochs are found + epochs = isla.get_epochs(target_name='star', band='28_40') + assert len(epochs) == 0 + + def test_get_long_term_timeseries(self): + temp_folder = create_temp_folder() + + isla = IntegralClass() + ltt = isla.get_long_term_timeseries(target_name='J174537.0-290107', instrument='jem-x', path=temp_folder.name) + + assert len(ltt) > 0 + assert 'fits' in ltt[0] + + close_files(ltt) + + # No correct instrument or band + with pytest.raises(ValueError) as err: + isla.get_long_term_timeseries(target_name='J174537.0-290107', instrument='test') + assert 'This is not a valid value for instrument or band.' in err.value.args[0] + + # No long term timeseries found + ltt = isla.get_long_term_timeseries(target_name='star', instrument='jem-x') + assert ltt is None + + temp_folder.cleanup() + + def test_get_short_term_timeseries(self): + temp_folder = create_temp_folder() + + isla = IntegralClass() + stt = isla.get_short_term_timeseries(target_name='J011705.1-732636', band='28_40', epoch='0745_06340000001', + path=temp_folder.name) + + assert len(stt) > 0 + assert 'fits' in stt[0] + + close_files(stt) + + # No correct instrument or band + with pytest.raises(ValueError) as err: + isla.get_short_term_timeseries(target_name='J011705.1-732636', band='1234', epoch='0745_06340000001') + assert 'This is not a valid value for instrument or band.' in err.value.args[0] + + # No correct epoch + with pytest.raises(ValueError) as err: + isla.get_short_term_timeseries(target_name='J011705.1-732636', band='28_40', epoch='123456') + assert 'Epoch 123456 is not available for this target and instrument/band.' in err.value.args[0] + + # No long term timeseries found + with pytest.raises(ValueError) as err: + isla.get_short_term_timeseries(target_name='star', band='28_40', epoch='0745_06340000001') + assert 'Epoch 0745_06340000001 is not available for this target and instrument/band.' in err.value.args[0] + + temp_folder.cleanup() + + def test_get_spectra(self): + temp_folder = create_temp_folder() + + isla = IntegralClass() + spectra = isla.get_spectra(target_name='J011705.1-732636', instrument='ibis', epoch='0745_06340000001', + path=temp_folder.name) + + assert len(spectra) > 0 + assert 'fits' in spectra[0] + + close_files(spectra) + + # No correct instrument or band + with pytest.raises(ValueError) as err: + isla.get_spectra(target_name='J011705.1-732636', instrument='camera', epoch='0745_06340000001') + assert 'This is not a valid value for instrument or band.' in err.value.args[0] + + # No correct epoch + with pytest.raises(ValueError) as err: + isla.get_spectra(target_name='J011705.1-732636', instrument='ibis', epoch='123456') + assert 'Epoch 123456 is not available for this target and instrument/band.' in err.value.args[0] + + # No long term timeseries found + with pytest.raises(ValueError) as err: + isla.get_spectra(target_name='star', instrument='ibis', epoch='0745_06340000001') + assert 'Epoch 0745_06340000001 is not available for this target and instrument/band.' in err.value.args[0] + + temp_folder.cleanup() + + def test_get_mosaics(self): + temp_folder = create_temp_folder() + + isla = IntegralClass() + mosaics = isla.get_mosaic(epoch='0727_88601650001', instrument='ibis', path=temp_folder.name) + + assert len(mosaics) > 0 + assert 'fits' in mosaics[0] + + close_files(mosaics) + + # No correct instrument or band + with pytest.raises(ValueError) as err: + isla.get_mosaic(epoch='0727_88601650001', instrument='camera') + assert 'This is not a valid value for instrument or band.' in err.value.args[0] + + # No correct epoch + with pytest.raises(ValueError) as err: + isla.get_mosaic(epoch='123456', instrument='ibis') + assert 'Epoch 123456 is not available for this target and instrument/band.' in err.value.args[0] + + temp_folder.cleanup() diff --git a/astroquery/esa/integral/tests/test_isla_tap.py b/astroquery/esa/integral/tests/test_isla_tap.py new file mode 100644 index 0000000000..ec17a6e4e6 --- /dev/null +++ b/astroquery/esa/integral/tests/test_isla_tap.py @@ -0,0 +1,678 @@ +# Licensed under a 3-clause BSD style license - see LICENSE.rst +""" +============== +ISLA TAP tests +============== + +European Space Astronomy Centre (ESAC) +European Space Agency (ESA) + +""" +import os +import shutil +import tempfile + +from astropy.coordinates import SkyCoord +from astroquery.esa.integral import IntegralClass +from astroquery.esa.integral import conf +from unittest.mock import PropertyMock, patch, Mock +import pytest + +from requests import HTTPError + +from astroquery.esa.integral.tests import mocks + + +def mock_instrument_bands(isla_module): + isla_module.instruments, isla_module.bands, isla_module.instrument_band_map = mocks.get_instrument_bands() + + +def data_path(filename): + data_dir = os.path.join(os.path.dirname(__file__), 'data') + return os.path.join(data_dir, filename) + + +def create_temp_folder(): + return tempfile.TemporaryDirectory() + + +def copy_to_temporal_path(data_path, temp_folder, filename): + temp_data_dir = os.path.join(temp_folder.name, filename) + shutil.copy(data_path, temp_data_dir) + return temp_data_dir + + +def close_file(file): + file.close() + + +def close_files(file_list): + for file in file_list: + close_file(file['fits']) + + +class TestIntegralTap: + + def test_get_tables(self): + # default parameters + table_set = PropertyMock() + table_set.keys.return_value = ['ila.epoch', 'ila.cons_pub_obs'] + table_set.values.return_value = ['ila.epoch', 'ila.cons_pub_obs'] + with patch('astroquery.esa.integral.core.pyvo.dal.TAPService', autospec=True) as isla_mock: + isla_mock.return_value.tables = table_set + isla = IntegralClass() + assert len(isla.get_tables(only_names=True)) == 2 + assert len(isla.get_tables()) == 2 + + def test_get_table(self): + table_set = PropertyMock() + tables_result = [Mock() for _ in range(3)] + tables_result[0].name = 'ila.epoch' + tables_result[1].name = 'ila.cons_pub_obs' + table_set.values.return_value = tables_result + + with patch('astroquery.esa.integral.core.pyvo.dal.TAPService', autospec=True) as isla_mock: + isla_mock.return_value.tables = table_set + isla = IntegralClass() + assert isla.get_table('ila.cons_pub_obs').name == 'ila.cons_pub_obs' + assert isla.get_table('test') is None + + @patch('astroquery.esa.integral.core.pyvo.dal.TAPService.capabilities', []) + @patch('astroquery.esa.integral.core.IntegralClass.tap') + @patch('astroquery.esa.integral.core.pyvo.dal.AsyncTAPJob') + def test_load_job(self, isla_job_mock, mock_tap): + jobid = '101' + mock_job = Mock() + mock_job.job_id = '101' + isla_job_mock.job_id.return_value = '101' + mock_tap.get_job.return_value = mock_job + isla = IntegralClass() + + job = isla.get_job(jobid=jobid) + assert job.job_id == '101' + + @patch('astroquery.esa.integral.core.pyvo.dal.TAPService.capabilities', []) + @patch('astroquery.esa.integral.core.IntegralClass.tap') + def test_get_job_list(self, mock_get_job_list): + mock_job = Mock() + mock_job.job_id = '101' + mock_get_job_list.get_job_list.return_value = [mock_job] + isla = IntegralClass() + + jobs = isla.get_job_list() + assert len(jobs) == 1 + + @patch('astroquery.esa.integral.core.pyvo.dal.TAPService.capabilities', []) + @patch('astroquery.esa.integral.core.IntegralClass.get_instrument_band_map') + @patch('astroquery.esa.utils.utils.ESAAuthSession.post') + def test_login_success(self, mock_post, instrument_band_mock): + instrument_band_mock.return_value = mocks.get_instrument_bands() + + mock_response = Mock() + mock_response.raise_for_status.return_value = None # Simulate no HTTP error + mock_response.json.return_value = {"status": "success", "token": "mocked_token"} + + # Configure the mock post method to return the mock Response + mock_post.return_value = mock_response + isla = IntegralClass() + isla.login(user='dummyUser', password='dummyPassword') + + mock_post.assert_called_once_with(url=conf.ISLA_LOGIN_SERVER, + data={"username": "dummyUser", "password": "dummyPassword"}, + headers={'Content-type': 'application/x-www-form-urlencoded', + 'Accept': 'text/plain'}) + + @patch('astroquery.esa.integral.core.pyvo.dal.TAPService.capabilities', []) + @patch('astroquery.esa.integral.core.IntegralClass.get_instrument_band_map') + @patch('astroquery.esa.utils.utils.ESAAuthSession.post') + def test_login_error(self, mock_post, instrument_band_mock): + instrument_band_mock.return_value = mocks.get_instrument_bands() + error_message = "Mocked HTTP error" + mock_response = mocks.get_mock_response() + + # Configure the mock post method to return the mock Response + mock_post.return_value = mock_response + isla = IntegralClass() + with pytest.raises(HTTPError) as err: + isla.login(user='dummyUser', password='dummyPassword') + assert error_message in err.value.args[0] + + @patch('astroquery.esa.integral.core.pyvo.dal.TAPService.capabilities', []) + @patch('astroquery.esa.integral.core.IntegralClass.get_instrument_band_map') + @patch('astroquery.esa.utils.utils.ESAAuthSession.post') + def test_logout_success(self, mock_post, instrument_band_mock): + instrument_band_mock.return_value = mocks.get_instrument_bands() + + mock_response = Mock() + mock_response.raise_for_status.return_value = None # Simulate no HTTP error + mock_response.json.return_value = {"status": "success", "token": "mocked_token"} + + # Configure the mock post method to return the mock Response + mock_post.return_value = mock_response + isla = IntegralClass() + isla.logout() + + mock_post.assert_called_once_with(url=conf.ISLA_LOGOUT_SERVER, + headers={'Content-type': 'application/x-www-form-urlencoded', + 'Accept': 'text/plain'}) + + @patch('astroquery.esa.integral.core.pyvo.dal.TAPService.capabilities', []) + @patch('astroquery.esa.integral.core.IntegralClass.get_instrument_band_map') + @patch('astroquery.esa.utils.utils.ESAAuthSession.post') + def test_logout_error(self, mock_post, instrument_band_mock): + instrument_band_mock.return_value = mocks.get_instrument_bands() + + error_message = "Mocked HTTP error" + mock_response = mocks.get_mock_response() + + # Configure the mock post method to return the mock Response + mock_post.return_value = mock_response + isla = IntegralClass() + with pytest.raises(HTTPError) as err: + isla.logout() + assert error_message in err.value.args[0] + + @patch('astroquery.esa.integral.core.pyvo.dal.TAPService.capabilities', []) + @patch('astropy.table.Table.write') + @patch('astroquery.esa.integral.core.IntegralClass.get_instrument_band_map') + @patch('astroquery.esa.integral.core.pyvo.dal.TAPService.search') + def test_query_tap_sync(self, search_mock, instrument_band_mock, table_mock): + instrument_band_mock.return_value = mocks.get_instrument_bands() + search_mock.return_value = mocks.get_dal_table() + + query = 'select * from ivoa.obscore' + isla = IntegralClass() + isla.query_tap(query=query, output_file='dummy.vot') + search_mock.assert_called_with(query) + table_mock.assert_called() + + @patch('astroquery.esa.integral.core.pyvo.dal.TAPService.capabilities', []) + @patch('pyvo.dal.AsyncTAPJob') + @patch('astroquery.esa.integral.core.IntegralClass.get_instrument_band_map') + @patch('astroquery.esa.integral.core.pyvo.dal.TAPService.submit_job') + def test_query_tap_async(self, submit_job_mock, instrument_band_mock, async_job_mock): + instrument_band_mock.return_value = mocks.get_instrument_bands() + async_job_mock.phase.return_value = 'COMPLETE' + async_job_mock.fetch_result.return_value = mocks.get_dal_table() + + query = 'select * from ivoa.obscore' + isla = IntegralClass() + isla.query_tap(query=query, async_job=True) + submit_job_mock.assert_called_with(query) + + @patch('astroquery.esa.integral.core.pyvo.dal.TAPService.capabilities', []) + @patch('astroquery.esa.integral.core.IntegralClass.query_tap') + @patch('astroquery.esa.integral.core.IntegralClass.get_instrument_band_map') + def test_get_sources_success_catalogue(self, instrument_band_mock, query_tap_mock): + instrument_band_mock.return_value = mocks.get_instrument_bands() + + query_tap_mock.return_value = mocks.get_sources_table().to_table() + isla = IntegralClass() + isla.get_sources(target_name='Crab') + + query_tap_mock.assert_called_with(query="select distinct src.name, src.ra, src.dec, src.source_id from " + "ila.v_cat_source src where src.name ilike '%Crab%' order by " + "src.name asc", async_job=False, output_file=None, output_format=None) + + @patch('astroquery.esa.integral.core.pyvo.dal.TAPService.capabilities', []) + @patch('astroquery.esa.utils.utils.resolve_target') + @patch('astroquery.esa.integral.core.IntegralClass.query_tap') + @patch('astroquery.esa.integral.core.IntegralClass.get_instrument_band_map') + def test_get_sources_success_resolver(self, instrument_band_mock, query_tap_mock, resolve_target_mock): + instrument_band_mock.return_value = mocks.get_instrument_bands() + resolve_target_mock.return_value = SkyCoord(ra=12, dec=13, unit="deg") + query = conf.ISLA_CONE_TARGET_CONDITION.format(12.0, 13.0, 0.0833) + + query_tap_mock.side_effect = [mocks.get_empty_table(), mocks.get_sources_table().to_table()] + isla = IntegralClass() + isla.get_sources(target_name='test') + + query_tap_mock.assert_called_with(query=query, async_job=False, output_file=None, output_format=None) + + @patch('astroquery.esa.integral.core.pyvo.dal.TAPService.capabilities', []) + @patch('astroquery.esa.utils.utils.resolve_target') + @patch('astroquery.esa.integral.core.IntegralClass.query_tap') + @patch('astroquery.esa.integral.core.IntegralClass.get_instrument_band_map') + def test_get_sources_error(self, instrument_band_mock, query_tap_mock, resolve_mock): + instrument_band_mock.return_value = mocks.get_instrument_bands() + + query_tap_mock.return_value = mocks.get_empty_table() + resolve_mock.return_value = None + isla = IntegralClass() + with pytest.raises(ValueError) as err: + isla.get_sources(target_name='test') + assert 'Target test cannot be resolved for ISLA' in err.value.args[0] + + @patch('astroquery.esa.integral.core.pyvo.dal.TAPService.capabilities', []) + @patch('astroquery.esa.integral.core.IntegralClass.query_tap') + @patch('astroquery.esa.integral.core.IntegralClass.get_instrument_band_map') + def test_get_observations_error(self, instrument_band_mock, query_tap_mock): + instrument_band_mock.return_value = mocks.get_instrument_bands() + query_tap_mock.return_value = mocks.get_empty_table() + + isla = IntegralClass() + with pytest.raises(TypeError) as err: + isla.get_observations(target_name='test', coordinates='test') + assert 'Please use only target or coordinates as parameter.' in err.value.args[0] + + @patch('astroquery.esa.integral.core.pyvo.dal.TAPService.capabilities', []) + @patch('astroquery.esa.integral.core.IntegralClass.query_tap') + @patch('astroquery.esa.integral.core.IntegralClass.get_sources') + @patch('astroquery.esa.integral.core.IntegralClass.get_instrument_band_map') + def test_get_observations_t1(self, instrument_band_mock, get_sources_mock, query_tap_mock): + instrument_band_mock.return_value = mocks.get_instrument_bands() + get_sources_mock.return_value = mocks.get_sources_table().to_table() + + # Coordinates + query_tap_mock.return_value = mocks.get_dal_table().to_table() + isla = IntegralClass() + isla.get_observations(target_name='crab', radius=12.0, start_revno='0290', end_revno='0599') + query_tap_mock.assert_called_with(query="select * from ila.cons_pub_obs where " + "1=CONTAINS(POINT('ICRS',ra,dec),CIRCLE('ICRS'" + ",83.63320922851562,22.01447105407715,12.0)) AND " + "end_revno >= '0290' AND start_revno <= '0599' order by obsid", + async_job=False, + output_file=None, + output_format=None) + + @patch('astroquery.esa.integral.core.pyvo.dal.TAPService.capabilities', []) + @patch('astroquery.esa.integral.core.IntegralClass.query_tap') + @patch('astroquery.esa.integral.core.IntegralClass.get_sources') + @patch('astroquery.esa.integral.core.IntegralClass.get_instrument_band_map') + def test_get_observations_t2(self, instrument_band_mock, get_sources_mock, query_tap_mock): + instrument_band_mock.return_value = mocks.get_instrument_bands() + get_sources_mock.return_value = mocks.get_sources_table().to_table() + + # Coordinates + query_tap_mock.return_value = mocks.get_dal_table().to_table() + isla = IntegralClass() + isla.get_observations(target_name='crab', radius=12.0, start_time='2024-12-13T00:00:00', + end_time='2024-12-14T00:00:00') + query_tap_mock.assert_called_with(query="select * from ila.cons_pub_obs where " + "1=CONTAINS(POINT('ICRS',ra,dec)," + "CIRCLE('ICRS',83.63320922851562,22.01447105407715,12.0)) AND " + "endtime >= '2024-12-13 00:00:00' AND starttime <= " + "'2024-12-14 00:00:00' order by obsid", + async_job=False, + output_file=None, + output_format=None) + + @patch('astroquery.esa.integral.core.pyvo.dal.TAPService.capabilities', []) + @patch('astroquery.esa.integral.core.IntegralClass.query_tap') + @patch('astroquery.esa.integral.core.IntegralClass.get_sources') + @patch('astroquery.esa.integral.core.IntegralClass.get_instrument_band_map') + def test_get_observations_query(self, instrument_band_mock, get_sources_mock, query_tap_mock): + instrument_band_mock.return_value = mocks.get_instrument_bands() + get_sources_mock.return_value = mocks.get_sources_table().to_table() + + # Coordinates + query_tap_mock.return_value = mocks.get_dal_table().to_table() + isla = IntegralClass() + query = isla.get_observations(target_name='crab', radius=12.0, start_time='2024-12-13T00:00:00', + end_time='2024-12-14T00:00:00', verbose=True) + assert (query == "select * from ila.cons_pub_obs where " + "1=CONTAINS(POINT('ICRS',ra,dec)," + "CIRCLE('ICRS',83.63320922851562,22.01447105407715,12.0)) AND " + "endtime >= '2024-12-13 00:00:00' AND starttime <= " + "'2024-12-14 00:00:00' order by obsid") + + @patch('astroquery.esa.integral.core.pyvo.dal.TAPService.capabilities', []) + @patch('astroquery.esa.utils.utils.download_file') + @patch('astroquery.esa.integral.core.IntegralClass.get_instrument_band_map') + def test_download_science_windows_error(self, instrument_band_mock, download_mock): + instrument_band_mock.return_value = mocks.get_instrument_bands() + download_mock.return_value = 'file.test' + + isla = IntegralClass() + with pytest.raises(ValueError) as err: + isla.download_science_windows() + assert 'Input parameters are wrong' in err.value.args[0] + + with pytest.raises(ValueError) as err: + isla.download_science_windows(science_windows='sc', observation_id='obs') + assert 'Only one parameter can be provided at a time.' in err.value.args[0] + + @patch('astroquery.esa.integral.core.pyvo.dal.TAPService.capabilities', []) + @patch('astroquery.esa.utils.utils.download_file') + @patch('astroquery.esa.integral.core.IntegralClass.get_instrument_band_map') + def test_download_science_windows(self, instrument_band_mock, download_mock): + instrument_band_mock.return_value = mocks.get_instrument_bands() + + isla = IntegralClass() + with pytest.raises(ValueError) as err: + isla.download_science_windows() + assert 'Input parameters are wrong' in err.value.args[0] + + with pytest.raises(ValueError) as err: + isla.download_science_windows(science_windows='sc', observation_id='obs') + assert 'Only one parameter can be provided at a time.' in err.value.args[0] + + temp_path = create_temp_folder() + temp_file = copy_to_temporal_path(data_path=data_path('zip_file.zip'), temp_folder=temp_path, + filename='zip_file.zip') + download_mock.return_value = temp_file + + sc = isla.download_science_windows(science_windows='sc') + + args, kwargs = download_mock.call_args + assert kwargs['params']['RETRIEVAL_TYPE'] == 'SCW' + + close_files(sc) + temp_path.cleanup() + + @patch('astroquery.esa.integral.core.pyvo.dal.TAPService.capabilities', []) + @patch('astroquery.esa.utils.utils.execute_servlet_request') + @patch('astroquery.esa.integral.core.IntegralClass.get_instrument_band_map') + def test_get_timeline(self, instrument_band_mock, servlet_mock): + instrument_band_mock.return_value = mocks.get_instrument_bands() + servlet_mock.return_value = mocks.get_mock_timeline() + + isla = IntegralClass() + coords = SkyCoord(ra=83.63320922851562, dec=22.01447105407715, unit="deg") + timeline = isla.get_timeline(coordinates=coords) + + assert len(timeline['timeline']['scwRevs']) > 0 + + @patch('astroquery.esa.integral.core.pyvo.dal.TAPService.capabilities', []) + @patch('astroquery.esa.integral.core.IntegralClass.get_instrument_band_map') + def test_get_epochs_error(self, instrument_band_mock): + instrument_band_mock.return_value = mocks.get_instrument_bands() + + isla = IntegralClass() + with pytest.raises(ValueError) as err: + isla.get_epochs(target_name='J011705.1-732636', instrument='i2') + assert 'This is not a valid value for instrument or band.' in err.value.args[0] + + @patch('astroquery.esa.integral.core.pyvo.dal.TAPService.capabilities', []) + @patch('astroquery.esa.integral.core.IntegralClass.query_tap') + @patch('astroquery.esa.integral.core.IntegralClass.get_instrument_band_map') + def test_get_epochs(self, instrument_band_mock, query_tap_mock): + instrument_band_mock.return_value = mocks.get_instrument_bands() + + isla = IntegralClass() + mock_instrument_bands(isla_module=isla) + isla.get_epochs(target_name='J011705.1-732636', instrument='i1') + + query_tap_mock.assert_called_with("select distinct epoch from ila.epoch where " + "source_id = 'J011705.1-732636' and " + "(instrument_oid = id1 or band_oid = id2)") + + @patch('astroquery.esa.integral.core.pyvo.dal.TAPService.capabilities', []) + @patch('astroquery.esa.utils.utils.download_file') + @patch('astroquery.esa.integral.core.log') + @patch('astroquery.esa.integral.core.IntegralClass.get_instrument_band_map') + def test_get_long_term_timeseries_error(self, instrument_band_mock, log_mock, download_mock): + instrument_band_mock.return_value = mocks.get_instrument_bands() + error_message = 'Error' + download_mock.side_effect = HTTPError(error_message) + + isla = IntegralClass() + mock_instrument_bands(isla_module=isla) + + isla.get_long_term_timeseries(target_name='J174537.0-290107', band='b1') + log_mock.error.assert_called_with('No long term timeseries have been found with these inputs. ' + error_message) + + @patch('astroquery.esa.integral.core.pyvo.dal.TAPService.capabilities', []) + @patch('astroquery.esa.utils.utils.download_file') + @patch('astroquery.esa.integral.core.log') + @patch('astroquery.esa.integral.core.IntegralClass.get_instrument_band_map') + def test_get_long_term_timeseries_exception(self, instrument_band_mock, log_mock, download_mock): + instrument_band_mock.return_value = mocks.get_instrument_bands() + error_message = 'Error' + download_mock.side_effect = ValueError(error_message) + + isla = IntegralClass() + mock_instrument_bands(isla_module=isla) + + isla.get_long_term_timeseries(target_name='J174537.0-290107', band='b1') + log_mock.error.assert_called_with('Problem when retrieving long term timeseries. ' + error_message) + + @patch('astroquery.esa.integral.core.pyvo.dal.TAPService.capabilities', []) + @patch('astroquery.esa.utils.utils.download_file') + @patch('astroquery.esa.integral.core.IntegralClass.get_instrument_band_map') + def test_get_long_term_timeseries(self, instrument_band_mock, download_mock): + instrument_band_mock.return_value = mocks.get_instrument_bands() + + temp_path = create_temp_folder() + temp_file = copy_to_temporal_path(data_path=data_path('zip_file.zip'), temp_folder=temp_path, + filename='zip_file.zip') + download_mock.return_value = temp_file + + isla = IntegralClass() + mock_instrument_bands(isla_module=isla) + lt_timeseries_list_extracted = isla.get_long_term_timeseries(target_name='J174537.0-290107', band='b1') + + assert len(lt_timeseries_list_extracted) == 2 + lt_timeseries_list_compressed = isla.get_long_term_timeseries(target_name='J174537.0-290107', band='b1', + read_fits=False) + assert type(lt_timeseries_list_compressed) is str + close_files(lt_timeseries_list_extracted) + temp_path.cleanup() + + @patch('astroquery.esa.integral.core.pyvo.dal.TAPService.capabilities', []) + @patch('astroquery.esa.integral.core.log') + @patch('astroquery.esa.utils.utils.download_file') + @patch('astroquery.esa.integral.core.IntegralClass.get_epochs') + @patch('astroquery.esa.integral.core.IntegralClass.get_instrument_band_map') + def test_get_short_term_timeseries_error(self, instrument_band_mock, epoch_mock, download_mock, log_mock): + instrument_band_mock.return_value = mocks.get_instrument_bands() + epoch_mock.return_value = {'epoch': ['time']} + error_message = 'Error' + download_mock.side_effect = HTTPError(error_message) + + isla = IntegralClass() + mock_instrument_bands(isla_module=isla) + isla.get_short_term_timeseries(target_name='target', + band='b1', epoch='time') + log_mock.error.assert_called_with( + 'No short term timeseries have been found with these inputs. {0}'.format(error_message)) + + @patch('astroquery.esa.integral.core.pyvo.dal.TAPService.capabilities', []) + @patch('astroquery.esa.integral.core.log') + @patch('astroquery.esa.utils.utils.download_file') + @patch('astroquery.esa.integral.core.IntegralClass.get_epochs') + @patch('astroquery.esa.integral.core.IntegralClass.get_instrument_band_map') + def test_get_short_term_timeseries_exception(self, instrument_band_mock, epoch_mock, download_mock, log_mock): + instrument_band_mock.return_value = mocks.get_instrument_bands() + epoch_mock.return_value = {'epoch': ['time']} + error_message = 'Error' + download_mock.side_effect = ValueError(error_message) + + isla = IntegralClass() + mock_instrument_bands(isla_module=isla) + isla.get_short_term_timeseries(target_name='target', + band='b1', epoch='time') + log_mock.error.assert_called_with('Problem when retrieving short term timeseries. ' + error_message) + + @patch('astroquery.esa.integral.core.pyvo.dal.TAPService.capabilities', []) + @patch('astroquery.esa.integral.core.IntegralClass.get_epochs') + @patch('astroquery.esa.integral.core.IntegralClass.get_instrument_band_map') + def test_get_short_term_timeseries_epoch_error(self, instrument_band_mock, epoch_mock): + + instrument_band_mock.return_value = mocks.get_instrument_bands() + epoch_mock.return_value = {'epoch': ['time2']} + + isla = IntegralClass() + mock_instrument_bands(isla_module=isla) + with pytest.raises(ValueError) as err: + isla.get_short_term_timeseries(target_name='target', + band='b1', epoch='time') + assert 'Epoch time is not available for this target and instrument/band.' in err.value.args[0] + + @patch('astroquery.esa.integral.core.pyvo.dal.TAPService.capabilities', []) + @patch('astroquery.esa.utils.utils.download_file') + @patch('astroquery.esa.integral.core.IntegralClass.get_epochs') + @patch('astroquery.esa.integral.core.IntegralClass.get_instrument_band_map') + def test_get_short_term_timeseries(self, instrument_band_mock, epoch_mock, download_mock): + + instrument_band_mock.return_value = mocks.get_instrument_bands() + epoch_mock.return_value = {'epoch': ['time']} + temp_path = create_temp_folder() + temp_file = copy_to_temporal_path(data_path=data_path('tar_file.tar'), temp_folder=temp_path, + filename='tar_file.tar') + download_mock.return_value = temp_file + + isla = IntegralClass() + mock_instrument_bands(isla_module=isla) + st_timeseries_list_extracted = isla.get_short_term_timeseries(target_name='target', + band='b1', epoch='time') + assert len(st_timeseries_list_extracted) == 3 + + st_timeseries_list_compressed = isla.get_short_term_timeseries(target_name='target', + band='b1', epoch='time', read_fits=False) + assert type(st_timeseries_list_compressed) is str + + close_files(st_timeseries_list_extracted) + temp_path.cleanup() + + @patch('astroquery.esa.integral.core.pyvo.dal.TAPService.capabilities', []) + @patch('astroquery.esa.integral.core.log') + @patch('astroquery.esa.utils.utils.execute_servlet_request') + @patch('astroquery.esa.integral.core.IntegralClass.get_epochs') + @patch('astroquery.esa.integral.core.IntegralClass.get_instrument_band_map') + def test_get_spectra_error_server(self, instrument_band_mock, epoch_mock, servlet_mock, log_mock): + instrument_band_mock.return_value = mocks.get_instrument_bands() + epoch_mock.return_value = {'epoch': ['time']} + error_message = 'Error' + servlet_mock.side_effect = HTTPError(error_message) + + isla = IntegralClass() + mock_instrument_bands(isla_module=isla) + isla.get_spectra(target_name='target', + band='b1', epoch='time') + log_mock.error.assert_called_with('Problem when retrieving spectra. ' + 'Error') + + @patch('astroquery.esa.integral.core.pyvo.dal.TAPService.capabilities', []) + @patch('astroquery.esa.integral.core.log') + @patch('astroquery.esa.utils.utils.execute_servlet_request') + @patch('astroquery.esa.integral.core.IntegralClass.get_epochs') + @patch('astroquery.esa.integral.core.IntegralClass.get_instrument_band_map') + def test_get_spectra_no_values(self, instrument_band_mock, epoch_mock, servlet_mock, log_mock): + instrument_band_mock.return_value = mocks.get_instrument_bands() + epoch_mock.return_value = {'epoch': ['time']} + servlet_mock.return_value = [] + + isla = IntegralClass() + mock_instrument_bands(isla_module=isla) + isla.get_spectra(target_name='target', + band='b1', epoch='time') + log_mock.error.assert_called_with('Spectra are not available with these inputs. ' + 'Please try with different input parameters.') + + @patch('astroquery.esa.integral.core.pyvo.dal.TAPService.capabilities', []) + @patch('astroquery.esa.integral.core.IntegralClass.get_epochs') + @patch('astroquery.esa.integral.core.log') + @patch('astroquery.esa.utils.utils.execute_servlet_request') + @patch('astroquery.esa.integral.core.IntegralClass.get_instrument_band_map') + def test_get_spectra_exception(self, instrument_band_mock, servlet_mock, log_mock, epoch_mock): + instrument_band_mock.return_value = mocks.get_instrument_bands() + epoch_mock.return_value = {'epoch': ['time']} + mock_response = mocks.get_mock_response() + servlet_mock.side_effect = mock_response + + isla = IntegralClass() + mock_instrument_bands(isla_module=isla) + isla.get_spectra(target_name='target', + band='b1', epoch='time') + log_mock.error.assert_called_with("Problem when retrieving spectra. " + "object of type 'Mock' has no len()") + + @patch('astroquery.esa.integral.core.pyvo.dal.TAPService.capabilities', []) + @patch('astroquery.esa.utils.utils.download_file') + @patch('astroquery.esa.integral.core.IntegralClass.get_epochs') + @patch('astroquery.esa.utils.utils.execute_servlet_request') + @patch('astroquery.esa.integral.core.IntegralClass.get_instrument_band_map') + def test_get_spectra(self, instrument_band_mock, servlet_mock, epoch_mock, download_mock): + instrument_band_mock.return_value = mocks.get_instrument_bands() + servlet_mock.return_value = mocks.get_mock_spectra() + epoch_mock.return_value = {'epoch': ['today']} + temp_path = create_temp_folder() + temp_file = copy_to_temporal_path(data_path=data_path('tar_file.tar'), temp_folder=temp_path, + filename='tar_file.tar') + download_mock.return_value = temp_file + + isla = IntegralClass() + mock_instrument_bands(isla_module=isla) + spectra_list_extracted = isla.get_spectra(target_name='target', + epoch='today', band='b1') + assert len(spectra_list_extracted) == 3 + + spectra_list_compressed = isla.get_spectra(target_name='target', + epoch='today', band='b1', read_fits=False) + assert type(spectra_list_compressed) is list + + close_files(spectra_list_extracted) + temp_path.cleanup() + + @patch('astroquery.esa.integral.core.pyvo.dal.TAPService.capabilities', []) + @patch('astroquery.esa.integral.core.log') + @patch('astroquery.esa.utils.utils.execute_servlet_request') + @patch('astroquery.esa.integral.core.IntegralClass.get_epochs') + @patch('astroquery.esa.integral.core.IntegralClass.get_instrument_band_map') + def test_get_mosaic_no_values(self, instrument_band_mock, epoch_mock, servlet_mock, log_mock): + instrument_band_mock.return_value = mocks.get_instrument_bands() + epoch_mock.return_value = {'epoch': ['time']} + servlet_mock.return_value = [] + + isla = IntegralClass() + mock_instrument_bands(isla_module=isla) + isla.get_mosaic(epoch='time', instrument='i1') + log_mock.error.assert_called_with('Mosaics are not available for these inputs. ' + 'Please try with different input parameters.') + + @patch('astroquery.esa.integral.core.pyvo.dal.TAPService.capabilities', []) + @patch('astroquery.esa.integral.core.IntegralClass.get_epochs') + @patch('astroquery.esa.integral.core.log') + @patch('astroquery.esa.utils.utils.execute_servlet_request') + @patch('astroquery.esa.integral.core.IntegralClass.get_instrument_band_map') + def test_get_mosaic_exception(self, instrument_band_mock, servlet_mock, log_mock, epoch_mock): + instrument_band_mock.return_value = mocks.get_instrument_bands() + epoch_mock.return_value = {'epoch': ['time']} + error_message = 'Error' + servlet_mock.side_effect = HTTPError(error_message) + + isla = IntegralClass() + mock_instrument_bands(isla_module=isla) + isla.get_mosaic(epoch='time', instrument='i1') + log_mock.error.assert_called_with("Problem when retrieving mosaics. " + "Error") + + @patch('astroquery.esa.integral.core.pyvo.dal.TAPService.capabilities', []) + @patch('astroquery.esa.utils.utils.download_file') + @patch('astroquery.esa.integral.core.IntegralClass.get_epochs') + @patch('astroquery.esa.utils.utils.execute_servlet_request') + @patch('astroquery.esa.integral.core.IntegralClass.get_instrument_band_map') + def test_get_mosaic(self, instrument_band_mock, servlet_mock, epoch_mock, download_mock): + instrument_band_mock.return_value = mocks.get_instrument_bands() + servlet_mock.return_value = mocks.get_mock_mosaic() + epoch_mock.return_value = {'epoch': ['today']} + temp_path = create_temp_folder() + temp_file = copy_to_temporal_path(data_path=data_path('tar_gz_file.gz'), temp_folder=temp_path, + filename='tar_gz_file.gz') + download_mock.return_value = temp_file + + isla = IntegralClass() + mock_instrument_bands(isla_module=isla) + mosaics_extracted = isla.get_mosaic(epoch='today', instrument='i1') + assert len(mosaics_extracted) == 2 + + mosaics_compressed = isla.get_mosaic(epoch='today', instrument='i1', read_fits=False) + + assert type(mosaics_compressed) is list + close_files(mosaics_extracted) + temp_path.cleanup() + + @patch('astroquery.esa.integral.core.pyvo.dal.TAPService.capabilities', []) + @patch('astroquery.esa.utils.utils.execute_servlet_request') + @patch('astroquery.esa.integral.core.IntegralClass.get_instrument_band_map') + def test_get_source_metadata(self, instrument_band_mock, servlet_mock): + instrument_band_mock.return_value = mocks.get_instrument_bands() + servlet_mock.return_value = {} + + isla = IntegralClass() + mock_instrument_bands(isla_module=isla) + isla.get_source_metadata(target_name='test') + + args, kwargs = servlet_mock.call_args + assert kwargs['query_params']['REQUEST'] == 'sources' diff --git a/astroquery/esa/utils/__init__.py b/astroquery/esa/utils/__init__.py index e1a2346afc..e5b27d922a 100644 --- a/astroquery/esa/utils/__init__.py +++ b/astroquery/esa/utils/__init__.py @@ -1,8 +1,8 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst """ -========== -eHST Init -========== +========= +ESA Utils +========= European Space Astronomy Centre (ESAC) European Space Agency (ESA) diff --git a/astroquery/esa/utils/tests/__init__.py b/astroquery/esa/utils/tests/__init__.py new file mode 100644 index 0000000000..49603ec083 --- /dev/null +++ b/astroquery/esa/utils/tests/__init__.py @@ -0,0 +1,9 @@ +""" +================ +UTILS Tests Init +================ + +European Space Astronomy Centre (ESAC) +European Space Agency (ESA) + +""" diff --git a/astroquery/esa/utils/tests/data/tar_file.tar b/astroquery/esa/utils/tests/data/tar_file.tar new file mode 100644 index 0000000000..aef8212611 Binary files /dev/null and b/astroquery/esa/utils/tests/data/tar_file.tar differ diff --git a/astroquery/esa/utils/tests/data/tar_gz_file.tar.gz b/astroquery/esa/utils/tests/data/tar_gz_file.tar.gz new file mode 100644 index 0000000000..5f251493ca Binary files /dev/null and b/astroquery/esa/utils/tests/data/tar_gz_file.tar.gz differ diff --git a/astroquery/esa/utils/tests/data/test.fits b/astroquery/esa/utils/tests/data/test.fits new file mode 100644 index 0000000000..c6c15c1da4 --- /dev/null +++ b/astroquery/esa/utils/tests/data/test.fits @@ -0,0 +1,21 @@ +SIMPLE = T / conforms to FITS standard BITPIX = 8 / array data type NAXIS = 0 / number of array dimensions EXTEND = T END XTENSION= 'BINTABLE' / binary table extension BITPIX = 8 / array data type NAXIS = 2 / number of array dimensions NAXIS1 = 12 / length of dimension 1 NAXIS2 = 863 / length of dimension 2 PCOUNT = 0 / number of group parameters GCOUNT = 1 / number of groups TFIELDS = 3 / number of table fields TTYPE1 = 'TIME ' TFORM1 = 'E ' TTYPE2 = 'RATE ' TFORM2 = 'E ' TTYPE3 = 'ERROR ' TFORM3 = 'E ' EXTNAME = 'TIMESERIES' / extension name IN_1 = '1A_1742-289_JEMX1_0535_04300000002_03_20_timeseries.fits' IN_2 = '1A_1742-289_JEMX1_0545_04200010038_03_20_timeseries.fits' IN_3 = '1A_1742-289_JEMX1_0545_04200620001_03_20_timeseries.fits' IN_4 = '1A_1742-289_JEMX1_0601_05300000001_03_20_timeseries.fits' IN_5 = '1A_1742-289_JEMX1_0604_05300000001_03_20_timeseries.fits' IN_6 = '1A_1742-289_JEMX1_0661_05300000002_03_20_timeseries.fits' IN_7 = '1A_1742-289_JEMX1_0667_05200010028_03_20_timeseries.fits' IN_8 = '1A_1742-289_JEMX1_1326_09200250020_03_20_timeseries.fits' IN_9 = '1A_1742-289_JEMX1_1333_10200210007_03_20_timeseries.fits' IN_10 = '1A_1742-289_JEMX1_1334_10200210008_03_20_timeseries.fits' IN_11 = '1A_1742-289_JEMX1_1334_10200210009_03_20_timeseries.fits' IN_12 = '1A_1742-289_JEMX1_1337_10200210014_03_20_timeseries.fits' IN_13 = '1A_1742-289_JEMX1_1337_10200210015_03_20_timeseries.fits' IN_14 = '1A_1742-289_JEMX1_1341_10200210017_03_20_timeseries.fits' IN_15 = '1A_1742-289_JEMX1_1343_10200210018_03_20_timeseries.fits' IN_16 = '1A_1742-289_JEMX1_1510_12200010003_03_20_timeseries.fits' IN_17 = '1A_1742-289_JEMX1_1514_11200270047_03_20_timeseries.fits' IN_18 = '1A_1742-289_JEMX1_1514_11200270048_03_20_timeseries.fits' IN_19 = '1A_1742-289_JEMX1_1514_12200010007_03_20_timeseries.fits' IN_20 = '1A_1742-289_JEMX1_1514_11200270049_03_20_timeseries.fits' IN_21 = '1A_1742-289_JEMX1_1517_12200010008_03_20_timeseries.fits' IN_22 = '1A_1742-289_JEMX1_1517_11200050004_03_20_timeseries.fits' IN_23 = '1A_1742-289_JEMX1_1521_11200050006_03_20_timeseries.fits' IN_24 = '1A_1742-289_JEMX1_1522_12200090002_03_20_timeseries.fits' IN_25 = '1A_1742-289_JEMX1_1532_11200050019_03_20_timeseries.fits' IN_26 = '1A_1742-289_JEMX1_1580_12200010017_03_20_timeseries.fits' IN_27 = '1A_1742-289_JEMX1_1580_12200090007_03_20_timeseries.fits' IN_28 = '1A_1742-289_JEMX1_1582_12200090009_03_20_timeseries.fits' IN_29 = '1A_1742-289_JEMX1_1583_12200090009_03_20_timeseries.fits' IN_30 = '1A_1742-289_JEMX1_1583_12200010019_03_20_timeseries.fits' IN_31 = '1A_1742-289_JEMX1_1584_12200090010_03_20_timeseries.fits' IN_32 = '1A_1742-289_JEMX1_1590_12200010024_03_20_timeseries.fits' IN_33 = '1A_1742-289_JEMX1_1591_12200090019_03_20_timeseries.fits' IN_34 = '1A_1742-289_JEMX1_1592_11200050016_03_20_timeseries.fits' IN_35 = '1A_1742-289_JEMX1_1592_11200050017_03_20_timeseries.fits' IN_36 = '1A_1742-289_JEMX1_1594_11200050021_03_20_timeseries.fits' IN_37 = '1A_1742-289_JEMX1_1595_11200050022_03_20_timeseries.fits' IN_38 = '1A_1742-289_JEMX1_1597_12200010029_03_20_timeseries.fits' IN_39 = '1A_1742-289_JEMX1_1597_11200050023_03_20_timeseries.fits' IN_40 = '1A_1742-289_JEMX1_1645_12200090023_03_20_timeseries.fits' IN_41 = '1A_1742-289_JEMX1_1649_12200090027_03_20_timeseries.fits' IN_42 = '1A_1742-289_JEMX1_1650_12200090028_03_20_timeseries.fits' IN_43 = '1A_1742-289_JEMX1_1650_13200010005_03_20_timeseries.fits' IN_44 = '1A_1742-289_JEMX1_1650_12200090030_03_20_timeseries.fits' IN_45 = '1A_1742-289_JEMX1_1650_12200090031_03_20_timeseries.fits' IN_46 = '1A_1742-289_JEMX1_1652_13200010006_03_20_timeseries.fits' IN_47 = '1A_1742-289_JEMX1_1652_12200090014_03_20_timeseries.fits' IN_48 = '1A_1742-289_JEMX1_1652_12200090032_03_20_timeseries.fits' IN_49 = '1A_1742-289_JEMX1_1653_12200090033_03_20_timeseries.fits' IN_50 = '1A_1742-289_JEMX1_1653_12200090034_03_20_timeseries.fits' NAME = '1A 1742-289' REVFIRST= 535 REVLAST = 1653 BACKAPP = T CHANTYPE= 'PI ' CONFIGUR= 'osa_2021-08-27T14:27:00' CREATOR = 'lc_pick 3.4.3' DEADAPP = T DEC_OBJ = -29.02564 EQUINOX = 2000.0 EUNIT = 'keV ' EXTVER = 1 E_MAX = 19.85 E_MIN = 3.08 GRPID1 = 1 INSTRUME= 'JMX1 ' ISDCLEVL= 'LCR ' MJDREF = 51544.0 ORIGIN = 'ISDC ' RADECSYS= 'FK5 ' RA_OBJ = 266.4013 STAMP = '2023-05-11T18:19:48 lc_pick 3.4.3' SW_TYPE = 'POINTING' TELESCOP= 'INTEGRAL' TIMEPIXR= 0.5 TIMEREF = 'LOCAL ' TIMESYS = 'TT ' TIMEUNIT= 'd ' VIGNAPP = T TFIRST = 2617.037037044936 TSTART = 2617.037037044936 TSTOP = 5911.423611122315 TLAST = 5911.423611122315 COMMENT Generated by ISLA post-processing S/W version 0.1 END E#A.!@E#@`?N.E#>@K?JCE#@C?E#AZ?>E#A>|@G9E#Q@|S?1iE#@6,?"E#@fZ?m]E#m??5@E#?|?($E#@vs?;dE#+@?@E#AS?E#Q@_ @cE#A@VE#@}IR?,ZE#@P?,6E#m@)?W?E#@P?E#A #?nE#A 8?E%hZ@6?^VE%h@/?!E%i?$ ?@|E%iw@$?=E%i@k?'lE%j4@ciD?E%y4As@E%ydA"@jE%zQAĜ?/E%z@Q?Ɍ~E%{mA1'?RE%{A,?%E%{@u?:E%|+@?<6E%|@G ?IE%|?ѷ?EE%|@4?I(E%}@Y?IJE%}wA?(E%}@>?E%~d@2bE%~4@7E%~@2b?E%~A1?E%!@g ?VE%Q@aG?WE%@12?E%@6Ow?YE%>@;?E%m?!?/E%@)_?vE%@T?E%ZA x?[E%AfA!@E%@~r@C-E%Ag x@[E%mAG?AE%A?RE%A!!?ƨE%+A:@ME%ZA>c@ +E%G@@0E%w@첖@07E%Az?/E%@P?h +E%dA 6?#E%@Q?|?E%A$v@*JE%!A6@+]E%BAeE%>AߥA]E%A#U?0E%A@?)E%A?E%+@ȴ?E%@?'8E%@P?(UE%?˓?3SE%G@VB[?3|E%@RT?E%@?}VE%4@V?75E%d@?/E%?Q?F E%>?FE%!?K?oE%Q?3E?NE/A>+?DE/AC-?:E/A:^@|E/A$@E/A x@UE/A~@@NE/ZA2ی?E/ALM?E/@3? E/AD@E/GA%l@E/dA"?E/A A ?LE/AP?րE/!@?DE/Q@?}?IJE/@?aE/@-?c9E/@}?aE/m@G??E/@?E/+@dZ@E/Z@k@7LE/A#?E/@Z?&E/@۷?:E/w>Q?P E/>?PE/@$?+E/4??g8E/d@D?=E/@҈?*E/@Z?%8E/@i?E/@!?E/A:@3E/>A-w@E/mAb@E/@?EE/@˒?GE/Z@]d?)E/@J?*E/@?'eE/G@ŭC?T yE/w@7?R E/@M?QE/AN@ QE/4A@E/dAcG@ E/QAL;?^E/A01?E/AA}V?7E/A-?XE/>@?E/@҉?яE/@j?eE/@S?E/GA<6?E/A a?gE/A?E/A&?#E/dA]d?{E/AO5?ʙ1E/A=@HE/Ay@E/AS@)E/m@&?M}E/@9X?MOE/@X[?#E/+@=-?$E/Z@OiD?$E/@?)E/wAU8@ + E/ANO@ E/A?@ZE04@oƧ?1E0d@?WE0@n?+:E0!@?/v`E0?8?E0? +?&E0@?fE0>?Rh +?QE0m?;?QE0@9?[XE0A&P?E0+@4?E0=w@E0A?}@ \E0A[W@ԕE0wA?SE0@e,?DE0@o?\E04@?[(E0d@}?YE0@/?IXE0@'?I~E0QA&^?J#E0A'?xlE0A?+E0@?E0@@DgE0Z@X?E0AW?E0A ?_E0r@GS?(QE0s@p?*6E0s4@Nl?(DE0s@?GE0s@U?CE0tQ?Ќ?5=E0t?M?2E0t@{O ?8E0u@T?|E0u>@Z?OE0u?o@/E9E0u@@5*0E0v+@ ?!E0vZ@Y?E0v@Zv?sE0v@8?9*0E0w@L?:0VE0ww@̚?I0E0w@?FE0w@p?JE0x4A.@mE0xdA>C@tjE0xA@ofE0y@I_?7KE0y=?+E0y߿a?GEE0z>@?VE0zmA?6E0zAI@!YE0{+A@+E0{٤?@8E0{?Mj@HE0|AtA@nlE0}A0@E0}4Al@E0}A!?=pE0}A ?\E0~QA$@[E0~A/@xE0ALd@E0Ae'S@VE0+@e?KƨE0Z@6z?F +E0@qv?JE0w?+?E0@#@N?E0@@I?kE04@@oj@64E0d@V@ME0@ח%?NE0!@l?E0>?@IE0@,q ?G_pE0@ r?E`BE0>@S?E0m@17?E0+@~ں?E0Z@xz?E0@? xE0@\?E0wAixl@2sE0AWں@1E0A@+n.E04@?LcE0d@?LE0@?'E0@z?&sE0!@Y?%YE0@l:?HE0@Ad?HQE0>A ?E0mA?PE0Am?;E0A%? \E0A>v?,E0GA qu?E0w@N? xE0A%$@E0A/Z@CE0@ں?;E0@?jE0>@?h +E0m@?E0@)?:E0+@3?Z6E0Z@ ?XیE0A?j-E0A ?k"E0Au?kdZE0wA\@1E0AyE@.&E0A???qE04@?E0d@m?E0@?GE0@}?EXE0@> ?E0@s{I?=E0@XE?CE0>@?1E0m@?1nE0@J?23E0@@E0+@]d@DgE0A ?A E0A&?oiE0G@&?-E0w@ ?-\E0@ ?,E0@?+(E04@5?(XyE0@"h?sE0@K?E0@?E0Q?#?דE0?:?WE0;@ lE0>A'LAۋE;@V?6$E;@e?5"E;m@&"?)_E;@N?*CE;@Zi?*͟E; +@z`?lq E; Z@N?oE; AFߤ?)_E; A?/E;!GAd@!E;!wA@"E;!AQ@*vE;"A"/?DE;"4A?rE;"A +r?q|E;"@{?pEE;#!A +?U3E;#QA+?c E;#A N?eE;#A@?Ƶ E;%ZAT]@=E;%A_ @!E;%A ?-E;&AP?E;&GACN?ME;&A@SE;&A@E;'A@E;(!A@h +E;(Aua?E;(Av-?E;)@H?>NE;)>@H?7YE;)@+?XQE;)@o\?VE;*ZA ?]cE;*A J?ԄE;*@yQ?MOE;+@*?RE;+?!&?:E;+??ƨE;,@H?E;,4@1d?BE;-@ʕ?U2E;-@B\?@E;.>Av_@QE;.mAl@^~E;.A@VE;.A;ANE;/Z@?e,E;/@@ E;/@kt@NE;0@wQ?QE;0G@{?E;0@j?5F E;0@?1GE;1@j +?5LE;1@ \?e"E;1@?em]E;2!@6 @E;2Q@`@ ?}E;2A>?jE;2A3#:?"E;3mA@%E;3Aн@,dE;3@A0 ?E;8mADg?HE;8AM?sE;8@0U?9 E;9+@-?9=E;9Z@{?;E;9@+k?SE;9@7?E;=@LD?8_E;=m?+@$ ?XK^E;>Z@?Y=E;>A5?mE;>A1?jE;?G@J?CEE;?w@e8?@eE;?@Dۋ?V_E;@@?IE;@4@?0E<[Z@9.?(E<\G@d|?F]E<\@f?;:E<]@?2@E<]d@/?+CE(A(@mEXAd?1EğAI@EbE\AU{@CaEŌA?AEŻ@h?3EA!vEa2@7/?^5EaaA7?Ea@t?,(Ea?SF>Ea?5>خEb@A? Eb7@O?hEua@='?OEu?Eu@|/?0Eu@?xeEzA' ?wE{@qu?7E{N?>]dE{~@J&?" IE{ARی?7E{A?3EfAlV?E2@?oHEa@D?+E??E@q?4EA^?E @|z?/E N?ě?E ~@?!uE @Xz?peETXA] ?CET@h?2&ET?j~?sET@xl?!4EU@?sEZ\@st?)ZEZ@0r?E??uE @u?0AE;@K?YcEk@ l?m}EК@q?!GE@F? cE#@/?E$A;s?E$N?"u%@>OvE$A#2?E%$@t?wE%S@x ?vE%A:B[?4E%@?{=E%@?E&X@f?;E&;d@@E&AP?ϓE'@?BE'E@)? E'tAM?ѷE'Ad&@E'A"ں?VmE(@aw2?:0VE(?t?_E)@&?PE*$@h?f'E*S?-?E*@?dZE*A +?3E*Ak?kE+A@ nE,EA@E,AYzx@E3@.?KE4$@ҷ?JxlE4S@Ի?&ȴE4@X?U+E4A@0UE5AU@:Ed@?Ed2@;A? +EdI@3?ĜEdy@?+Ed@ ?@|Ed@R4?HEhEA?O;dEht@s?SEh??NEh@n/?1 [EiA`? EiI@4n?3{JEiy@pPH?Ei@lL?4EiAt?J)E$AB?f?ES@nu&?33E@PH?E@?.EA??zE@@?1@Eo@ +2>6E@g4?UE@r?bsE@?*~ENA#@@6EݭA r?cE@?(9E Ac?kPEAA7@EA> ?5E@@\?eEo@{J?WlE߇@u?R]EA?GE@yq?OE ?]d?A E;@xn?9ES@.H?6Ek@g4?81'EAd?lE@0Ĝ?1hsE@G?E(@D|?,EXAr?`bNE7@T ? <6Ef@?`E@?&E@FE? BE@`Ѹ? w2E$???AES@ ?HQEAK?<6EA@֟E@A&?L6Eo@ @?YE@c?xE@ف?T yE>?`E-@9X?,VE\@F? EA 5?.E2A#B?NvEa@?EA ?EڇA+?&EڟA6? +EADE?wEA'??E@h?? E@?d[Et@M(?rEAN?OE2A,?lE@n?,?E@Z0V? E@Oa?E-@z?*)E\@?7E@D6?)0E$A@)ESA0`A?$EA8v`?gKE@Ǚ?E@e%?EA +D?:EA>@)^E@?Cn/EE@9?Et@vs?$jE@ezy?#tEwa@57?EwAK?|EwA?~($Ew@+?2Ex@;?ExN@S?Exf@?oEx~??Ex??Ex@Q?q|EA@حE@1?E@y?LE@fZ?+E?w>+jE@?#E$A<6?oiEESA(=?ooE@)?$E??>*E@?+E@ ?&$E@N?(1'E@A$u%?XEo@IR?2u&E@S&?/E@x?,EA'Ow?,E7A2?}Ef@6?J#E͕?hs> +>E???$OEA9q?HE$@}?2:*ES?V>E΂@>E2@~?\)EaA_?? E@7 ? [E?>hE@?E7@>?jE@ q >?E@h?K/E@?&+E??#FE7@҉?$zEN@p;?"TaEf@ߤ?$mE@4m?2u&EtA=>?ykE@?(E?><6E@V?!GE2A+?s{IEa@]?&&E@>_E@?>EA*?SE@?Ea@l#@maEq\@:?R]Eq?H?=DEq@l?QGEqAW? lErA?UErI@mV?E@A?EfA@E~@e@5E@Fs?&]E@d?SE@u?Z0VEE@ ?D`Et@p-?/HEA8?E@?IrGE2@?LEaAc@N?%EA?wE7@y?*WEf@?E@?UoEAA@tC-EARXy?,=E(A) +?mEXA?ɆEA9#@V{EAZ@EaAO? EA U?P)EA ?'RE@ @ENA(@E~@o?q E$@)?-ES@?;"E@,?(uEA ^?vEEAo@zE@AR?AEoAC?WEA9?EAq@EA[t@uEAh&@.EAL@tEt@s?OO EA0}?n;EAj@ojEaA|#?˓E@g ?2ZE@_?E@??E@Լk?:uEAGc?4E$A-e@'ESA]?kQEA4i?aEA>ݘ?ZEA/@YEA7C?BE-ANU3?f1E\A"33?ˋEAY@pE@ѳ?EE@+?&8EI@-?YCEyA'?E@c?:ENACԕ@3EfA;@?PE~Aa@7oEA2v@$ EA!?E @?JQEkA?T/EAo?ڬEAnH@E(Ay:@oEXAo@ \ No newline at end of file diff --git a/astroquery/esa/utils/tests/data/zip_file.zip b/astroquery/esa/utils/tests/data/zip_file.zip new file mode 100644 index 0000000000..29afbffde6 Binary files /dev/null and b/astroquery/esa/utils/tests/data/zip_file.zip differ diff --git a/astroquery/esa/utils/tests/setup_package.py b/astroquery/esa/utils/tests/setup_package.py new file mode 100644 index 0000000000..291bc087e3 --- /dev/null +++ b/astroquery/esa/utils/tests/setup_package.py @@ -0,0 +1,18 @@ +# Licensed under a 3-clause BSD style license - see LICENSE.rst +import os + + +# setup paths to the test data +# can specify a single file or a list of files +def get_package_data(): + paths = [os.path.join('data', '*.vot'), + os.path.join('data', '*.xml'), + os.path.join('data', '*.zip'), + os.path.join('data', '*.gz'), + os.path.join('data', '*.tar'), + os.path.join('data', '*.fits'), + os.path.join('data', '*.txt'), + ] # etc, add other extensions + # you can also enlist files individually by names + # finally construct and return a dict for the sub module + return {'astroquery.esa.utils.tests': paths} diff --git a/astroquery/esa/utils/tests/test_utils.py b/astroquery/esa/utils/tests/test_utils.py new file mode 100644 index 0000000000..c0e15bf427 --- /dev/null +++ b/astroquery/esa/utils/tests/test_utils.py @@ -0,0 +1,291 @@ +# Licensed under a 3-clause BSD style license - see LICENSE.rst +""" +=============== +ESA UTILS tests +=============== + +European Space Astronomy Centre (ESAC) +European Space Agency (ESA) + +""" +import os.path +import shutil +import tempfile +from unittest.mock import patch, Mock + +import astroquery.esa.utils.utils as esautils +from astropy.io.registry import IORegistryError +from astropy.table import Table +from requests import HTTPError +from astropy import units as u + +import pytest + + +def get_dummy_table(): + data = { + "name": ['Crab'], + "ra": [83.63320922851562], + "dec": [22.01447105407715], + } + + # Create an Astropy Table with the mock data + return Table(data) + + +def get_iter_content_mock(): + test_file_content = b"Mocked file content." + for i in range(0, len(test_file_content), 8192): + yield test_file_content[i:i + 8192] + + +def get_dummy_json(): + return {"resolver": "Test", "objects": [{"decDegrees": 22.0174, "raDegrees": 83.6324}]} + + +def data_path(filename): + data_dir = os.path.join(os.path.dirname(__file__), 'data') + return os.path.join(data_dir, filename) + + +def close_file(file): + file.close() + + +def close_files(file_list): + for file in file_list: + close_file(file['fits']) + + +def create_temp_folder(): + return tempfile.TemporaryDirectory() + + +def copy_to_temporal_path(data_path, temp_folder, filename): + temp_data_dir = os.path.join(temp_folder.name, filename) + shutil.copy(data_path, temp_data_dir) + return temp_data_dir + + +class TestIntegralTap: + + @patch('pyvo.auth.authsession.AuthSession._request') + def test_esa_auth_session_url(self, mock_get): + mock_get.return_value = {} + + esa_session = esautils.ESAAuthSession() + esa_session._request('GET', 'https://dummy.com/service') + + mock_get.assert_called_once_with('GET', 'https://dummy.com/service', + params={'TAPCLIENT': 'ASTROQUERY', 'format': 'votable_plain'}) + + @patch('pyvo.auth.authsession.AuthSession.post') + def test_login_success(self, mock_post): + + mock_response = Mock() + mock_response.raise_for_status.return_value = None # Simulate no HTTP error + mock_response.json.return_value = {"status": "success", "token": "mocked_token"} + + # Configure the mock post method to return the mock Response + mock_post.return_value = mock_response + + esa_session = esautils.ESAAuthSession() + esa_session.login(login_url='https://dummy.com/login', user='dummyUser', password='dummyPassword') + + mock_post.assert_called_once_with(url='https://dummy.com/login', + data={'username': 'dummyUser', 'password': 'dummyPassword'}, + headers={'Content-type': 'application/x-www-form-urlencoded', + 'Accept': 'text/plain'}) + + @patch('pyvo.auth.authsession.AuthSession.post') + def test_login_error(self, mock_post): + error_message = "Mocked HTTP error" + mock_post.side_effect = HTTPError(error_message) + + with pytest.raises(HTTPError) as err: + esa_session = esautils.ESAAuthSession() + esa_session.login(login_url='https://dummy.com/login', user='dummyUser', password='dummyPassword') + assert error_message in err.value.args[0] + + @patch('pyvo.auth.authsession.AuthSession.post') + def test_logout_success(self, mock_post): + mock_post.raise_for_status.return_value = None # Simulate no HTTP error + mock_post.json.return_value = {"status": "success", "token": "mocked_token"} + + esa_session = esautils.ESAAuthSession() + esa_session.logout(logout_url='https://dummy.com/logout') + + mock_post.assert_called_once_with(url='https://dummy.com/logout', + headers={'Content-type': 'application/x-www-form-urlencoded', + 'Accept': 'text/plain'}) + + @patch('pyvo.auth.authsession.AuthSession.post') + def test_logout_error(self, mock_post): + error_message = "Mocked HTTP error" + mock_post.side_effect = HTTPError(error_message) + + esa_session = esautils.ESAAuthSession() + with pytest.raises(HTTPError) as err: + esa_session.logout(logout_url='https://dummy.com/logout') + assert error_message in err.value.args[0] + + def test_get_degree_radius(self): + assert esautils.get_degree_radius(12.0) == 12.0 + assert esautils.get_degree_radius(12) == 12.0 + assert esautils.get_degree_radius(30 * u.arcmin) == 0.5 + + def test_download_table(self, tmp_cwd): + dummy_table = get_dummy_table() + filename = 'test.votable' + esautils.download_table(dummy_table, output_file=filename, output_format='votable') + assert os.path.exists(filename) + + with pytest.raises(IORegistryError) as err: + esautils.download_table(dummy_table, output_file=filename) + assert 'Format could not be identified' in err.value.args[0] + + @patch('astroquery.esa.integral.core.pyvo.dal.TAPService.capabilities', []) + @patch('astroquery.esa.integral.core.pyvo.dal.TAPService') + def test_execute_servlet_request(self, mock_tap): + + mock_tap._session.get.raise_for_status.return_value = None + mock_tap._session.get.json.return_value = {"status": "success", "token": "mocked_token"} + + query_params = {'test': 'dummy'} + esautils.execute_servlet_request(url='https://dummyurl.com/service', tap=mock_tap, query_params=query_params) + + mock_tap._session.get.assert_called_once_with(url='https://dummyurl.com/service', + params={'test': 'dummy', 'TAPCLIENT': 'ASTROQUERY'}) + + @patch('astroquery.esa.integral.core.pyvo.dal.TAPService.capabilities', []) + @patch('astroquery.esa.integral.core.pyvo.dal.TAPService') + def test_execute_servlet_request_error(self, mock_tap): + error_message = "Mocked HTTP error" + mock_tap._session.get.side_effect = HTTPError(error_message) + + query_params = {'test': 'dummy'} + + with pytest.raises(HTTPError) as err: + esautils.execute_servlet_request(url='https://dummyurl.com/service', + tap=mock_tap, query_params=query_params) + assert error_message in err.value.args[0] + + @patch("pyvo.auth.authsession.AuthSession.get") + def test_download_local(self, mock_get, tmp_cwd): + iter_content_mock = get_iter_content_mock() + mock_response = Mock() + mock_response.iter_content.side_effect = iter_content_mock + mock_response.json.return_value = iter_content_mock + mock_response.raise_for_status.return_value = None + + mock_get.iter_content.return_value = mock_response + + esa_session = esautils.ESAAuthSession() + + filename = 'test_file.fits' + esautils.download_file(url='http://dummyurl.com/download', session=esa_session, + params={'file': filename}, filename=filename) + + mock_get.assert_called_once_with('http://dummyurl.com/download', stream=True, + params={'file': filename, 'TAPCLIENT': 'ASTROQUERY'}) + + assert os.path.exists(filename) + + @patch("pyvo.auth.authsession.AuthSession.get") + def test_download_cache(self, mock_get, tmp_cwd): + iter_content_mock = get_iter_content_mock() + mock_response = Mock() + mock_response.iter_content.side_effect = iter_content_mock + mock_response.json.return_value = iter_content_mock + mock_response.raise_for_status.return_value = None + + mock_get.iter_content.return_value = mock_response + + esa_session = esautils.ESAAuthSession() + + filename = 'test_file2.fits' + cache_folder = './cache/' + esautils.download_file(url='http://dummyurl.com/download', session=esa_session, + params={'file': filename}, filename=filename, + cache=True, cache_folder=cache_folder) + + mock_get.assert_called_once_with('http://dummyurl.com/download', stream=True, + params={'file': filename, 'TAPCLIENT': 'ASTROQUERY'}) + + assert not os.path.exists(filename) + assert os.path.exists(esautils.get_cache_filepath(filename=filename, cache_path=cache_folder)) + + def test_read_tar(self): + temp_path = create_temp_folder() + tar_file = copy_to_temporal_path(data_path=data_path('tar_file.tar'), temp_folder=temp_path, + filename='tar_file.tar') + + files = esautils.read_downloaded_fits([tar_file]) + + assert len(files) == 1 + assert files[0]['filename'] == 'test.fits' + + close_files(files) + temp_path.cleanup() + + def test_read_tar_gz(self): + temp_path = create_temp_folder() + tar_gz_file = copy_to_temporal_path(data_path=data_path('tar_gz_file.tar.gz'), temp_folder=temp_path, + filename='tar_gz_file.tar.gz') + + files = esautils.read_downloaded_fits([tar_gz_file]) + + # Only 1 FITS file inside, the other file is a .txt file, so it should not be read + assert len(files) == 1 + assert files[0]['filename'] == 'test.fits' + + close_files(files) + temp_path.cleanup() + + def test_read_zip(self): + temp_path = create_temp_folder() + zip_file = copy_to_temporal_path(data_path=data_path('zip_file.zip'), temp_folder=temp_path, + filename='zip_file.zip') + + files = esautils.read_downloaded_fits([zip_file]) + + assert len(files) == 1 + assert files[0]['filename'] == 'test.fits' + + close_files(files) + temp_path.cleanup() + + def test_read_uncompressed(self, tmp_cwd): + uncompressed_file = data_path('test.fits') + + files = esautils.read_downloaded_fits([uncompressed_file]) + assert len(files) == 1 + assert files[0]['filename'] == 'test.fits' + + close_files(files) + + @patch('astroquery.esa.utils.utils.ESAAuthSession.get') + def test_resolve_target(self, mock_get): + mock_response = Mock() + mock_response.json.return_value = get_dummy_json() + mock_get.return_value.__enter__.return_value = mock_response + + esa_session = esautils.ESAAuthSession() + + target = esautils.resolve_target(url='http://dummyurl.com/target_resolver', session=esa_session, + target_name='dummy_target', target_resolver='ALL') + assert target.ra.degree == 83.6324 + assert target.dec.degree == 22.0174 + + @patch('astroquery.esa.utils.utils.ESAAuthSession.get') + def test_resolve_target_error(self, mock_get): + + error_message = "Mocked HTTP error" + mock_get.side_effect = ValueError(error_message) + + esa_session = esautils.ESAAuthSession() + + with pytest.raises(ValueError) as err: + esautils.resolve_target(url='http://dummyurl.com/target_resolver', session=esa_session, + target_name='dummy_target', target_resolver='ALL') + assert 'This target cannot be resolved' in err.value.args[0] diff --git a/astroquery/esa/utils/utils.py b/astroquery/esa/utils/utils.py index fe4ea65dc3..6cf0cf097b 100644 --- a/astroquery/esa/utils/utils.py +++ b/astroquery/esa/utils/utils.py @@ -7,11 +7,398 @@ European Space Agency (ESA) """ - +import datetime +import getpass import os +import binascii +import shutil + +import tarfile as esatar +import zipfile +from astropy import log +from astropy.coordinates import SkyCoord +from astropy import units as u + +from astropy.units import Quantity +from astropy.io import fits +from pyvo.auth.authsession import AuthSession + + +TARGET_RESOLVERS = ['ALL', 'SIMBAD', 'NED', 'VIZIER'] + + +# We do trust the ESA tar files, this is to avoid the new to Python 3.12 deprecation warning +# https://docs.python.org/3.12/library/tarfile.html#tarfile-extraction-filter +if hasattr(esatar, "fully_trusted_filter"): + esatar.TarFile.extraction_filter = staticmethod(esatar.fully_trusted_filter) + + +# Subclass AuthSession to customize requests +class ESAAuthSession(AuthSession): + """ + Session to login/logout an ESA TAP using PyVO + """ + + def __init__(self: str): + """ + Initialize the custom authentication session. + + Parameters: + login_url (str): The login endpoint URL. + """ + super().__init__() + + def login(self, login_url, *, user=None, password=None): + """ + Performs a login. + TAP+ only + User and password shall be used + + Parameters + ---------- + login_url: str, mandatory + URL to execute the login request + user : str, mandatory, default None + Username. If no value is provided, a prompt to type it will appear + password : str, mandatory, default None + User password. If no value is provided, a prompt to type it will appear + """ + + if user is None: + user = input("Username:") + if password is None: + password = getpass.getpass("Password:") + + if user and password: + args = { + "username": str(user), + "password": str(password)} + header = { + "Content-type": "application/x-www-form-urlencoded", + "Accept": "text/plain" + } + + try: + response = self.post(url=login_url, data=args, headers=header) + response.raise_for_status() + log.info('User has been logged successfully.') + except Exception as e: + log.error('Logging error: {}'.format(e)) + raise e + + def logout(self, logout_url): + """ + Performs a logout. + TAP+ only + User and password shall be used + + Parameters + ---------- + logout_url: str, mandatory + URL to execute the logout request + """ + header = { + "Content-type": "application/x-www-form-urlencoded", + "Accept": "text/plain" + } + + try: + response = self.post(url=logout_url, headers=header) + response.raise_for_status() + log.info('Logout executed successfully.') + except Exception as e: + log.error('Logout error: {}'.format(e)) + raise e + + def _request(self, method, url, *args, **kwargs): + """ + Intercept the request method and add TAPCLIENT=ASTROQUERY + + Parameters + ---------- + method: str, mandatory + method to be executed + url: str, mandatory + url for the request + + Returns + ------- + The request with the modified url + """ + + # Add the custom query parameter to the URL + additional_params = {'TAPCLIENT': 'ASTROQUERY', + 'format': 'votable_plain'} + if kwargs is not None and 'params' in kwargs: + kwargs['params'].update(additional_params) + elif kwargs is not None: + kwargs['params'] = additional_params + return super()._request(method, url, **kwargs) + + +def get_degree_radius(radius): + """ + Method to parse the radius and retrieve it in degrees + + Parameters + ---------- + radius: number or Quantity, mandatory + radius to be transformed to degrees + + Returns + ------- + The radius in degrees + """ + if radius is not None: + if isinstance(radius, Quantity): + return radius.to(u.deg).value + elif isinstance(radius, float): + return radius + elif isinstance(radius, int): + return float(radius) + raise ValueError('Radius must be either a Quantity or float value') + + +def download_table(astropy_table, output_file=None, output_format=None): + """ + Auxiliary method to download an astropy table + + Parameters + ---------- + astropy_table: Table, mandatory + Input Astropy Table + output_file: str, optional + File where the table will be saved + output_format: str, optional + Format of the file to be exported + """ + astropy_table.write(output_file, format=output_format, overwrite=True) + + +def execute_servlet_request(url, tap, *, query_params=None): + """ + Method to execute requests to the servlets on a server + + Parameters + ---------- + url: str, mandatory + Url of the servlet + tap: PyVO TAP, mandatory + TAP instance from where the session will be extracted + query_params: dict, optional + Parameters to be included in the request + + Returns + ------- + The request with the modified url + """ + + if 'TAPCLIENT' not in query_params: + query_params['TAPCLIENT'] = 'ASTROQUERY' + + # Use the TAPService session to perform a custom GET request + response = tap._session.get(url=url, params=query_params) + if response.status_code == 200: + return response.json() + else: + response.raise_for_status() + + +def download_file(url, session, *, params=None, path='', filename=None, cache=False, cache_folder=None, verbose=False): + """ + Download a file in streaming mode using an existing session + + Parameters + ---------- + url: str, mandatory + URL to be downloaded + session: ESAAuthSession, mandatory + session to download the file, including the cookies from ESA login + params: dict, optional + Additional params for the request + path: str, optional + Path where the file will be stored + filename: str, optional + filename to be given to the final file + cache: bool, optional, default False + flag to store the file in the Astroquery cache + cache_folder: str, optional + folder to store the cached file + verbose: boolean, optional, default False + Write the outputs in console + + Returns + ------- + The request with the modified url + """ + if params is None or len(params) == 0: + params = {} + if 'TAPCLIENT' not in params: + params['TAPCLIENT'] = 'ASTROQUERY' + with session.get(url, stream=True, params=params) as response: + response.raise_for_status() + + if filename is None: + content_disposition = response.headers.get('Content-Disposition') + if content_disposition: + filename = content_disposition.split('filename=')[-1].strip('"') + else: + filename = os.path.basename(url.split('?')[0]) + if cache: + filename = get_cache_filepath(filename, cache_folder) + path = '' + # Open a local file in binary write mode + if verbose: + log.info('Downloading: ' + filename) + file_path = os.path.join(path, filename) + with open(file_path, 'wb') as file: + for chunk in response.iter_content(chunk_size=8192): + file.write(chunk) + if verbose: + log.info(f"File {file_path} has been downloaded successfully") + return file_path + + +def get_cache_filepath(filename=None, cache_path=None): + """ + Stores the content from a response as an Astroquery cache object. + + Parameters: + response (requests.Response): + The HTTP response object with iterable content. + filename: str, optional + filename to be given to the final file + cache_filename (str, optional): + The desired filename in the cache. If None, a default name is used. + + Returns: + str: Path to the cached file. + """ + # Determine the cache path + cache_file_path = os.path.join(cache_path, filename) + # Create the cache directory if it doesn't exist + os.makedirs(cache_path, exist_ok=True) + + return cache_file_path + + +def read_downloaded_fits(files): + extracted_files = [] + for file in files: + extracted_files.extend(extract_file(file)) + + fits_files = [] + for file in extracted_files: + fits_file = safe_open_fits(file) + if fits_file: + fits_files.append({ + 'filename': os.path.basename(file), + 'path': file, + 'fits': fits_file + }) + + return fits_files + + +def safe_open_fits(file_path): + """ + Safely open a FITS file using astropy.io.fits. + + Parameters: + file_path: string + The path to the file to be opened. + + Returns: + fits.HDUList or None + Returns the HDUList object if the file is a valid FITS file, otherwise None. + """ + try: + hdu_list = fits.open(file_path) + return hdu_list + except (OSError, fits.VerifyError) as e: + print(f"Skipping file {file_path}: {e}") + return None + + +def extract_file(file_path, output_dir=None): + """ + Extracts a .tar, .tar.gz, or .zip file. If the file is in a different format, + returns the path of the original file. + + Parameters: + file_path (str): + Path to the archive file (.tar, .tar.gz, or .zip). + output_dir (str, optional): + Directory to store the extracted files. If None, a directory + with the same name as the archive file (minus the extension) + is created. + + Returns: + list: List of paths to the extracted files. + """ + if not output_dir: + output_dir = os.path.abspath(file_path) + if esatar.is_tarfile(file_path): + with esatar.open(file_path, "r") as tar_ref: + return extract_from_tar(tar_ref, file_path, output_dir) + elif is_gz_file(file_path): + with esatar.open(file_path, "r:gz") as tar: + return extract_from_tar(tar, file_path, output_dir) + elif zipfile.is_zipfile(file_path): + return extract_from_zip(file_path, output_dir) + elif not is_gz_file(file_path): + return [str(file_path)] + + +def is_gz_file(filepath): + with open(filepath, 'rb') as test_f: + return binascii.hexlify(test_f.read(2)) == b'1f8b' + + +def extract_from_tar(tar, file_path, output_dir=None): + """ + Extract files from a tar file (both .tar and .tar.gz formats). + """ + # Prepare the output directory + output_dir = prepare_output_dir(file_path) + + # Extract all files into the specified directory + tar.extractall(output_dir) + + # Get the paths of the extracted files + extracted_files = [os.path.join(output_dir, member.name) for member in tar.getmembers()] + return extracted_files + + +def extract_from_zip(file_path, output_dir=None): + """ + Handle extraction of .zip files. + """ + with zipfile.ZipFile(file_path, 'r') as zip_ref: + # Prepare the output directory + output_dir = prepare_output_dir(file_path) + + # Extract all files into the specified directory + zip_ref.extractall(output_dir) + + # Get the paths of the extracted files + extracted_files = [os.path.join(output_dir, file) for file in zip_ref.namelist()] + return extracted_files def check_rename_to_gz(filename): + """ + Check if the file is compressed as gz and rename it + Parameters + ---------- + filename: str, mandatory + filename to verify + + Returns + ------- + The renamed file + """ + rename = False if os.path.exists(filename): with open(filename, 'rb') as test_f: @@ -24,3 +411,58 @@ def check_rename_to_gz(filename): return os.path.basename(output) else: return os.path.basename(filename) + + +def prepare_output_dir(file_path): + """ + Prepare the output directory. If output_dir is provided, use it. Otherwise, + create a new directory with the name of the file (without the extension). + """ + # Create a directory based on the file name without extension + base_path = os.path.dirname(file_path) + base_name = os.path.basename(file_path) + file_name_without_extensions = base_name.split('.')[0] + current_time = datetime.datetime.now().strftime("%y%m%d%H%M%S") + + output_dir = os.path.join(base_path, f"{file_name_without_extensions}_{current_time}") + + if os.path.exists(output_dir): + shutil.rmtree(output_dir) + os.makedirs(output_dir, exist_ok=True) + return output_dir + + +def resolve_target(url, session, target_name, target_resolver): + """ + Download a file in streaming mode using a existing session + + Parameters + ---------- + url: str, mandatory + URL to be downloaded + session: ESAAuthSession, mandatory + session to download the file, including the cookies from ESA login + target_name: str, mandatory + Name of the target + target_resolver: str, mandatory + Name of the resolver. Possible values: ALL, SIMBAD, NED, VIZIER + + Returns + ------- + The request with the modified url + """ + + if target_resolver not in TARGET_RESOLVERS: + raise ValueError("This target resolver is not allowed") + + resolver_url = url.format(target_name, target_resolver) + try: + with session.get(resolver_url, stream=True) as response: + response.raise_for_status() + target_result = response.json() + if target_result['objects']: + ra = target_result['objects'][0]['raDegrees'] + dec = target_result['objects'][0]['decDegrees'] + return SkyCoord(ra=ra, dec=dec, unit="deg") + except (ValueError, KeyError) as err: + raise ValueError('This target cannot be resolved. {}'.format(err)) diff --git a/docs/esa/integral/integral.rst b/docs/esa/integral/integral.rst new file mode 100644 index 0000000000..65316636dc --- /dev/null +++ b/docs/esa/integral/integral.rst @@ -0,0 +1,239 @@ +.. _astroquery.esa.integral: + +********************************************************************** +ESA Integral Science Legacy Archive (ISLA) (`astroquery.esa.integral`) +********************************************************************** + +INTEGRAL is the INTernational Gamma-Ray Astrophysics Laboratory of the +European Space Agency. It observes the Universe in the X-ray and soft +gamma-ray band. Since its launch, on October 17, 2002, the ISDC receives +the spacecraft telemetry within seconds and provides alerts, processed +data and analysis software to the worldwide scientific community. + +======== +Examples +======== + +--------------------------- +1. ADQL Queries to ISLA TAP +--------------------------- + +The Query TAP functionality facilitates the execution of custom Table Access Protocol (TAP) +queries within the Integral Science Legacy Archive. Results can be exported to a specified +file in the chosen format, and queries may be executed asynchronously. + +.. doctest-remote-data:: + + >>> from astroquery.esa.integral import IntegralClass + >>> isla = IntegralClass() + >>> isla.query_tap(query='select * from ivoa.obscore') + + access_estsize access_format access_url calib_level ... t_max t_min t_resolution t_xel + int64 object object int32 ... float64 float64 float64 int64 + -------------- ----------------- ------------------------------------------------------------------------------ ----------- ... ----------- ----------- ------------ ----- + 26923 application/x-tar https://isla.esac.esa.int/tap/data?retrieval_type=SCW&b2JzaWQ9MDA2MDAwMjAwMDE= 2 ... 52594.80985 52593.47308 6.1e-05 1 + 488697 application/x-tar https://isla.esac.esa.int/tap/data?retrieval_type=SCW&b2JzaWQ9MDA2MDAwNDAwMDE= 2 ... 52596.64583 52596.46613 6.1e-05 1 + 3459831 application/x-tar https://isla.esac.esa.int/tap/data?retrieval_type=SCW&b2JzaWQ9MDA2MDAwNjAwMDI= 2 ... 52600.54167 52599.4592 6.1e-05 1 + ... ... ... ... ... ... ... ... ... + 351253 application/x-tar https://isla.esac.esa.int/tap/data?retrieval_type=SCW&b2JzaWQ9ODg4MDExMzAwMDQ= 2 ... 57188.63542 57188.28125 6.1e-05 1 + 10195 application/x-tar https://isla.esac.esa.int/tap/data?retrieval_type=SCW&b2JzaWQ9ODg4MDExMzAwMDU= 2 ... 57189.64284 57189.28867 6.1e-05 1 + 817730 application/x-tar https://isla.esac.esa.int/tap/data?retrieval_type=SCW&b2JzaWQ9ODg5OTgwMTAzMDE= 2 ... 52636.82597 52636.49534 6.1e-05 1 + +------------------ +2. Getting sources +------------------ + +Users can utilize this method to retrieve a target from the Archive by specifying a target name. +The output can be formatted and saved as needed. + +.. doctest-remote-data:: + + >>> from astroquery.esa.integral import IntegralClass + >>> isla = IntegralClass() + >>> isla.get_sources(target_name='crab') + + name ra dec source_id + object float64 float64 object + ------ ----------------- ----------------- ---------------- + Crab 83.63320922851562 22.01447105407715 J053432.0+220052 + +----------------------------------------- +3. Getting metadata associated to sources +----------------------------------------- + +By invoking this method, users gain access to detailed metadata for a given source, +identified by its target name. The metadata provides in-depth information about the source's archival. + +.. doctest-remote-data:: + + >>> from astroquery.esa.integral import IntegralClass + >>> isla = IntegralClass() + >>> metadata = isla.get_source_metadata(target_name='Crab') + >>> metadata + [{'name': 'Integral', 'link': None, 'metadata': [{'param': 'Name', 'value': 'Crab'}, {'param': 'Id', 'value': 'J053432.0+220052'}, {'param': 'Coordinates degrees', 'value': '83.6324 22.0174'}, {'param': 'Coordinates', 'value': '05 34 31.78 22 01 02.64'}, {'param': 'Galactic', 'value': '184.55 -5.78'}, {'param': 'Isgri flag2', 'value': 'very bright source'}, {'param': 'Jemx flag', 'value': 'detected'}, {'param': 'Spi flag', 'value': 'detected'}, {'param': 'Picsit flag', 'value': 'detected'}]}, {'name': 'Simbad', 'link': 'https://simbad.cds.unistra.fr/simbad/sim-id?Ident=Crab', 'metadata': [{'param': 'Id', 'value': 'NAME Crab'}, {'param': 'Type', 'value': 'SuperNova Remnant'}, {'param': 'Other types', 'value': 'HII|IR|Psr|Rad|SNR|X|gam'}]}] + +------------------------------------ +4. Retrieving observations from ISLA +------------------------------------ + +Observation data can be extracted using this method, defining a criteria such as target name, +coordinates, search radius, time range, or revolution number range. +The data can be formatted and saved to a file, with the option to perform the +operation asynchronously. + +.. doctest-remote-data:: + + >>> from astroquery.esa.integral import IntegralClass + >>> isla = IntegralClass() + >>> isla.get_observations(target_name='crab', radius=12.0, start_revno='0290', end_revno='0599') + dec email end_revno endtime ... starttime surname title + float64 str60 object object ... object str20 str120 + ---------- ------------------------------ --------- ------------------- ... ------------------- ---------- -------------------------------------------------------------- + 26.3157778 peter.kretschmar!@obs.unige.ch 0352 2005-09-02 21:20:59 ... 2005-08-31 11:07:06 Kretschmar Target of Opportunity Observations of an Outburst in A 0535+26 + 22.684 isdc-iswt@unige.ch 0300 2005-03-30 22:42:38 ... 2005-03-30 10:34:41 ISWT Crab spring/05 IBIS 10 deg arc + 20.611 isdc-iswt@unige.ch 0300 2005-03-31 13:35:11 ... 2005-03-30 22:44:07 ISWT Crab spring/05 IBIS 10 deg arc + ... ... ... ... ... ... ... ... + 22.0144444 inthelp@sciops.esa.int 0541 2007-03-21 00:08:52 ... 2007-03-20 21:30:30 Public Crab-2007 Spring: JEM-X VC settings test + 22.0144444 inthelp@sciops.esa.int 0541 2007-03-22 03:16:24 ... 2007-03-21 00:10:22 Public Crab-2007 Spring: IBIS on-axis staring + 22.0144444 inthelp@sciops.esa.int 0541 2007-03-20 15:35:22 ... 2007-03-19 12:53:21 Public Crab-2007 Spring: SPI 5x5 pattern + + +------------------------------ +5. Downloading Science Windows +------------------------------ + +Science window data can be downloaded using this method by providing only one identifier, +such as science window IDs, observation ID, revolution number, or proposal ID. +An additional parameter, read_fits (default value True) reads automatically the downloaded FITS files. + +* If ``read_fits=True``, a list of objects containing filename, path and the FITS file opened is returned. +* If ``read_fits=False``, the file name and path where the file has been downloaded is provided. + + +.. doctest-remote-data:: + + >>> from astroquery.esa.integral import IntegralClass + >>> isla = IntegralClass() + >>> isla.download_science_windows(science_windows=['008100430010', '033400230030'], output_file=None) + + +--------------------- +6. Timeline retrieval +--------------------- + +This method enables the exploration of the observation timeline for a specific region in the sky. +Users can provide right ascension (RA) and declination (Dec) coordinates and adjust the radius +to refine their search. + +.. doctest-remote-data:: + + >>> from astroquery.esa.integral import IntegralClass + >>> from astropy.coordinates import SkyCoord + >>> isla = IntegralClass() + >>> coordinates = SkyCoord(83.63320922851562, 22.01447105407715, unit="deg") + >>> timeline = isla.get_timeline(coordinates=coordinates) + >>> timeline + {'total_items': 8714, 'fraFC': 0.8510442965343126, 'totEffExpo': 16416293.994214607, 'timeline': + scwExpo scwRevs scwTimes scwOffAxis + float64 float64 object float64 + ------------------ ------------------ -------------------------- --------------------- + 5147.824025240096 39.07800595179441 2003-02-07 07:01:25.500000 0.0025430719729003476 + 4212.920636876789 39.09690979681126 2003-02-07 08:21:21.500000 0.0025430719729003476 + 4212.920651101999 39.11392562227784 2003-02-07 09:33:18.500000 0.0025430719729003476 + ... ... ... ... + 1675.7455103834102 2767.3106645083526 2024-04-17 13:39:13.500000 4.729406754053243 + 1815.2853971426027 2767.31963717696 2024-04-17 14:13:35.500000 4.203559877974954 + 2014.34083657953 2767.3294779577823 2024-04-17 14:51:17 4.731196416616404} + +-------------------- +7. Retrieving epochs +-------------------- + +A list of observation epochs can be retrieved using this method, focusing on periods when data for a +specific target, instrument, or energy band is available. + +.. doctest-remote-data:: + + >>> from astroquery.esa.integral import IntegralClass + >>> isla = IntegralClass() + >>> isla.get_epochs(target_name='J011705.1-732636', band='28_40') + epoch + object + ---------------- + 0152_01200360001 + 0745_06340000001 + 0746_06340000001 + ... + 1618_12200430006 + 1618_12200430009 + 1618_12200430011 + +This will perform an ADQL search to the Integral database and will return the output. + +---------------- +8. Data Download +---------------- + +For each of the following features, an additional parameter, read_fits, is available. + +* If ``read_fits=True``, a list of objects containing filename, path and the FITS file opened is returned. +* If ``read_fits=False``, the file names and paths where the files have been downloaded is provided. + +8.1. Retrieving Long-Term Timeseries +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This method provides access to long-term timeseries data for a specified target. +Users can refine their results by selecting an instrument or energy band. + + +.. doctest-remote-data:: + + >>> from astroquery.esa.integral import IntegralClass + >>> isla = IntegralClass() + >>> ltt = isla.get_long_term_timeseries(target_name='J174537.0-290107', instrument='jem-x') + +8.2. Retrieving Short-Term Timeseries +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This method allows for the download of short-term time series data for a target and epoch of interest. +Users can refine their search using instrument or energy band filters and +the results are saved to a file for detailed examination. + +.. doctest-remote-data:: + + >>> from astroquery.esa.integral import IntegralClass + >>> isla = IntegralClass() + >>> stt = isla.get_short_term_timeseries(target_name='J011705.1-732636', band='28_40', epoch='0745_06340000001') + +8.3. Retrieving spectra +~~~~~~~~~~~~~~~~~~~~~~~ + +This method allows users to download spectral data for a target and epoch. +Users can apply filters, such as instrument or energy band, and save the results to an +output file for further processing. + +.. doctest-remote-data:: + + >>> from astroquery.esa.integral import IntegralClass + >>> isla = IntegralClass() + >>> spectra = isla.get_spectra(target_name='J011705.1-732636', instrument='ibis', epoch='0745_06340000001') + + +8.4. Retrieving mosaics +~~~~~~~~~~~~~~~~~~~~~~~ + +Mosaic images corresponding to a specified epoch can be downloaded using this method. +Users can filter by instrument or energy band and save the resulting image to a file for later use. + +.. doctest-remote-data:: + + >>> from astroquery.esa.integral import IntegralClass + >>> isla = IntegralClass() + >>> mosaics = isla.get_mosaic(epoch='0727_88601650001', instrument='ibis') + + +Reference/API +============= + +.. automodapi:: astroquery.esa.integral + :no-inheritance-diagram: \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index 4995d97795..62f23d97b4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -141,6 +141,7 @@ queries based on coordinates or object names. Some simple examples, using SIMBA >>> from astroquery.simbad import Simbad >>> result_table = Simbad.query_object("m1") >>> result_table.pprint() + main_id ra dec coo_err_maj coo_err_min coo_err_angle coo_wavelength coo_bibcode matched_id deg deg mas mas deg ------- ------- ------- ----------- ----------- ------------- -------------- ------------------- ---------- @@ -257,6 +258,7 @@ The following modules have been completed using a common API: linelists/cdms/cdms.rst esa/hsa/hsa.rst esa/hubble/hubble.rst + esa/integral/integral.rst esa/iso/iso.rst esa/jwst/jwst.rst esa/xmm_newton/xmm_newton.rst