diff --git a/CHANGES.rst b/CHANGES.rst index 3491bb4e31..3f6c6db91c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,6 +3,10 @@ New Tools and Services ---------------------- +esa.jwst +^^^^^^^^^^ + +- New module to provide access to eJWST Science Archive metadata and datasets. [#2140] Service fixes and enhancements diff --git a/astroquery/esa/jwst/__init__.py b/astroquery/esa/jwst/__init__.py new file mode 100644 index 0000000000..c6855b96ea --- /dev/null +++ b/astroquery/esa/jwst/__init__.py @@ -0,0 +1,59 @@ +# Licensed under a 3-clause BSD style license - see LICENSE.rst +""" +========== +eJWST Init +========== + +@author: Raul Gutierrez-Sanchez +@contact: raul.gutierrez@sciops.esa.int + +European Space Astronomy Centre (ESAC) +European Space Agency (ESA) + +Created on 23 oct. 2018 + +""" + + +from astropy import config as _config + + +class Conf(_config.ConfigNamespace): + """ + Configuration parameters for `astroquery.esa.jwst`. + """ + + JWST_TAP_SERVER = _config.ConfigItem("http://jwstdummytap.com", "eJWST TAP Server") + JWST_DATA_SERVER = _config.ConfigItem("http://jwstdummydata.com", "eJWST Data Server") + JWST_TOKEN = _config.ConfigItem("jwstToken", "eJWST token") + JWST_MESSAGES = _config.ConfigItem("notification?action=GetNotifications", "eJWST Messages") + + JWST_MAIN_TABLE = _config.ConfigItem("jwst.main", "JWST main table, combination of observation and plane tables.") + + JWST_MAIN_TABLE_RA = _config.ConfigItem("target_ra", "Name of RA parameter in table") + + JWST_MAIN_TABLE_DEC = _config.ConfigItem("target_dec", "Name of Dec parameter in table") + + JWST_ARTIFACT_TABLE = _config.ConfigItem("jwst.artifact", "JWST artifacts (data files) table.") + + JWST_OBSERVATION_TABLE = _config.ConfigItem("jwst.observation", "JWST observation table") + + JWST_PLANE_TABLE = _config.ConfigItem("jwst.plane", "JWST plane table") + + JWST_OBS_MEMBER_TABLE = _config.ConfigItem("jwst.observationmember", "JWST observation member table") + + JWST_OBSERVATION_TABLE_RA = _config.ConfigItem("targetposition_coordinates_cval1", + "Name of RA parameter " + "in table") + + JWST_OBSERVATION_TABLE_DEC = _config.ConfigItem("targetposition_coordinates_cval2", + "Name of Dec parameter " + "in table") + + +conf = Conf() + +from .core import Jwst, JwstClass +from .data_access import JwstDataHandler + +__all__ = ['Jwst', 'JwstClass', 'JwstDataHandler', 'Conf', 'conf'] diff --git a/astroquery/esa/jwst/core.py b/astroquery/esa/jwst/core.py new file mode 100644 index 0000000000..aed3ab608a --- /dev/null +++ b/astroquery/esa/jwst/core.py @@ -0,0 +1,1242 @@ +# Licensed under a 3-clause BSD style license - see LICENSE.rst +""" +======================= +eJWST Astroquery Module +======================= + +European Space Astronomy Centre (ESAC) +European Space Agency (ESA) + +""" +import binascii +import gzip +import os +import shutil +import tarfile +import zipfile +from builtins import isinstance +from datetime import datetime + +from astropy import log +from astropy import units +from astropy.coordinates import SkyCoord +from astropy.table import vstack +from astropy.units import Quantity +from requests.exceptions import ConnectionError + +from astroquery.exceptions import RemoteServiceError +from astroquery.ipac.ned import Ned +from astroquery.query import BaseQuery +from astroquery.simbad import Simbad +from astroquery.utils import commons +from astroquery.utils.tap import TapPlus +from astroquery.vizier import Vizier +from . import conf +from .data_access import JwstDataHandler + +__all__ = ['Jwst', 'JwstClass'] + + +class JwstClass(BaseQuery): + + """ + Proxy class to default TapPlus object (pointing to JWST Archive) + THIS MODULE IS NOT OPERATIVE YET. METHODS WILL NOT WORK UNTIL eJWST ARCHIVE IS OFFICIALLY RELEASED + """ + + JWST_DEFAULT_COLUMNS = ['observationid', 'calibrationlevel', 'public', + 'dataproducttype', 'instrument_name', + 'energy_bandpassname', 'target_name', 'target_ra', + 'target_dec', 'position_bounds_center', + 'position_bounds_spoly'] + + PLANE_DATAPRODUCT_TYPES = ['image', 'cube', 'measurements', 'spectrum'] + ARTIFACT_PRODUCT_TYPES = ['info', 'thumbnail', 'auxiliary', 'science', + 'preview'] + INSTRUMENT_NAMES = ['NIRISS', 'NIRSPEC', 'NIRCAM', 'MIRI', 'FGS'] + TARGET_RESOLVERS = ['ALL', 'SIMBAD', 'NED', 'VIZIER'] + CAL_LEVELS = ['ALL', 1, 2, 3, -1] + REQUESTED_OBSERVATION_ID = "Missing required argument: 'observation_id'" + + def __init__(self, *, tap_plus_handler=None, data_handler=None): + if tap_plus_handler is None: + self.__jwsttap = TapPlus(url=conf.JWST_TAP_SERVER, + data_context='data') + else: + self.__jwsttap = tap_plus_handler + + if data_handler is None: + self.__jwstdata = JwstDataHandler( + base_url=conf.JWST_DATA_SERVER) + else: + self.__jwstdata = data_handler + print("THIS MODULE IS NOT OPERATIVE YET. METHODS WILL NOT WORK UNTIL eJWST ARCHIVE IS OFFICIALLY RELEASED") + + def load_tables(self, *, only_names=False, include_shared_tables=False, + verbose=False): + """Loads all public tables + TAP & TAP+ + + Parameters + ---------- + only_names : bool, TAP+ only, optional, default 'False' + True to load table names only + include_shared_tables : bool, TAP+, optional, default 'False' + True to include shared tables + verbose : bool, optional, default 'False' + flag to display information about the process + + Returns + ------- + A list of table objects + """ + return self.__jwsttap.load_tables(only_names, + include_shared_tables, + verbose) + + def load_table(self, table, *, verbose=False): + """Loads the specified table + TAP+ only + + Parameters + ---------- + table : str, mandatory + full qualified table name (i.e. schema name + table name) + verbose : bool, optional, default 'False' + flag to display information about the process + + Returns + ------- + A table object + """ + return self.__jwsttap.load_table(table, verbose) + + def launch_job(self, query, *, name=None, output_file=None, + output_format="votable", verbose=False, dump_to_file=False, + background=False, upload_resource=None, upload_table_name=None, + async_job=False): + """Launches a synchronous or asynchronous job + TAP & TAP+ + + Parameters + ---------- + query : str, mandatory + query to be executed + name : str, optional, default None + name of the job to be executed + 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. Options are: + 'votable': str, binary VOTable format + 'csv': str, comma-separated values format + 'fits': str, FITS format + verbose : bool, optional, default 'False' + flag to display information about the process + dump_to_file : bool, optional, default 'False' + if True, the results are saved in a file instead of using memory + background : bool, optional, default 'False' + when the job is executed in asynchronous mode, this flag specifies + whether the execution will wait until results are available + upload_resource: str, optional, default None + resource to be uploaded to UPLOAD_SCHEMA + upload_table_name: str, required if uploadResource is provided + Default None + resource temporary table name associated to the uploaded resource + async_job: bool, optional, default 'False' + tag to execute the job in sync or async mode + + Returns + ------- + A Job object + """ + if async_job: + return (self.__jwsttap.launch_job_async(query, + name=name, + output_file=output_file, + output_format=output_format, + verbose=verbose, + dump_to_file=dump_to_file, + background=background, + upload_resource=upload_resource, + upload_table_name=upload_table_name)) + else: + return self.__jwsttap.launch_job(query, + name=name, + output_file=output_file, + output_format=output_format, + verbose=verbose, + dump_to_file=dump_to_file, + upload_resource=upload_resource, + upload_table_name=upload_table_name) + + def load_async_job(self, *, jobid=None, name=None, verbose=False): + """Loads an asynchronous job + TAP & TAP+ + + Parameters + ---------- + jobid : str, mandatory if no name is provided, default None + job identifier + name : str, mandatory if no jobid is provided, default None + job name + verbose : bool, optional, default 'False' + flag to display information about the process + + Returns + ------- + A Job object + """ + return self.__jwsttap.load_async_job(jobid, name, verbose) + + def search_async_jobs(self, *, jobfilter=None, verbose=False): + """Searches for jobs applying the specified filter + TAP+ only + + Parameters + ---------- + jobfilter : JobFilter, optional, default None + job filter + verbose : bool, optional, default 'False' + flag to display information about the process + + Returns + ------- + A list of Job objects + """ + return self.__jwsttap.search_async_jobs(jobfilter, verbose) + + def list_async_jobs(self, *, verbose=False): + """Returns all the asynchronous jobs + TAP & TAP+ + + Parameters + ---------- + verbose : bool, optional, default 'False' + flag to display information about the process + + Returns + ------- + A list of Job objects + """ + return self.__jwsttap.list_async_jobs(verbose) + + def query_region(self, coordinate, *, + radius=None, + width=None, + height=None, + observation_id=None, + cal_level="Top", + prod_type=None, + instrument_name=None, + filter_name=None, + proposal_id=None, + only_public=False, + show_all_columns=False, + async_job=False, verbose=False): + """Launches a query region job in sync/async mode + TAP & TAP+ + + Parameters + ---------- + coordinate : astropy.coordinate, mandatory + coordinates center point + radius : astropy.units, required if no 'width' nor 'height' + are provided + radius (deg) + width : astropy.units, required if no 'radius' is provided + box width + height : astropy.units, required if no 'radius' is provided + box height + observation_id : str, optional, default None + get the observation given by its ID. + cal_level : object, optional, default 'Top' + get the planes with the given calibration level. Options are: + 'Top': str, only the planes with the highest calibration level + 1,2,3: int, the given calibration level + prod_type : str, optional, default None + get the observations providing the given product type. Options are: + 'image','cube','measurements','spectrum': str, only results of the + given product type + instrument_name : str, optional, default None + get the observations corresponding to the given instrument name. + Options are: + 'NIRISS', 'NIRSPEC', 'NIRCAM', 'MIRI', 'FGS': str, only results of + the given instrument + filter_name : str, optional, default None + get the observations made with the given filter. + proposal_id : str, optional, default None + get the observations from the given proposal ID. + show_all_columns : bool, optional, default 'False' + flag to show all available columns in the output. + Default behaviour is to show the most representative columns only + only_public : bool, optional, default 'False' + flag to show only metadata corresponding to public observations + async_job : bool, optional, default 'False' + executes the query (job) in asynchronous/synchronous mode (default + synchronous) + verbose : bool, optional, default 'False' + flag to display information about the process + + Returns + ------- + The job results (astropy.table). + """ + coord = self.__get_coord_input(value=coordinate, msg="coordinate") + job = None + if radius is not None: + job = self.cone_search(coordinate=coord, + radius=radius, + only_public=only_public, + observation_id=observation_id, + cal_level=cal_level, + prod_type=prod_type, + instrument_name=instrument_name, + filter_name=filter_name, + proposal_id=proposal_id, + show_all_columns=show_all_columns, + async_job=async_job, verbose=verbose) + else: + raHours, dec = commons.coord_to_radec(coord) + ra = raHours * 15.0 # Converts to degrees + widthQuantity = self.__get_quantity_input(value=width, msg="width") + heightQuantity = self.__get_quantity_input(value=height, msg="height") + widthDeg = widthQuantity.to(units.deg) + heightDeg = heightQuantity.to(units.deg) + + obsid_cond = self.__get_observationid_condition(value=observation_id) + cal_level_condition = self.__get_callevel_condition(cal_level=cal_level) + public_condition = self.__get_public_condition(only_public=only_public) + prod_cond = self.__get_plane_dataproducttype_condition(prod_type=prod_type) + instr_cond = self.__get_instrument_name_condition(value=instrument_name) + filter_name_cond = self.__get_filter_name_condition(value=filter_name) + props_id_cond = self.__get_proposal_id_condition(value=proposal_id) + + columns = str(', '.join(self.JWST_DEFAULT_COLUMNS)) + if show_all_columns: + columns = '*' + + query = (f"SELECT DISTANCE(POINT('ICRS'," + f"{str(conf.JWST_MAIN_TABLE_RA)}," + f"{str(conf.JWST_MAIN_TABLE_DEC)} ), " + f"POINT('ICRS',{str(ra)},{str(dec)} )) " + f"AS dist, {columns} " + f"FROM {str(conf.JWST_MAIN_TABLE)} " + f"WHERE CONTAINS(" + f"POINT('ICRS'," + f"{str(conf.JWST_MAIN_TABLE_RA)}," + f"{str(conf.JWST_MAIN_TABLE_DEC)})," + f"BOX('ICRS',{str(ra)},{str(dec)}, " + f"{str(widthDeg.value)}, " + f"{str(heightDeg.value)}))=1 " + f"{obsid_cond}" + f"{cal_level_condition}" + f"{public_condition}" + f"{prod_cond}" + f"{instr_cond}" + f"{filter_name_cond}" + f"{props_id_cond}" + f"ORDER BY dist ASC") + if verbose: + print(query) + if async_job: + job = self.__jwsttap.launch_job_async(query, verbose=verbose) + else: + job = self.__jwsttap.launch_job(query, verbose=verbose) + return job.get_results() + + def cone_search(self, coordinate, radius, *, + observation_id=None, + cal_level="Top", + prod_type=None, + instrument_name=None, + filter_name=None, + proposal_id=None, + only_public=False, + show_all_columns=False, + async_job=False, + background=False, + output_file=None, + output_format="votable", + verbose=False, + dump_to_file=False): + """Cone search sorted by distance in sync/async mode + TAP & TAP+ + + Parameters + ---------- + coordinate : astropy.coordinate, mandatory + coordinates center point + radius : astropy.units, mandatory + radius + observation_id : str, optional, default None + get the observation given by its ID. + cal_level : object, optional, default 'Top' + get the planes with the given calibration level. Options are: + 'Top': str, only the planes with the highest calibration level + 1,2,3: int, the given calibration level + prod_type : str, optional, default None + get the observations providing the given product type. Options are: + 'image','cube','measurements','spectrum': str, only results of + the given product type + instrument_name : str, optional, default None + get the observations corresponding to the given instrument name. + Options are: + 'NIRISS', 'NIRSPEC', 'NIRCAM', 'MIRI', 'FGS': str, only results + of the given instrument + filter_name : str, optional, default None + get the observations made with the given filter. + proposal_id : str, optional, default None + get the observations from the given proposal ID. + only_public : bool, optional, default 'False' + flag to show only metadata corresponding to public observations + show_all_columns : bool, optional, default 'False' + flag to show all available columns in the output. Default behaviour + is to show the most representative columns only + async_job : bool, optional, default 'False' + executes the job in asynchronous/synchronous mode (default + synchronous) + background : bool, optional, default 'False' + when the job is executed in asynchronous mode, this flag specifies + whether the execution will wait until results are available + 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. Options are: + 'votable': str, binary VOTable format + 'csv': str, comma-separated values format + 'fits': str, FITS format + verbose : bool, optional, default 'False' + flag to display information about the process + dump_to_file : bool, optional, default 'False' + if True, the results are saved in a file instead of using memory + + Returns + ------- + A Job object + """ + coord = self.__get_coord_input(value=coordinate, msg="coordinate") + ra_hours, dec = commons.coord_to_radec(coord) + ra = ra_hours * 15.0 # Converts to degrees + + obsid_condition = self.__get_observationid_condition(value=observation_id) + cal_level_condition = self.__get_callevel_condition(cal_level=cal_level) + public_condition = self.__get_public_condition(only_public=only_public) + prod_type_cond = self.__get_plane_dataproducttype_condition(prod_type=prod_type) + inst_name_cond = self.__get_instrument_name_condition(value=instrument_name) + filter_name_condition = self.__get_filter_name_condition(value=filter_name) + proposal_id_condition = self.__get_proposal_id_condition(value=proposal_id) + + columns = str(', '.join(self.JWST_DEFAULT_COLUMNS)) + if show_all_columns: + columns = '*' + + if radius is not None: + radius_quantity = self.__get_quantity_input(value=radius, msg="radius") + radius_deg = commons.radius_to_unit(radius_quantity, unit='deg') + + query = (f"SELECT DISTANCE(POINT('ICRS'," + f"{str(conf.JWST_MAIN_TABLE_RA)}," + f"{str(conf.JWST_MAIN_TABLE_DEC)}), " + f"POINT('ICRS',{str(ra)},{str(dec)})) AS dist, {columns} " + f"FROM {str(conf.JWST_MAIN_TABLE)} WHERE CONTAINS(" + f"POINT('ICRS',{str(conf.JWST_MAIN_TABLE_RA)}," + f"{str(conf.JWST_MAIN_TABLE_DEC)})," + f"CIRCLE('ICRS',{str(ra)},{str(dec)}, " + f"{str(radius_deg)}))=1" + f"{obsid_condition}" + f"{cal_level_condition}" + f"{public_condition}" + f"{prod_type_cond}" + f"{inst_name_cond}" + f"{filter_name_condition}" + f"{proposal_id_condition}" + f"ORDER BY dist ASC") + if async_job: + return self.__jwsttap.launch_job_async(query=query, + output_file=output_file, + output_format=output_format, + verbose=verbose, + dump_to_file=dump_to_file, + background=background) + else: + return self.__jwsttap.launch_job(query=query, + output_file=output_file, + output_format=output_format, + verbose=verbose, + dump_to_file=dump_to_file) + + def query_target(self, target_name, *, target_resolver="ALL", + radius=None, + width=None, + height=None, + observation_id=None, + cal_level="Top", + prod_type=None, + instrument_name=None, + filter_name=None, + proposal_id=None, + only_public=False, + show_all_columns=False, + async_job=False, + verbose=False): + """Searches for a specific target defined by its name and other parameters + TAP & TAP+ + + Parameters + ---------- + target_name : str, mandatory + name of the target that will be used as center point + target_resolver : str, optional, default ALL + resolver used to associate the target name with its coordinates. + The ALL option evaluates a "SIMBAD then NED then VIZIER" + approach. Options are: ALL, SIMBAD, NED, VIZIER. + radius : astropy.units, required if no 'width' nor 'height' are + provided. + radius (deg) + width : astropy.units, required if no 'radius' is provided + box width + height : astropy.units, required if no 'radius' is provided + box height + observation_id : str, optional, default None + get the observation given by its ID. + cal_level : object, optional, default 'Top' + get the planes with the given calibration level. Options are: + 'Top': str, only the planes with the highest calibration level + 1,2,3: int, the given calibration level + prod_type : str, optional, default None + get the observations providing the given product type. Options are: + 'image','cube','measurements','spectrum': str, only results of the + given product type + instrument_name : str, optional, default None + get the observations corresponding to the given instrument name. + Options are: + 'NIRISS', 'NIRSPEC', 'NIRCAM', 'MIRI', 'FGS': str, only results + of the given instrument + filter_name : str, optional, default None + get the observations made with the given filter. + proposal_id : str, optional, default None + get the observations from the given proposal ID. + only_public : bool, optional, default 'False' + flag to show only metadata corresponding to public observations + show_all_columns : bool, optional, default 'False' + flag to show all available columns in the output. Default behaviour + is to show the most + representative columns only + async_job : bool, optional, default 'False' + executes the query (job) in asynchronous/synchronous mode (default + synchronous) + verbose : bool, optional, default 'False' + flag to display information about the process + + Returns + ------- + The job results (astropy.table). + """ + coordinates = self.resolve_target_coordinates(target_name=target_name, + target_resolver=target_resolver) + return self.query_region(coordinate=coordinates, + radius=radius, + width=width, + height=height, + observation_id=observation_id, + cal_level=cal_level, + prod_type=prod_type, + instrument_name=instrument_name, + filter_name=filter_name, + proposal_id=proposal_id, + only_public=only_public, + async_job=async_job, + show_all_columns=show_all_columns, + verbose=verbose) + + def resolve_target_coordinates(self, target_name, target_resolver): + if target_resolver not in self.TARGET_RESOLVERS: + raise ValueError("This target resolver is not allowed") + + result_table = None + if target_resolver == "ALL" or target_resolver == "SIMBAD": + try: + result_table = Simbad.query_object(target_name) + return SkyCoord((f'{result_table["RA"][0]} ' + f'{result_table["DEC"][0]}'), + unit=(units.hourangle, + units.deg), frame="icrs") + except (KeyError, TypeError, ConnectionError): + log.info("SIMBAD could not resolve this target") + if target_resolver == "ALL" or target_resolver == "NED": + try: + result_table = Ned.query_object(target_name) + return SkyCoord(result_table["RA"][0], + result_table["DEC"][0], + unit="deg", frame="fk5") + except (RemoteServiceError, KeyError, ConnectionError): + log.info("NED could not resolve this target") + if target_resolver == "ALL" or target_resolver == "VIZIER": + try: + result_table = Vizier.query_object(target_name, + catalog="II/336/apass9")[0] + # Sorted to use the record with the least uncertainty + result_table.sort(["e_RAJ2000", "e_DEJ2000"]) + return SkyCoord(result_table["RAJ2000"][0], + result_table["DEJ2000"][0], + unit="deg", frame="fk5") + except (IndexError, AttributeError, ConnectionError): + log.info("VIZIER could not resolve this target") + if result_table is None: + raise ValueError(f"This target name cannot be determined with" + f" this resolver: {target_resolver}") + + def remove_jobs(self, jobs_list, *, verbose=False): + """Removes the specified jobs + TAP+ + + Parameters + ---------- + jobs_list : str, mandatory + jobs identifiers to be removed + verbose : bool, optional, default 'False' + flag to display information about the process + + """ + return self.__jwsttap.remove_jobs(jobs_list, verbose=verbose) + + def save_results(self, job, *, verbose=False): + """Saves job results + TAP & TAP+ + + Parameters + ---------- + job : Job, mandatory + job + verbose : bool, optional, default 'False' + flag to display information about the process + """ + return self.__jwsttap.save_results(job, verbose) + + def login(self, *, user=None, password=None, credentials_file=None, + token=None, verbose=False): + """Performs a login. + TAP+ only + User and password can be used or a file that contains user name and + password (2 lines: one for user name and the following one for the + password) + + Parameters + ---------- + user : str, mandatory if 'file' is not provided, default None + login name + password : str, mandatory if 'file' is not provided, default None + user password + credentials_file : str, mandatory if no 'user' & 'password' are + provided + file containing user and password in two lines + token: str, optional + MAST token to have access to propietary data + verbose : bool, optional, default 'False' + flag to display information about the process + """ + self.__jwsttap.login(user=user, + password=password, + credentials_file=credentials_file, + verbose=verbose) + if token: + self.set_token(token=token) + + def login_gui(self, *, verbose=False): + """Performs a login using a GUI dialog + TAP+ only + + Parameters + ---------- + verbose : bool, optional, default 'False' + flag to display information about the process + """ + return self.__jwsttap.login_gui(verbose) + + def logout(self, *, verbose=False): + """Performs a logout + TAP+ only + + Parameters + ---------- + verbose : bool, optional, default 'False' + flag to display information about the process + """ + return self.__jwsttap.logout(verbose) + + def set_token(self, token): + """Links a MAST token to the logged user + + Parameters + ---------- + token: str, mandatory + MAST token to have access to propietary data + """ + subContext = conf.JWST_TOKEN + args = {"token": token} + connHandler = self.__jwsttap._TapPlus__getconnhandler() + data = connHandler.url_encode(args) + response = connHandler.execute_secure(subContext, data, True) + if response.status == 403: + print("ERROR: MAST tokens cannot be assigned or requested by anonymous users") + elif response.status == 500: + print("ERROR: Server error when setting the token") + else: + print("MAST token has been set successfully") + + def get_status_messages(self): + """Retrieve the messages to inform users about + the status of JWST TAP + """ + + subContext = conf.JWST_MESSAGES + connHandler = self.__jwsttap._TapPlus__getconnhandler() + response = connHandler.execute_tapget(subContext, False) + if response.status == 200: + for line in response: + string_message = line.decode("utf-8") + print(string_message[string_message.index('=')+1:]) + + def get_product_list(self, *, observation_id=None, + cal_level="ALL", + product_type=None): + """Get the list of products of a given JWST observation_id. + + Parameters + ---------- + observation_id : str, mandatory + Observation identifier. + cal_level : str or int, optional + Calibration level. Default value is 'ALL', to download all the + products associated to this observation_id and lower processing + levels. Requesting more accurate levels than the one associated + to the observation_id is not allowed (as level 3 observations are + composite products based on level 2 products). To request upper + levels, please use get_related_observations functions first. + Possible values: 'ALL', 3, 2, 1, -1 + product_type : str, optional, default None + List only products of the given type. If None, all products are + listed. Possible values: 'thumbnail', 'preview', 'info', + 'auxiliary', 'science'. + + Returns + ------- + The list of products (astropy.table). + """ + self.__validate_cal_level(cal_level=cal_level) + + if observation_id is None: + raise ValueError(self.REQUESTED_OBSERVATION_ID) + plane_ids, max_cal_level = self._get_plane_id(observation_id=observation_id) + if (cal_level == 3 and cal_level > max_cal_level): + raise ValueError("Requesting upper levels is not allowed") + list = self._get_associated_planes(plane_ids=plane_ids, + cal_level=cal_level, + max_cal_level=max_cal_level, + is_url=False) + + query = (f"select distinct a.uri, a.artifactid, a.filename, " + f"a.contenttype, a.producttype, p.calibrationlevel, " + f"p.public FROM {conf.JWST_PLANE_TABLE} p JOIN " + f"{conf.JWST_ARTIFACT_TABLE} a ON (p.planeid=a.planeid) " + f"WHERE a.planeid IN {list}" + f"{self.__get_artifact_producttype_condition(product_type=product_type)};") + job = self.__jwsttap.launch_job(query=query) + return job.get_results() + + def __validate_cal_level(self, cal_level): + if (cal_level not in self.CAL_LEVELS): + raise ValueError("This calibration level is not valid") + + def _get_associated_planes(self, plane_ids, cal_level, + max_cal_level, is_url): + if (cal_level == max_cal_level): + if (not is_url): + list = "('{}')".format("', '".join(plane_ids)) + else: + list = "{}".format(",".join(plane_ids)) + return list + else: + plane_list = [] + for plane_id in plane_ids: + siblings = self.__get_sibling_planes(planeid=plane_id, cal_level=cal_level) + members = self.__get_member_planes(planeid=plane_id, cal_level=cal_level) + plane_id_table = vstack([siblings, members]) + plane_list.extend(plane_id_table['product_planeid'].pformat( + show_name=False)) + if (not is_url): + list = "('{}')".format("', '".join(plane_list)) + else: + list = "{}".format(",".join(plane_list)) + return list + + def _get_plane_id(self, observation_id): + try: + planeids = [] + query_plane = (f"select distinct m.planeid, m.calibrationlevel " + f"from {conf.JWST_MAIN_TABLE} m where " + f"m.observationid = '{observation_id}'") + job = self.__jwsttap.launch_job(query=query_plane) + job.get_results().sort(["calibrationlevel"]) + job.get_results().reverse() + max_cal_level = job.get_results()["calibrationlevel"][0] + for row in job.get_results(): + if(row["calibrationlevel"] == max_cal_level): + planeids.append( + JwstClass.get_decoded_string(row["planeid"])) + return planeids, max_cal_level + except Exception as e: + raise ValueError("This observation_id does not exist in " + "JWST database") + + def __get_sibling_planes(self, planeid, *, cal_level='ALL'): + where_clause = "" + if (cal_level == "ALL"): + where_clause = "WHERE sp.calibrationlevel<=p.calibrationlevel "\ + "AND p.planeid =" + else: + where_clause = (f"WHERE sp.calibrationlevel={cal_level} AND " + f"p.planeid =") + try: + query_siblings = (f"SELECT o.observationuri, p.planeid, " + f"p.calibrationlevel, sp.planeid as " + f"product_planeid, sp.calibrationlevel as " + f"product_level FROM " + f"{conf.JWST_OBSERVATION_TABLE} o JOIN " + f"{conf.JWST_PLANE_TABLE} p ON " + f"p.obsid=o.obsid JOIN " + f"{conf.JWST_PLANE_TABLE} sp ON " + f"sp.obsid=o.obsid {where_clause}'{planeid}'") + job = self.__jwsttap.launch_job(query=query_siblings) + return job.get_results() + except Exception as e: + raise ValueError(e) + + def __get_member_planes(self, planeid, *, cal_level='ALL'): + where_clause = "" + if (cal_level == "ALL"): + where_clause = "WHERE p.planeid =" + else: + where_clause = (f"WHERE mp.calibrationlevel={cal_level} AND " + f"p.planeid =") + try: + query_members = (f"SELECT o.observationuri, p.planeid, " + f"p.calibrationlevel, mp.planeid as " + f"product_planeid, mp.calibrationlevel as " + f"product_level FROM " + f"{conf.JWST_OBSERVATION_TABLE} o JOIN " + f"{conf.JWST_PLANE_TABLE} p on " + f"o.obsid=p.obsid JOIN " + f"{conf.JWST_OBS_MEMBER_TABLE} m on " + f"o.obsid=m.parentid JOIN " + f"{conf.JWST_OBSERVATION_TABLE} " + f"mo on m.memberid=mo.observationuri JOIN " + f"{conf.JWST_PLANE_TABLE} mp on " + f"mo.obsid=mp.obsid " + f"{where_clause}'{planeid}'") + job = self.__jwsttap.launch_job(query=query_members) + return job.get_results() + except Exception as e: + raise ValueError(e) + + def get_related_observations(self, observation_id): + """In case of processing levels < 3, get the list of level 3 + products that make use of a given JWST observation_id. In case of + processing level 3, retrieves the list of products used to create + this composite observation + + Parameters + ---------- + observation_id : str, mandatory + Observation identifier. + + Returns + ------- + A list of strings with the observation_id of the associated + observations that can be used in get_product_list and + get_obs_products functions + """ + if observation_id is None: + raise ValueError(self.REQUESTED_OBSERVATION_ID) + query_upper = (f"select * from {conf.JWST_MAIN_TABLE} m " + f"where m.members like " + f"'%{observation_id}%'") + job = self.__jwsttap.launch_job(query=query_upper) + if any(job.get_results()["observationid"]): + oids = job.get_results()["observationid"].pformat(show_name=False) + else: + query_members = (f"select m.members from {conf.JWST_MAIN_TABLE} " + f"m where m.observationid" + f"='{observation_id}'") + job = self.__jwsttap.launch_job(query=query_members) + oids = JwstClass.get_decoded_string( + job.get_results()["members"][0]).\ + replace("caom:JWST/", "").split(" ") + return oids + + def get_product(self, *, artifact_id=None, file_name=None): + """Get a JWST product given its Artifact ID or File name. + + Parameters + ---------- + artifact_id : str, mandatory (if no file_name is provided) + Artifact ID of the product. + file_name : str, mandatory (if no artifact_id is provided) + + Returns + ------- + local_path : str + Returns the local path that the file was downloaded to. + """ + + params_dict = {} + params_dict['RETRIEVAL_TYPE'] = 'PRODUCT' + params_dict['DATA_RETRIEVAL_ORIGIN'] = 'ASTROQUERY' + + self.__check_product_input(artifact_id=artifact_id, + file_name=file_name) + + if file_name is None: + try: + output_file_name = self._query_get_product(artifact_id=artifact_id) + err_msg = str(artifact_id) + except Exception as exx: + raise ValueError('Cannot retrieve product for artifact_id ' + + artifact_id + ': %s' % str(exx)) + else: + output_file_name = str(file_name) + err_msg = str(file_name) + + if artifact_id is not None: + params_dict['ARTIFACTID'] = str(artifact_id) + else: + try: + params_dict['ARTIFACTID'] = (self._query_get_product( + file_name=file_name)) + except Exception as exx: + raise ValueError('Cannot retrieve product for file_name ' + + file_name + ': %s' % str(exx)) + + try: + self.__jwsttap.load_data(params_dict=params_dict, + output_file=output_file_name) + except Exception as exx: + log.info("error") + raise ValueError('Error retrieving product for ' + + err_msg + ': %s' % str(exx)) + return output_file_name + + def _query_get_product(self, *, artifact_id=None, file_name=None): + if(file_name): + query_artifactid = (f"select * from {conf.JWST_ARTIFACT_TABLE} " + f"a where a.filename = " + f"'{file_name}'") + job = self.__jwsttap.launch_job(query=query_artifactid) + return JwstClass.get_decoded_string( + job.get_results()['artifactid'][0]) + else: + query_filename = (f"select * from {conf.JWST_ARTIFACT_TABLE} a " + f"where a.artifactid = " + f"'{artifact_id}'") + job = self.__jwsttap.launch_job(query=query_filename) + return JwstClass.get_decoded_string( + job.get_results()['filename'][0]) + + def __check_product_input(self, artifact_id, file_name): + if artifact_id is None and file_name is None: + raise ValueError("Missing required argument: " + "'artifact_id' or 'file_name'") + + def get_obs_products(self, *, observation_id=None, cal_level="ALL", + product_type=None, output_file=None): + """Get a JWST product given its observation ID. + + Parameters + ---------- + observation_id : str, mandatory + Observation identifier. + cal_level : str or int, optional + Calibration level. Default value ia 'ALL', to download all the + products associated to this observation_id and lower levels. + Requesting more accurate levels than the one associated to the + observation_id is not allowed (as level 3 observations are + composite products based on level 2 products). To request upper + levels, please use get_related_observations functions first. + Possible values: 'ALL', 3, 2, 1, -1 + product_type : str, optional, default None + List only products of the given type. If None, all products are \ + listed. Possible values: 'thumbnail', 'preview', 'auxiliary', \ + 'science'. + output_file : str, optional + Output file. If no value is provided, a temporary one is created. + + Returns + ------- + local_path : str + Returns the local path where the product(s) are saved. + """ + + if observation_id is None: + raise ValueError(self.REQUESTED_OBSERVATION_ID) + plane_ids, max_cal_level = self._get_plane_id(observation_id=observation_id) + + if (cal_level == 3 and cal_level > max_cal_level): + raise ValueError("Requesting upper levels is not allowed") + + params_dict = {} + params_dict['RETRIEVAL_TYPE'] = 'OBSERVATION' + params_dict['DATA_RETRIEVAL_ORIGIN'] = 'ASTROQUERY' + + plane_ids = self._get_associated_planes(plane_ids=plane_ids, + cal_level=cal_level, + max_cal_level=max_cal_level, + is_url=True) + params_dict['planeid'] = plane_ids + self.__set_additional_parameters(param_dict=params_dict, + cal_level=cal_level, + max_cal_level=max_cal_level, + product_type=product_type) + output_file_full_path, output_dir = self.__set_dirs(output_file=output_file, + observation_id=observation_id) + # Get file name only + output_file_name = os.path.basename(output_file_full_path) + + try: + self.__jwsttap.load_data(params_dict=params_dict, + output_file=output_file_full_path) + except Exception as exx: + raise ValueError('Cannot retrieve products for observation ' + + observation_id + ': %s' % str(exx)) + + files = [] + self.__extract_file(output_file_full_path=output_file_full_path, + output_dir=output_dir, + files=files) + if (files): + return files + + self.__check_file_number(output_dir=output_dir, + output_file_name=output_file_name, + output_file_full_path=output_file_full_path, + files=files) + + return files + + def __check_file_number(self, output_dir, output_file_name, + output_file_full_path, files): + num_files_in_dir = len(os.listdir(output_dir)) + if num_files_in_dir == 1: + if output_file_name.endswith("_all_products"): + p = output_file_name.rfind('_all_products') + output_f = output_file_name[0:p] + else: + output_f = output_file_name + + output_full_path = output_dir + os.sep + output_f + + os.rename(output_file_full_path, output_full_path) + files.append(output_full_path) + else: + # r=root, d=directories, f = files + for r, d, f in os.walk(output_dir): + for file in f: + if file != output_file_name: + files.append(os.path.join(r, file)) + + def __extract_file(self, output_file_full_path, output_dir, files): + if tarfile.is_tarfile(output_file_full_path): + with tarfile.open(output_file_full_path) as tar_ref: + tar_ref.extractall(path=output_dir) + elif zipfile.is_zipfile(output_file_full_path): + with zipfile.ZipFile(output_file_full_path, 'r') as zip_ref: + zip_ref.extractall(output_dir) + elif not JwstClass.is_gz_file(output_file_full_path): + # single file: return it + files.append(output_file_full_path) + return files + + def __set_dirs(self, output_file, observation_id): + if output_file is None: + now = datetime.now() + formatted_now = now.strftime("%Y%m%d_%H%M%S") + output_dir = os.getcwd() + os.sep + "temp_" + \ + formatted_now + output_file_full_path = output_dir + os.sep + observation_id +\ + "_all_products" + else: + output_file_full_path = output_file + output_dir = os.path.dirname(output_file_full_path) + try: + os.makedirs(output_dir, exist_ok=True) + except OSError as err: + raise OSError("Creation of the directory %s failed: %s" + % (output_dir, err.strerror)) + return output_file_full_path, output_dir + + def __set_additional_parameters(self, param_dict, cal_level, + max_cal_level, product_type): + if cal_level is not None: + self.__validate_cal_level(cal_level=cal_level) + if(cal_level == max_cal_level or cal_level == 2): + param_dict['calibrationlevel'] = 'SELECTED' + elif(cal_level == 1): + param_dict['calibrationlevel'] = 'LEVEL1ONLY' + else: + param_dict['calibrationlevel'] = cal_level + + if product_type is not None: + param_dict['product_type'] = str(product_type) + + def __get_quantity_input(self, value, msg): + if value is None: + raise ValueError("Missing required argument: '"+str(msg)+"'") + if not (isinstance(value, str) or isinstance(value, units.Quantity)): + raise ValueError( + str(msg) + " must be either a string or astropy.coordinates") + if isinstance(value, str): + q = Quantity(value) + return q + else: + return value + + def __get_coord_input(self, value, msg): + if not (isinstance(value, str) or isinstance(value, + commons.CoordClasses)): + raise ValueError( + str(msg) + " must be either a string or astropy.coordinates") + if isinstance(value, str): + c = commons.parse_coordinates(value) + return c + else: + return value + + def __get_observationid_condition(self, *, value=None): + condition = "" + if(value is not None): + if(not isinstance(value, str)): + raise ValueError("observation_id must be string") + else: + condition = " AND observationid LIKE '"+value.lower()+"' " + return condition + + def __get_callevel_condition(self, cal_level): + condition = "" + if(cal_level is not None): + if(isinstance(cal_level, str) and cal_level == 'Top'): + condition = " AND max_cal_level=calibrationlevel " + elif(isinstance(cal_level, int)): + condition = " AND calibrationlevel=" +\ + str(cal_level)+" " + else: + raise ValueError("cal_level must be either " + "'Top' or an integer") + return condition + + def __get_public_condition(self, only_public): + condition = "" + if(not isinstance(only_public, bool)): + raise ValueError("only_public must be boolean") + elif(only_public is True): + condition = " AND public='true' " + return condition + + def __get_plane_dataproducttype_condition(self, *, prod_type=None): + condition = "" + if(prod_type is not None): + if(not isinstance(prod_type, str)): + raise ValueError("prod_type must be string") + elif(str(prod_type).lower() not in self.PLANE_DATAPRODUCT_TYPES): + raise ValueError("prod_type must be one of: " + + str(', '.join(self.PLANE_DATAPRODUCT_TYPES))) + else: + condition = " AND dataproducttype LIKE '"+prod_type.lower() + \ + "' " + return condition + + def __get_instrument_name_condition(self, *, value=None): + condition = "" + if(value is not None): + if(not isinstance(value, str)): + raise ValueError("instrument_name must be string") + elif(str(value).upper() not in self.INSTRUMENT_NAMES): + raise ValueError("instrument_name must be one of: " + + str(', '.join(self.INSTRUMENT_NAMES))) + else: + condition = " AND instrument_name LIKE '"+value.upper()+"' " + return condition + + def __get_filter_name_condition(self, *, value=None): + condition = "" + if(value is not None): + if(not isinstance(value, str)): + raise ValueError("filter_name must be string") + + else: + condition = " AND energy_bandpassname ILIKE '%"+value+"%' " + return condition + + def __get_proposal_id_condition(self, *, value=None): + condition = "" + if(value is not None): + if(not isinstance(value, str)): + raise ValueError("proposal_id must be string") + + else: + condition = " AND proposal_id ILIKE '%"+value+"%' " + return condition + + def __get_artifact_producttype_condition(self, *, product_type=None): + condition = "" + if(product_type is not None): + if(not isinstance(product_type, str)): + raise ValueError("product_type must be string") + elif(product_type not in self.ARTIFACT_PRODUCT_TYPES): + raise ValueError("product_type must be one of: " + + str(', '.join(self.ARTIFACT_PRODUCT_TYPES))) + else: + condition = " AND producttype LIKE '"+product_type+"'" + return condition + + @staticmethod + def is_gz_file(filepath): + with open(filepath, 'rb') as test_f: + return binascii.hexlify(test_f.read(2)) == b'1f8b' + + @staticmethod + def gzip_uncompress(input_file, output_file): + with open(output_file, 'wb') as f_out, gzip.open(input_file, + 'rb') as f_in: + shutil.copyfileobj(f_in, f_out) + + @staticmethod + def gzip_uncompress_and_rename_single_file(input_file): + output_dir = os.path.dirname(input_file) + file = os.path.basename(input_file) + output_decompressed_file = output_dir + os.sep + file + "_decompressed" + JwstClass.gzip_uncompress(input_file=input_file, + output_file=output_decompressed_file) + # Remove uncompressed file and rename decompressed file to the + # original one + os.remove(input_file) + if file.lower().endswith(".gz"): + # remove .gz + new_file_name = file[:len(file)-3] + output = output_dir + os.sep + new_file_name + else: + output = input_file + os.rename(output_decompressed_file, output) + return output + + @staticmethod + def get_decoded_string(str): + try: + return str.decode('utf-8') + # return str + except (UnicodeDecodeError, AttributeError): + return str + + +Jwst = JwstClass() diff --git a/astroquery/esa/jwst/data_access.py b/astroquery/esa/jwst/data_access.py new file mode 100644 index 0000000000..310a7133d9 --- /dev/null +++ b/astroquery/esa/jwst/data_access.py @@ -0,0 +1,28 @@ +# Licensed under a 3-clause BSD style license - see LICENSE.rst +""" +================= +eJWST Data Access +================= + +European Space Astronomy Centre (ESAC) +European Space Agency (ESA) + +""" + +from astropy.utils import data + +__all__ = ['JwstDataHandler'] + + +class JwstDataHandler: + def __init__(self, base_url=None): + if base_url is None: + self.base_url = "http://jwstdummydata.com" + else: + self.base_url = base_url + + def download_file(self, url): + return data.download_file(url, cache=True) + + def clear_download_cache(self): + data.clear_download_cache() diff --git a/astroquery/esa/jwst/tests/DummyDataHandler.py b/astroquery/esa/jwst/tests/DummyDataHandler.py new file mode 100644 index 0000000000..f82ac9d576 --- /dev/null +++ b/astroquery/esa/jwst/tests/DummyDataHandler.py @@ -0,0 +1,60 @@ +# Licensed under a 3-clause BSD style license - see LICENSE.rst +""" +======================== +eJWST Dummy Data Handler +======================== + +European Space Astronomy Centre (ESAC) +European Space Agency (ESA) + +""" + + +class DummyDataHandler: + + def __init__(self): + self.base_url = "http://test/data?" + self.__invokedMethod = None + self.__parameters = {} + + def reset(self): + self.__parameters = {} + self.__invokedMethod = None + + def check_call(self, method_name, parameters): + self.check_method(method_name) + self.check_parameters(parameters, method_name) + + def check_method(self, method): + if method == self.__invokedMethod: + return + else: + raise ValueError(f"Method '+{str(method)} " + f"' not invoked. (Invoked method is '" + + f"{str(self.__invokedMethod)}')") + + def check_parameters(self, parameters, method_name): + if parameters is None: + return len(self.__parameters) == 0 + if len(parameters) != len(self.__parameters): + raise ValueError(f"Wrong number of parameters for " + f"method '{method_name}'. " + f"Found: {len(self.__parameters)}. " + f"Expected {len(parameters)}") + for key in parameters: + if key in self.__parameters: + # check value + if self.__parameters[key] != parameters[key]: + raise ValueError(f"Wrong '{method_name}' parameter " + f"value for method '{key}'. " + f"Found: '{self.__parameters[key]}'. " + f"Expected: '{parameters[key]}'") + else: + raise ValueError(f"Parameter '{str(key)}' not found for " + f"method '{method_name}'") + return False + + def download_file(self, url=None): + self.__invokedMethod = 'download_file' + self.__parameters['url'] = url + return None diff --git a/astroquery/esa/jwst/tests/DummyTapHandler.py b/astroquery/esa/jwst/tests/DummyTapHandler.py new file mode 100644 index 0000000000..7984bf0fb4 --- /dev/null +++ b/astroquery/esa/jwst/tests/DummyTapHandler.py @@ -0,0 +1,240 @@ +# Licensed under a 3-clause BSD style license - see LICENSE.rst +""" +======================= +eJWST Dummy Tap Handler +======================= + +European Space Astronomy Centre (ESAC) +European Space Agency (ESA) + +""" + +from astroquery.utils.tap.model.job import Job + + +class DummyTapHandler: + + def __init__(self): + self.__invokedMethod = None + self.__parameters = {} + self.__dummy_results = "dummy_results" + self.__job = Job(async_job=False) + self.__job.set_results(self.__dummy_results) + + def reset(self): + self.__parameters = {} + self.__invokedMethod = None + self.__dummy_results = "dummy_results" + self.__job = Job(async_job=False) + self.__job.set_results(self.__dummy_results) + + def set_job(self, job): + self.__job = job + + def get_job(self): + return self.__job + + def check_call(self, method_name, parameters): + self.check_method(method_name) + self.check_parameters(parameters, method_name) + + def check_method(self, method): + if method == self.__invokedMethod: + return + else: + raise ValueError(f"Method '+{str(method)}" + + f"' not invoked. (Invoked method is '" + + f"{str(self.__invokedMethod)}"+"')") + + def check_parameters(self, parameters, method_name): + print("FOUND") + print(self.__parameters) + print("EXPECTED") + print(parameters) + if parameters is None: + return len(self.__parameters) == 0 + if len(parameters) != len(self.__parameters): + raise ValueError(f"Wrong number of parameters " + f"for method '{method_name}'" + f" Found: {len(self.__parameters)}. " + f"Expected {len(parameters)}") + for key in parameters: + if key in self.__parameters: + # check value + if self.__parameters[key] != parameters[key]: + raise ValueError(f"Wrong {key} parameter value for " + f" method '{method_name}'. " + f"Found: {self.__parameters[key]}. " + f"Expected: {parameters[key]}") + else: + raise ValueError(f"Parameter '{str(key)}' not found " + f"for method '{method_name}'") + return False + + def load_tables(self, only_names=False, include_shared_tables=False, + verbose=False): + self.__invokedMethod = 'load_tables' + self.__parameters['only_names'] = only_names + self.__parameters['include_shared_tables'] = include_shared_tables + self.__parameters['verbose'] = verbose + return None + + def load_table(self, table, verbose=False): + self.__invokedMethod = 'load_table' + self.__parameters['table'] = table + self.__parameters['verbose'] = verbose + return None + + def launch_job(self, query, name=None, output_file=None, + output_format="votable", verbose=False, dump_to_file=False, + upload_resource=None, upload_table_name=None): + self.__invokedMethod = 'launch_job' + self.__parameters['query'] = query + self.__parameters['name'] = name + self.__parameters['output_file'] = output_file + self.__parameters['output_format'] = output_format + self.__parameters['verbose'] = verbose + self.__parameters['dump_to_file'] = dump_to_file + self.__parameters['upload_resource'] = upload_resource + self.__parameters['upload_table_name'] = upload_table_name + return self.__job + + def launch_job_async(self, query, name=None, output_file=None, + output_format="votable", verbose=False, + dump_to_file=False, background=False, + upload_resource=None, upload_table_name=None): + self.__invokedMethod = 'launch_job_async' + self.__parameters['query'] = query + self.__parameters['name'] = name + self.__parameters['output_file'] = output_file + self.__parameters['output_format'] = output_format + self.__parameters['verbose'] = verbose + self.__parameters['dump_to_file'] = dump_to_file + self.__parameters['background'] = background + self.__parameters['upload_resource'] = upload_resource + self.__parameters['upload_table_name'] = upload_table_name + return self.__job + + def load_async_job(self, jobid=None, name=None, verbose=False): + self.__invokedMethod = 'load_async_job' + self.__parameters['jobid'] = jobid + self.__parameters['name'] = name + self.__parameters['verbose'] = verbose + return None + + def search_async_jobs(self, jobfilter=None, verbose=False): + self.__invokedMethod = 'search_async_jobs' + self.__parameters['jobfilter'] = jobfilter + self.__parameters['verbose'] = verbose + return None + + def list_async_jobs(self, verbose=False): + self.__invokedMethod = 'list_async_jobs' + self.__parameters['verbose'] = verbose + return None + + def query_object(self, coordinate, radius=None, width=None, height=None, + verbose=False): + self.__invokedMethod = 'query_object' + self.__parameters['coordinate'] = coordinate + self.__parameters['radius'] = radius + self.__parameters['width'] = width + self.__parameters['height'] = height + self.__parameters['verbose'] = verbose + return None + + def query_object_async(self, coordinate, radius=None, width=None, + height=None, verbose=False): + self.__invokedMethod = 'query_object_async' + self.__parameters['coordinate'] = coordinate + self.__parameters['radius'] = radius + self.__parameters['width'] = width + self.__parameters['height'] = height + self.__parameters['verbose'] = verbose + return None + + def query_region(self, coordinate, radius=None, width=None): + self.__invokedMethod = 'query_region' + self.__parameters['coordinate'] = coordinate + self.__parameters['radius'] = radius + self.__parameters['width'] = width + return None + + def query_region_async(self, coordinate, radius=None, width=None): + self.__invokedMethod = 'query_region_async' + self.__parameters['coordinate'] = coordinate + self.__parameters['radius'] = radius + self.__parameters['width'] = width + return None + + def get_images(self, coordinate): + self.__invokedMethod = 'get_images' + self.__parameters['coordinate'] = coordinate + return None + + def get_images_async(self, coordinate): + self.__invokedMethod = 'get_images_sync' + self.__parameters['coordinate'] = coordinate + return None + + def cone_search(self, coordinate, radius, output_file=None, + output_format="votable", verbose=False, + dump_to_file=False): + self.__invokedMethod = 'cone_search' + self.__parameters['coordinate'] = coordinate + self.__parameters['radius'] = radius + self.__parameters['output_file'] = output_file + self.__parameters['output_format'] = output_format + self.__parameters['verbose'] = verbose + self.__parameters['dump_to_file'] = dump_to_file + return None + + def cone_search_async(self, coordinate, radius, background=False, + output_file=None, output_format="votable", + verbose=False, dump_to_file=False): + self.__invokedMethod = 'cone_search_async' + self.__parameters['coordinate'] = coordinate + self.__parameters['radius'] = radius + self.__parameters['background'] = background + self.__parameters['output_file'] = output_file + self.__parameters['output_format'] = output_format + self.__parameters['verbose'] = verbose + self.__parameters['dump_to_file'] = dump_to_file + return None + + def remove_jobs(self, jobs_list, verbose=False): + self.__invokedMethod = 'remove_jobs' + self.__parameters['jobs_list'] = jobs_list + self.__parameters['verbose'] = verbose + return None + + def save_results(self, job, verbose=False): + self.__invokedMethod = 'save_results' + self.__parameters['job'] = job + self.__parameters['verbose'] = verbose + return None + + def login(self, user=None, password=None, credentials_file=None, + verbose=False): + self.__invokedMethod = 'login' + self.__parameters['user'] = verbose + self.__parameters['password'] = verbose + self.__parameters['credentials_file'] = verbose + self.__parameters['verbose'] = verbose + return None + + def login_gui(self, verbose=False): + self.__invokedMethod = 'login_gui' + self.__parameters['verbose'] = verbose + return None + + def logout(self, verbose=False): + self.__invokedMethod = 'logout' + self.__parameters['verbose'] = verbose + return None + + def load_data(self, params_dict, output_file=None, verbose=False): + self.__invokedMethod = 'load_data' + self.__parameters['params_dict'] = params_dict + self.__parameters['output_file'] = output_file + self.__parameters['verbose'] = verbose diff --git a/astroquery/esa/jwst/tests/__init__.py b/astroquery/esa/jwst/tests/__init__.py new file mode 100644 index 0000000000..b56c49280d --- /dev/null +++ b/astroquery/esa/jwst/tests/__init__.py @@ -0,0 +1,10 @@ +# Licensed under a 3-clause BSD style license - see LICENSE.rst +""" +=============== +eJWST TEST Init +=============== + +European Space Astronomy Centre (ESAC) +European Space Agency (ESA) + +""" diff --git a/astroquery/esa/jwst/tests/data/job_1.vot b/astroquery/esa/jwst/tests/data/job_1.vot new file mode 100644 index 0000000000..695342347d --- /dev/null +++ b/astroquery/esa/jwst/tests/data/job_1.vot @@ -0,0 +1,29 @@ + + + + + + + +alpha + + +delta + + +source_id + + +table1_oid + + + + +AD/wAAAAAAAAQAAAAAAAAAAAAAABYQAAAAEAQAgAAAAAAABAEAAAAAAAAAAAAAFi +AAAAAgBAFAAAAAAAAEAYAAAAAAAAAAAAAWMAAAAD + + + +
+
+
diff --git a/astroquery/esa/jwst/tests/data/single_product_retrieval.tar b/astroquery/esa/jwst/tests/data/single_product_retrieval.tar new file mode 100644 index 0000000000..49e635b24b Binary files /dev/null and b/astroquery/esa/jwst/tests/data/single_product_retrieval.tar differ diff --git a/astroquery/esa/jwst/tests/data/single_product_retrieval_1.fits b/astroquery/esa/jwst/tests/data/single_product_retrieval_1.fits new file mode 100644 index 0000000000..4132249fe6 Binary files /dev/null and b/astroquery/esa/jwst/tests/data/single_product_retrieval_1.fits differ diff --git a/astroquery/esa/jwst/tests/data/single_product_retrieval_2.fits.gz b/astroquery/esa/jwst/tests/data/single_product_retrieval_2.fits.gz new file mode 100644 index 0000000000..44af5123ad Binary files /dev/null and b/astroquery/esa/jwst/tests/data/single_product_retrieval_2.fits.gz differ diff --git a/astroquery/esa/jwst/tests/data/single_product_retrieval_3.fits.zip b/astroquery/esa/jwst/tests/data/single_product_retrieval_3.fits.zip new file mode 100644 index 0000000000..c16a934b7f Binary files /dev/null and b/astroquery/esa/jwst/tests/data/single_product_retrieval_3.fits.zip differ diff --git a/astroquery/esa/jwst/tests/data/test_query_by_target_name_ned.vot b/astroquery/esa/jwst/tests/data/test_query_by_target_name_ned.vot new file mode 100644 index 0000000000..2c62ceac68 --- /dev/null +++ b/astroquery/esa/jwst/tests/data/test_query_by_target_name_ned.vot @@ -0,0 +1,123 @@ + + + + + + + Main information about object (Cone Search results) + + + + A sequential object number applicable to this list only. + + + + + NED preferred name for the object + + + + + Right Ascension in degrees (Equatorial J2000.0) + + + + + Declination in degrees (Equatorial J2000.0) + + + + + NED's Preferred Object Type: G,GPair,GTrpl,GGroup,GClstr,QSO,AbLS + ,RadioS,IrS,EmLS,UvES,XrayS,SN + + + + + Velocity in km/sec, based on known heliocentric redshift + + + + + Heliocentric redshift for the object, if it exists in NED + + + + + Quality flag for known heliocentric redshift + + + + + NED's Basic Data magnitude and filter, generally in an optical + band. + + + + + Angular separation of the object coordinates and the search + coordinate. + + + + + Number of literature references in NED + + + + + Number of catalog notes in NED + + + + + Number of photometric data points stored by NED for the object + + + + + Number of position data points stored by NED for the object + + + + + Number of redshift data points stored by NED for the object. + + + + + Number of diameter data points stored by NED for the object. + + + + + Number of NED associations for the object. + + + + + + + + + + + + + + + + + + + + +
1MESSIER 001 83.63321 22.01446SNR + + + + + 13626118020
+
+
diff --git a/astroquery/esa/jwst/tests/data/test_query_by_target_name_ned_query.txt b/astroquery/esa/jwst/tests/data/test_query_by_target_name_ned_query.txt new file mode 100644 index 0000000000..0d4ba7ab69 --- /dev/null +++ b/astroquery/esa/jwst/tests/data/test_query_by_target_name_ned_query.txt @@ -0,0 +1 @@ +SELECT DISTANCE(POINT('ICRS',target_ra,target_dec), POINT('ICRS',83.63321000000002,22.01446)) AS dist, observationid, calibrationlevel, public, dataproducttype, instrument_name, energy_bandpassname, target_name, target_ra, target_dec, position_bounds_center, position_bounds_spoly FROM jwst.main WHERE CONTAINS(POINT('ICRS',target_ra,target_dec),CIRCLE('ICRS',83.63321000000002,22.01446, 5.0))=1 AND max_cal_level=calibrationlevel ORDER BY dist ASC \ No newline at end of file diff --git a/astroquery/esa/jwst/tests/data/test_query_by_target_name_simbad.vot b/astroquery/esa/jwst/tests/data/test_query_by_target_name_simbad.vot new file mode 100644 index 0000000000..4dccb88b3c --- /dev/null +++ b/astroquery/esa/jwst/tests/data/test_query_by_target_name_simbad.vot @@ -0,0 +1,85 @@ + + + + + + + Simbad script executed on 2020.04.27CEST17:47:25 + + + + Main identifier for an object + + + + + + Right ascension + + + + + Declination + + + + + Right ascension precision + + + + + Declination precision + + + + + Coordinate error major axis + + + + + Coordinate error minor axis + + + + + Coordinate error angle + + + + + Coordinate quality + + + + + Wavelength class for the origin of the coordinates (R,I,V,U,X,G) + + + + + Coordinate reference + + + + + + + + + + + + + + + + + +
M 105 34 31.94+22 00 52.266 + + 0CR2011A&A...533A..10L
+
+
diff --git a/astroquery/esa/jwst/tests/data/test_query_by_target_name_simbad_ned_error.vot b/astroquery/esa/jwst/tests/data/test_query_by_target_name_simbad_ned_error.vot new file mode 100644 index 0000000000..20317d7c8b --- /dev/null +++ b/astroquery/esa/jwst/tests/data/test_query_by_target_name_simbad_ned_error.vot @@ -0,0 +1,371 @@ + + + + + + + The APASS catalog + + + + Record number assigned by the VizieR team. Should Not be used for + identification. + + + + + Right ascension in decimal degrees (J2000) + + + + + Declination in decimal degrees (J2000) + + + + + [0/2.4] RA uncertainty + + + + + [0/2.4] DEC uncertainty + + + + + [20110001/9999988888] Field name + + + + + [2/387] Number of observed nights + + + + + [2/3476] Number of images for this field, usually nobs*5 + + + + + [-7.5/13]? B-V color index + + + + + + [0/10.1]? B-V uncertainty + + + + + + [5.5/27.4]? Johnson V-band magnitude + + + + + + [0/7]? Vmag uncertainty + + + + + + [5.4/27.3]? Johnson B-band magnitude + + + + + + [0/10]? Bmag uncertainty + + + + + + [5.9/24.2]? g'-band AB magnitude, Sloan filter + + + + + + [0/9.7]? g'mag uncertainty + + + + + + [5.1/23.9]? r'-band AB magnitude, Sloan filter + + + + + + [0/6.5]? r'mag uncertainty + + + + + + [4.2/29.1]? i'-band AB magnitude, Sloan filter + + + + + + [0/9.6]? i'mag uncertainty + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
25168643 83.652835 21.987047 1.383 1.0762015108736 0.822 0.32815.468 0.06816.290 0.32015.648 0.000 + + 14.602 0.000
25168645 83.655449 21.999445 1.187 0.90920151087627 0.930 0.05614.773 0.03115.702 0.04715.332 0.17014.651 0.18314.072 0.036
25169359 83.625453 21.982948 0.421 0.32320151087630 0.902 0.06813.885 0.04314.787 0.05414.242 0.06713.536 0.05513.330 0.034
25169364 83.634518 22.009752 0.982 0.6212012108823 0.693 0.19915.461 0.00016.154 0.199 + + 15.099 0.000 + +
25169365 83.630254 22.017264 0.733 1.24020151087411 0.678 0.02414.622 0.02315.300 0.00615.054 0.06414.279 0.00013.756 0.095
25169366 83.631052 22.016343 1.459 1.3852015108747 0.880 0.00014.487 0.00015.367 0.00015.049 0.03814.383 0.094 + +
25169367 83.642783 22.033328 0.455 0.80220151087630 0.869 0.06013.969 0.02614.838 0.05514.364 0.06113.585 0.04913.387 0.058
25169368 83.659578 22.029752 1.151 0.81720151087417 0.644 0.10414.839 0.08015.483 0.06615.100 0.02214.548 0.13914.296 0.011
25169369 83.667646 22.021452 1.180 0.4532015108739 1.512 0.00015.483 0.00016.995 0.00016.115 0.01315.103 0.14714.798 0.000
25169370 83.620278 22.026738 0.746 1.3562015108726 0.691 0.11515.125 0.11215.816 0.02715.673 0.00015.053 0.000 + +
25169372 83.610487 22.037842 0.534 1.4032015108827 0.891 0.04214.357 0.03415.248 0.02414.857 0.00014.141 0.00013.807 0.000
+
+
diff --git a/astroquery/esa/jwst/tests/data/test_query_by_target_name_simbad_query.txt b/astroquery/esa/jwst/tests/data/test_query_by_target_name_simbad_query.txt new file mode 100644 index 0000000000..a4997da31f --- /dev/null +++ b/astroquery/esa/jwst/tests/data/test_query_by_target_name_simbad_query.txt @@ -0,0 +1 @@ +SELECT DISTANCE(POINT('ICRS',target_ra,target_dec), POINT('ICRS',83.63309095802303,22.0144947866347)) AS dist, observationid, calibrationlevel, public, dataproducttype, instrument_name, energy_bandpassname, target_name, target_ra, target_dec, position_bounds_center, position_bounds_spoly FROM jwst.main WHERE CONTAINS(POINT('ICRS',target_ra,target_dec),CIRCLE('ICRS',83.63309095802303,22.0144947866347, 5.0))=1 AND max_cal_level=calibrationlevel ORDER BY dist ASC \ No newline at end of file diff --git a/astroquery/esa/jwst/tests/data/test_query_by_target_name_vizier.vot b/astroquery/esa/jwst/tests/data/test_query_by_target_name_vizier.vot new file mode 100644 index 0000000000..20317d7c8b --- /dev/null +++ b/astroquery/esa/jwst/tests/data/test_query_by_target_name_vizier.vot @@ -0,0 +1,371 @@ + + + + + + + The APASS catalog + + + + Record number assigned by the VizieR team. Should Not be used for + identification. + + + + + Right ascension in decimal degrees (J2000) + + + + + Declination in decimal degrees (J2000) + + + + + [0/2.4] RA uncertainty + + + + + [0/2.4] DEC uncertainty + + + + + [20110001/9999988888] Field name + + + + + [2/387] Number of observed nights + + + + + [2/3476] Number of images for this field, usually nobs*5 + + + + + [-7.5/13]? B-V color index + + + + + + [0/10.1]? B-V uncertainty + + + + + + [5.5/27.4]? Johnson V-band magnitude + + + + + + [0/7]? Vmag uncertainty + + + + + + [5.4/27.3]? Johnson B-band magnitude + + + + + + [0/10]? Bmag uncertainty + + + + + + [5.9/24.2]? g'-band AB magnitude, Sloan filter + + + + + + [0/9.7]? g'mag uncertainty + + + + + + [5.1/23.9]? r'-band AB magnitude, Sloan filter + + + + + + [0/6.5]? r'mag uncertainty + + + + + + [4.2/29.1]? i'-band AB magnitude, Sloan filter + + + + + + [0/9.6]? i'mag uncertainty + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
25168643 83.652835 21.987047 1.383 1.0762015108736 0.822 0.32815.468 0.06816.290 0.32015.648 0.000 + + 14.602 0.000
25168645 83.655449 21.999445 1.187 0.90920151087627 0.930 0.05614.773 0.03115.702 0.04715.332 0.17014.651 0.18314.072 0.036
25169359 83.625453 21.982948 0.421 0.32320151087630 0.902 0.06813.885 0.04314.787 0.05414.242 0.06713.536 0.05513.330 0.034
25169364 83.634518 22.009752 0.982 0.6212012108823 0.693 0.19915.461 0.00016.154 0.199 + + 15.099 0.000 + +
25169365 83.630254 22.017264 0.733 1.24020151087411 0.678 0.02414.622 0.02315.300 0.00615.054 0.06414.279 0.00013.756 0.095
25169366 83.631052 22.016343 1.459 1.3852015108747 0.880 0.00014.487 0.00015.367 0.00015.049 0.03814.383 0.094 + +
25169367 83.642783 22.033328 0.455 0.80220151087630 0.869 0.06013.969 0.02614.838 0.05514.364 0.06113.585 0.04913.387 0.058
25169368 83.659578 22.029752 1.151 0.81720151087417 0.644 0.10414.839 0.08015.483 0.06615.100 0.02214.548 0.13914.296 0.011
25169369 83.667646 22.021452 1.180 0.4532015108739 1.512 0.00015.483 0.00016.995 0.00016.115 0.01315.103 0.14714.798 0.000
25169370 83.620278 22.026738 0.746 1.3562015108726 0.691 0.11515.125 0.11215.816 0.02715.673 0.00015.053 0.000 + +
25169372 83.610487 22.037842 0.534 1.4032015108827 0.891 0.04214.357 0.03415.248 0.02414.857 0.00014.141 0.00013.807 0.000
+
+
diff --git a/astroquery/esa/jwst/tests/data/test_query_by_target_name_vizier_error.vot b/astroquery/esa/jwst/tests/data/test_query_by_target_name_vizier_error.vot new file mode 100644 index 0000000000..4dccb88b3c --- /dev/null +++ b/astroquery/esa/jwst/tests/data/test_query_by_target_name_vizier_error.vot @@ -0,0 +1,85 @@ + + + + + + + Simbad script executed on 2020.04.27CEST17:47:25 + + + + Main identifier for an object + + + + + + Right ascension + + + + + Declination + + + + + Right ascension precision + + + + + Declination precision + + + + + Coordinate error major axis + + + + + Coordinate error minor axis + + + + + Coordinate error angle + + + + + Coordinate quality + + + + + Wavelength class for the origin of the coordinates (R,I,V,U,X,G) + + + + + Coordinate reference + + + + + + + + + + + + + + + + + +
M 105 34 31.94+22 00 52.266 + + 0CR2011A&A...533A..10L
+
+
diff --git a/astroquery/esa/jwst/tests/data/test_query_by_target_name_vizier_query.txt b/astroquery/esa/jwst/tests/data/test_query_by_target_name_vizier_query.txt new file mode 100644 index 0000000000..56c679b566 --- /dev/null +++ b/astroquery/esa/jwst/tests/data/test_query_by_target_name_vizier_query.txt @@ -0,0 +1 @@ +SELECT DISTANCE(POINT('ICRS',target_ra,target_dec), POINT('ICRS',83.62545300000001,21.982948)) AS dist, observationid, calibrationlevel, public, dataproducttype, instrument_name, energy_bandpassname, target_name, target_ra, target_dec, position_bounds_center, position_bounds_spoly FROM jwst.main WHERE CONTAINS(POINT('ICRS',target_ra,target_dec),CIRCLE('ICRS',83.62545300000001,21.982948, 5.0))=1 AND max_cal_level=calibrationlevel ORDER BY dist ASC \ No newline at end of file diff --git a/astroquery/esa/jwst/tests/data/three_products_retrieval.tar b/astroquery/esa/jwst/tests/data/three_products_retrieval.tar new file mode 100644 index 0000000000..0b753ff7aa Binary files /dev/null and b/astroquery/esa/jwst/tests/data/three_products_retrieval.tar differ diff --git a/astroquery/esa/jwst/tests/setup_package.py b/astroquery/esa/jwst/tests/setup_package.py new file mode 100644 index 0000000000..f82d0059d4 --- /dev/null +++ b/astroquery/esa/jwst/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.jwst.tests': paths} diff --git a/astroquery/esa/jwst/tests/test_jwstdata.py b/astroquery/esa/jwst/tests/test_jwstdata.py new file mode 100644 index 0000000000..4bc92007b8 --- /dev/null +++ b/astroquery/esa/jwst/tests/test_jwstdata.py @@ -0,0 +1,61 @@ +# Licensed under a 3-clause BSD style license - see LICENSE.rst +""" +================ +eJWST DATA Tests +================ + +European Space Astronomy Centre (ESAC) +European Space Agency (ESA) + +""" +import os +import pytest + +from astroquery.esa.jwst.tests.DummyTapHandler import DummyTapHandler +from astroquery.esa.jwst.tests.DummyDataHandler import DummyDataHandler + +from astroquery.esa.jwst.core import JwstClass + + +def data_path(filename): + data_dir = os.path.join(os.path.dirname(__file__), 'data') + return os.path.join(data_dir, filename) + + +def get_product_mock(params, *args, **kwargs): + if kwargs and kwargs.get('file_name'): + return "00000000-0000-0000-8740-65e2827c9895" + else: + return "jw00617023001_02102_00001_nrcb4_uncal.fits" + + +@pytest.fixture(autouse=True) +def get_product_request(request): + mp = request.getfixturevalue("monkeypatch") + mp.setattr(JwstClass, '_query_get_product', get_product_mock) + return mp + + +class TestData: + + def test_get_product(self): + dummyTapHandler = DummyTapHandler() + jwst = JwstClass(tap_plus_handler=dummyTapHandler) + # default parameters + parameters = {} + parameters['artifact_id'] = None + with pytest.raises(ValueError) as err: + jwst.get_product() + assert "Missing required argument: 'artifact_id'" in err.value.args[0] + # test with parameters + dummyTapHandler.reset() + parameters = {} + params_dict = {} + params_dict['RETRIEVAL_TYPE'] = 'PRODUCT' + params_dict['DATA_RETRIEVAL_ORIGIN'] = 'ASTROQUERY' + params_dict['ARTIFACTID'] = '00000000-0000-0000-8740-65e2827c9895' + parameters['params_dict'] = params_dict + parameters['output_file'] = 'jw00617023001_02102_00001_nrcb4_uncal.fits' + parameters['verbose'] = False + jwst.get_product(artifact_id='00000000-0000-0000-8740-65e2827c9895') + dummyTapHandler.check_call('load_data', parameters) diff --git a/astroquery/esa/jwst/tests/test_jwsttap.py b/astroquery/esa/jwst/tests/test_jwsttap.py new file mode 100644 index 0000000000..ebbc362d41 --- /dev/null +++ b/astroquery/esa/jwst/tests/test_jwsttap.py @@ -0,0 +1,970 @@ +# Licensed under a 3-clause BSD style license - see LICENSE.rst +""" +=============== +eJWST TAP tests +=============== + +European Space Astronomy Centre (ESAC) +European Space Agency (ESA) + +""" +import os +import shutil +from unittest.mock import MagicMock + +import astropy.units as u +import numpy as np +import pytest +from astropy import units +from astropy.coordinates.sky_coordinate import SkyCoord +from astropy.table import Table +from astropy.units import Quantity + +from astroquery.esa.jwst import JwstClass +from astroquery.esa.jwst.tests.DummyTapHandler import DummyTapHandler +from astroquery.ipac.ned import Ned +from astroquery.simbad import Simbad +from astroquery.utils import TableList +from astroquery.utils.tap.conn.tests.DummyConnHandler import DummyConnHandler +from astroquery.utils.tap.conn.tests.DummyResponse import DummyResponse +from astroquery.utils.tap.core import TapPlus +from astroquery.utils.tap.xmlparser import utils +from astroquery.vizier import Vizier + +from astroquery.esa.jwst import conf + + +def data_path(filename): + data_dir = os.path.join(os.path.dirname(__file__), 'data') + return os.path.join(data_dir, filename) + + +def get_plane_id_mock(url, *args, **kwargs): + return ['00000000-0000-0000-879d-ae91fa2f43e2'], 3 + + +@pytest.fixture(autouse=True) +def plane_id_request(request): + mp = request.getfixturevalue("monkeypatch") + mp.setattr(JwstClass, '_get_plane_id', get_plane_id_mock) + return mp + + +def get_associated_planes_mock(url, *args, **kwargs): + if kwargs.get("max_cal_level") == 2: + return "('00000000-0000-0000-879d-ae91fa2f43e2')" + else: + return planeids + + +@pytest.fixture(autouse=True) +def associated_planes_request(request): + mp = request.getfixturevalue("monkeypatch") + mp.setattr(JwstClass, '_get_associated_planes', get_associated_planes_mock) + return mp + + +def get_product_mock(params, *args, **kwargs): + if('file_name' in kwargs and kwargs.get('file_name') == 'file_name_id'): + return "00000000-0000-0000-8740-65e2827c9895" + else: + return "jw00617023001_02102_00001_nrcb4_uncal.fits" + + +@pytest.fixture(autouse=True) +def get_product_request(request): + mp = request.getfixturevalue("monkeypatch") + mp.setattr(JwstClass, '_query_get_product', get_product_mock) + return mp + + +planeids = "('00000000-0000-0000-879d-ae91fa2f43e2', '00000000-0000-0000-9852-a9fa8c63f7ef')" + + +class TestTap: + + def test_load_tables(self): + dummyTapHandler = DummyTapHandler() + tap = JwstClass(tap_plus_handler=dummyTapHandler) + # default parameters + parameters = {} + parameters['only_names'] = False + parameters['include_shared_tables'] = False + parameters['verbose'] = False + tap.load_tables() + dummyTapHandler.check_call('load_tables', parameters) + # test with parameters + dummyTapHandler.reset() + parameters = {} + parameters['only_names'] = True + parameters['include_shared_tables'] = True + parameters['verbose'] = True + tap.load_tables(only_names=True, include_shared_tables=True, verbose=True) + dummyTapHandler.check_call('load_tables', parameters) + + def test_load_table(self): + dummyTapHandler = DummyTapHandler() + tap = JwstClass(tap_plus_handler=dummyTapHandler) + # default parameters + parameters = {} + parameters['table'] = 'table' + parameters['verbose'] = False + tap.load_table('table') + dummyTapHandler.check_call('load_table', parameters) + # test with parameters + dummyTapHandler.reset() + parameters = {} + parameters['table'] = 'table' + parameters['verbose'] = True + tap.load_table('table', verbose=True) + dummyTapHandler.check_call('load_table', parameters) + + def test_launch_sync_job(self): + dummyTapHandler = DummyTapHandler() + tap = JwstClass(tap_plus_handler=dummyTapHandler) + query = "query" + # default parameters + parameters = {} + parameters['query'] = query + parameters['name'] = None + parameters['output_file'] = None + parameters['output_format'] = 'votable' + parameters['verbose'] = False + parameters['dump_to_file'] = False + parameters['upload_resource'] = None + parameters['upload_table_name'] = None + tap.launch_job(query) + dummyTapHandler.check_call('launch_job', parameters) + # test with parameters + dummyTapHandler.reset() + name = 'name' + output_file = 'output' + output_format = 'format' + verbose = True + dump_to_file = True + upload_resource = 'upload_res' + upload_table_name = 'upload_table' + parameters['query'] = query + parameters['name'] = name + parameters['output_file'] = output_file + parameters['output_format'] = output_format + parameters['verbose'] = verbose + parameters['dump_to_file'] = dump_to_file + parameters['upload_resource'] = upload_resource + parameters['upload_table_name'] = upload_table_name + tap.launch_job(query, + name=name, + output_file=output_file, + output_format=output_format, + verbose=verbose, + dump_to_file=dump_to_file, + upload_resource=upload_resource, + upload_table_name=upload_table_name) + dummyTapHandler.check_call('launch_job', parameters) + + def test_launch_async_job(self): + dummyTapHandler = DummyTapHandler() + tap = JwstClass(tap_plus_handler=dummyTapHandler) + query = "query" + # default parameters + parameters = {} + parameters['query'] = query + parameters['name'] = None + parameters['output_file'] = None + parameters['output_format'] = 'votable' + parameters['verbose'] = False + parameters['dump_to_file'] = False + parameters['background'] = False + parameters['upload_resource'] = None + parameters['upload_table_name'] = None + tap.launch_job(query, async_job=True) + dummyTapHandler.check_call('launch_job_async', parameters) + # test with parameters + dummyTapHandler.reset() + name = 'name' + output_file = 'output' + output_format = 'format' + verbose = True + dump_to_file = True + background = True + upload_resource = 'upload_res' + upload_table_name = 'upload_table' + parameters['query'] = query + parameters['name'] = name + parameters['output_file'] = output_file + parameters['output_format'] = output_format + parameters['verbose'] = verbose + parameters['dump_to_file'] = dump_to_file + parameters['background'] = background + parameters['upload_resource'] = upload_resource + parameters['upload_table_name'] = upload_table_name + tap.launch_job(query, + name=name, + output_file=output_file, + output_format=output_format, + verbose=verbose, + dump_to_file=dump_to_file, + background=background, + upload_resource=upload_resource, + upload_table_name=upload_table_name, + async_job=True) + dummyTapHandler.check_call('launch_job_async', parameters) + + def test_list_async_jobs(self): + dummyTapHandler = DummyTapHandler() + tap = JwstClass(tap_plus_handler=dummyTapHandler) + # default parameters + parameters = {} + parameters['verbose'] = False + tap.list_async_jobs() + dummyTapHandler.check_call('list_async_jobs', parameters) + # test with parameters + dummyTapHandler.reset() + parameters['verbose'] = True + tap.list_async_jobs(verbose=True) + dummyTapHandler.check_call('list_async_jobs', parameters) + + def test_query_region(self): + connHandler = DummyConnHandler() + tapplus = TapPlus("http://test:1111/tap", connhandler=connHandler) + tap = JwstClass(tap_plus_handler=tapplus) + + # Launch response: we use default response because the + # query contains decimals + responseLaunchJob = DummyResponse() + responseLaunchJob.set_status_code(200) + responseLaunchJob.set_message("OK") + jobDataFile = data_path('job_1.vot') + jobData = utils.read_file_content(jobDataFile) + responseLaunchJob.set_data(method='POST', + context=None, + body=jobData, + headers=None) + # The query contains decimals: force default response + connHandler.set_default_response(responseLaunchJob) + sc = SkyCoord(ra=29.0, dec=15.0, unit=(u.degree, u.degree), + frame='icrs') + with pytest.raises(ValueError) as err: + tap.query_region(sc) + assert "Missing required argument: 'width'" in err.value.args[0] + + width = Quantity(12, u.deg) + height = Quantity(10, u.deg) + + with pytest.raises(ValueError) as err: + tap.query_region(sc, width=width) + assert "Missing required argument: 'height'" in err.value.args[0] + + assert (isinstance(tap.query_region(sc, width=width, height=height), Table)) + + # Test observation_id argument + with pytest.raises(ValueError) as err: + tap.query_region(sc, width=width, height=height, observation_id=1) + assert "observation_id must be string" in err.value.args[0] + + assert(isinstance(tap.query_region(sc, width=width, height=height, observation_id="observation"), Table)) + # raise ValueError + + # Test cal_level argument + with pytest.raises(ValueError) as err: + tap.query_region(sc, width=width, height=height, cal_level='a') + assert "cal_level must be either 'Top' or an integer" in err.value.args[0] + + assert (isinstance(tap.query_region(sc, width=width, height=height, cal_level='Top'), Table)) + assert (isinstance(tap.query_region(sc, width=width, height=height, cal_level=1), Table)) + + # Test only_public + with pytest.raises(ValueError) as err: + tap.query_region(sc, width=width, height=height, only_public='a') + assert "only_public must be boolean" in err.value.args[0] + + assert (isinstance(tap.query_region(sc, width=width, height=height, only_public=True), Table)) + + # Test dataproduct_type argument + with pytest.raises(ValueError) as err: + tap.query_region(sc, width=width, height=height, prod_type=1) + assert "prod_type must be string" in err.value.args[0] + + with pytest.raises(ValueError) as err: + tap.query_region(sc, width=width, height=height, prod_type='a') + assert "prod_type must be one of: " in err.value.args[0] + + assert (isinstance(tap.query_region(sc, width=width, height=height, prod_type='image'), Table)) + + # Test instrument_name argument + with pytest.raises(ValueError) as err: + tap.query_region(sc, width=width, height=height, instrument_name=1) + assert "instrument_name must be string" in err.value.args[0] + + assert (isinstance(tap.query_region(sc, width=width, height=height, instrument_name='NIRCAM'), Table)) + + with pytest.raises(ValueError) as err: + tap.query_region(sc, width=width, height=height, + instrument_name='a') + assert "instrument_name must be one of: " in err.value.args[0] + + # Test filter_name argument + with pytest.raises(ValueError) as err: + tap.query_region(sc, width=width, height=height, filter_name=1) + assert "filter_name must be string" in err.value.args[0] + + assert (isinstance(tap.query_region(sc, width=width, height=height, filter_name='filter'), Table)) + + # Test proposal_id argument + with pytest.raises(ValueError) as err: + tap.query_region(sc, width=width, height=height, proposal_id=123) + assert "proposal_id must be string" in err.value.args[0] + + assert (isinstance(tap.query_region(sc, width=width, height=height, proposal_id='123'), Table)) + + table = tap.query_region(sc, width=width, height=height) + assert len(table) == 3, f"Wrong job results (num rows). Expected: {3}, found {len(table)}" + self.__check_results_column(table, + 'alpha', + 'alpha', + None, + np.float64) + self.__check_results_column(table, + 'delta', + 'delta', + None, + np.float64) + self.__check_results_column(table, + 'source_id', + 'source_id', + None, + object) + self.__check_results_column(table, + 'table1_oid', + 'table1_oid', + None, + np.int32) + # by radius + radius = Quantity(1, u.deg) + table = tap.query_region(sc, radius=radius) + assert len(table) == 3, f"Wrong job results (num rows). Expected: {3}, found {len(table)}" + self.__check_results_column(table, + 'alpha', + 'alpha', + None, + np.float64) + self.__check_results_column(table, + 'delta', + 'delta', + None, + np.float64) + self.__check_results_column(table, + 'source_id', + 'source_id', + None, + object) + self.__check_results_column(table, + 'table1_oid', + 'table1_oid', + None, + np.int32) + + def test_query_region_async(self): + connHandler = DummyConnHandler() + tapplus = TapPlus("http://test:1111/tap", connhandler=connHandler) + tap = JwstClass(tap_plus_handler=tapplus) + jobid = '12345' + # Launch response + responseLaunchJob = DummyResponse() + responseLaunchJob.set_status_code(303) + responseLaunchJob.set_message("OK") + # list of list (httplib implementation for headers in response) + launchResponseHeaders = [['location', 'http://test:1111/tap/async/' + jobid]] + responseLaunchJob.set_data(method='POST', + context=None, + body=None, + headers=launchResponseHeaders) + connHandler.set_default_response(responseLaunchJob) + # Phase response + responsePhase = DummyResponse() + responsePhase.set_status_code(200) + responsePhase.set_message("OK") + responsePhase.set_data(method='GET', + context=None, + body="COMPLETED", + headers=None) + req = "async/" + jobid + "/phase" + connHandler.set_response(req, responsePhase) + # Results response + responseResultsJob = DummyResponse() + responseResultsJob.set_status_code(200) + responseResultsJob.set_message("OK") + jobDataFile = data_path('job_1.vot') + jobData = utils.read_file_content(jobDataFile) + responseResultsJob.set_data(method='GET', + context=None, + body=jobData, + headers=None) + req = "async/" + jobid + "/results/result" + connHandler.set_response(req, responseResultsJob) + sc = SkyCoord(ra=29.0, dec=15.0, unit=(u.degree, u.degree), + frame='icrs') + width = Quantity(12, u.deg) + height = Quantity(10, u.deg) + table = tap.query_region(sc, width=width, height=height, async_job=True) + assert len(table) == 3, f"Wrong job results (num rows). Expected: {3}, found {len(table)}" + self.__check_results_column(table, + 'alpha', + 'alpha', + None, + np.float64) + self.__check_results_column(table, + 'delta', + 'delta', + None, + np.float64) + self.__check_results_column(table, + 'source_id', + 'source_id', + None, + object) + self.__check_results_column(table, + 'table1_oid', + 'table1_oid', + None, + np.int32) + # by radius + radius = Quantity(1, u.deg) + table = tap.query_region(sc, radius=radius, async_job=True) + assert len(table) == 3, f"Wrong job results (num rows). Expected: {3}, found {len(table)}" + self.__check_results_column(table, + 'alpha', + 'alpha', + None, + np.float64) + self.__check_results_column(table, + 'delta', + 'delta', + None, + np.float64) + self.__check_results_column(table, + 'source_id', + 'source_id', + None, + object) + self.__check_results_column(table, + 'table1_oid', + 'table1_oid', + None, + np.int32) + + def test_cone_search_sync(self): + connHandler = DummyConnHandler() + tapplus = TapPlus("http://test:1111/tap", connhandler=connHandler) + tap = JwstClass(tap_plus_handler=tapplus) + # Launch response: we use default response because the + # query contains decimals + responseLaunchJob = DummyResponse() + responseLaunchJob.set_status_code(200) + responseLaunchJob.set_message("OK") + jobDataFile = data_path('job_1.vot') + jobData = utils.read_file_content(jobDataFile) + responseLaunchJob.set_data(method='POST', + context=None, + body=jobData, + headers=None) + ra = 19.0 + dec = 20.0 + sc = SkyCoord(ra=ra, dec=dec, unit=(u.degree, u.degree), frame='icrs') + radius = Quantity(1.0, u.deg) + connHandler.set_default_response(responseLaunchJob) + job = tap.cone_search(sc, radius) + assert job is not None, "Expected a valid job" + assert job.async_ is False, "Expected a synchronous job" + assert job.get_phase() == 'COMPLETED', f"Wrong job phase. Expected: {'COMPLETED'}, found {job.get_phase()}" + assert job.failed is False, "Wrong job status (set Failed = True)" + # results + results = job.get_results() + assert len(results) == 3, f"Wrong job results (num rows). Expected: {3}, found {len(results)}" + self.__check_results_column(results, + 'alpha', + 'alpha', + None, + np.float64) + self.__check_results_column(results, + 'delta', + 'delta', + None, + np.float64) + self.__check_results_column(results, + 'source_id', + 'source_id', + None, + object) + self.__check_results_column(results, + 'table1_oid', + 'table1_oid', + None, + np.int32) + + # Test observation_id argument + with pytest.raises(ValueError) as err: + tap.cone_search(sc, radius, observation_id=1) + assert "observation_id must be string" in err.value.args[0] + + # Test cal_level argument + with pytest.raises(ValueError) as err: + tap.cone_search(sc, radius, cal_level='a') + assert "cal_level must be either 'Top' or an integer" in err.value.args[0] + + # Test only_public + with pytest.raises(ValueError) as err: + tap.cone_search(sc, radius, only_public='a') + assert "only_public must be boolean" in err.value.args[0] + + # Test dataproduct_type argument + with pytest.raises(ValueError) as err: + tap.cone_search(sc, radius, prod_type=1) + assert "prod_type must be string" in err.value.args[0] + + with pytest.raises(ValueError) as err: + tap.cone_search(sc, radius, prod_type='a') + assert "prod_type must be one of: " in err.value.args[0] + + # Test instrument_name argument + with pytest.raises(ValueError) as err: + tap.cone_search(sc, radius, instrument_name=1) + assert "instrument_name must be string" in err.value.args[0] + + with pytest.raises(ValueError) as err: + tap.cone_search(sc, radius, instrument_name='a') + assert "instrument_name must be one of: " in err.value.args[0] + + # Test filter_name argument + with pytest.raises(ValueError) as err: + tap.cone_search(sc, radius, filter_name=1) + assert "filter_name must be string" in err.value.args[0] + + # Test proposal_id argument + with pytest.raises(ValueError) as err: + tap.cone_search(sc, radius, proposal_id=123) + assert "proposal_id must be string" in err.value.args[0] + + def test_cone_search_async(self): + connHandler = DummyConnHandler() + tapplus = TapPlus("http://test:1111/tap", connhandler=connHandler) + tap = JwstClass(tap_plus_handler=tapplus) + jobid = '12345' + # Launch response + responseLaunchJob = DummyResponse() + responseLaunchJob.set_status_code(303) + responseLaunchJob.set_message("OK") + # list of list (httplib implementation for headers in response) + launchResponseHeaders = [['location', 'http://test:1111/tap/async/' + jobid]] + responseLaunchJob.set_data(method='POST', + context=None, + body=None, + headers=launchResponseHeaders) + ra = 19 + dec = 20 + sc = SkyCoord(ra=ra, dec=dec, unit=(u.degree, u.degree), frame='icrs') + radius = Quantity(1.0, u.deg) + connHandler.set_default_response(responseLaunchJob) + # Phase response + responsePhase = DummyResponse() + responsePhase.set_status_code(200) + responsePhase.set_message("OK") + responsePhase.set_data(method='GET', + context=None, + body="COMPLETED", + headers=None) + req = "async/" + jobid + "/phase" + connHandler.set_response(req, responsePhase) + # Results response + responseResultsJob = DummyResponse() + responseResultsJob.set_status_code(200) + responseResultsJob.set_message("OK") + jobDataFile = data_path('job_1.vot') + jobData = utils.read_file_content(jobDataFile) + responseResultsJob.set_data(method='GET', + context=None, + body=jobData, + headers=None) + req = "async/" + jobid + "/results/result" + connHandler.set_response(req, responseResultsJob) + job = tap.cone_search(sc, radius, async_job=True) + assert job is not None, "Expected a valid job" + assert job.async_ is True, "Expected an asynchronous job" + assert job.get_phase() == 'COMPLETED', f"Wrong job phase. Expected: {'COMPLETED'}, found {job.get_phase()}" + assert job.failed is False, "Wrong job status (set Failed = True)" + # results + results = job.get_results() + assert len(results) == 3, "Wrong job results (num rows). Expected: {3}, found {len(results)}" + self.__check_results_column(results, + 'alpha', + 'alpha', + None, + np.float64) + self.__check_results_column(results, + 'delta', + 'delta', + None, + np.float64) + self.__check_results_column(results, + 'source_id', + 'source_id', + None, + object) + self.__check_results_column(results, + 'table1_oid', + 'table1_oid', + None, + np.int32) + + def test_get_product_by_artifactid(self): + dummyTapHandler = DummyTapHandler() + jwst = JwstClass(tap_plus_handler=dummyTapHandler, data_handler=dummyTapHandler) + # default parameters + with pytest.raises(ValueError) as err: + jwst.get_product() + assert "Missing required argument: 'artifact_id' or 'file_name'" in err.value.args[0] + + # test with parameters + dummyTapHandler.reset() + + parameters = {} + parameters['output_file'] = 'jw00617023001_02102_00001_nrcb4_uncal.fits' + parameters['verbose'] = False + + param_dict = {} + param_dict['RETRIEVAL_TYPE'] = 'PRODUCT' + param_dict['DATA_RETRIEVAL_ORIGIN'] = 'ASTROQUERY' + param_dict['ARTIFACTID'] = '00000000-0000-0000-8740-65e2827c9895' + parameters['params_dict'] = param_dict + + jwst.get_product(artifact_id='00000000-0000-0000-8740-65e2827c9895') + dummyTapHandler.check_call('load_data', parameters) + + def test_get_product_by_filename(self): + dummyTapHandler = DummyTapHandler() + jwst = JwstClass(tap_plus_handler=dummyTapHandler, data_handler=dummyTapHandler) + # default parameters + with pytest.raises(ValueError) as err: + jwst.get_product() + assert "Missing required argument: 'artifact_id' or 'file_name'" in err.value.args[0] + + # test with parameters + dummyTapHandler.reset() + + parameters = {} + parameters['output_file'] = 'file_name_id' + parameters['verbose'] = False + + param_dict = {} + param_dict['RETRIEVAL_TYPE'] = 'PRODUCT' + param_dict['DATA_RETRIEVAL_ORIGIN'] = 'ASTROQUERY' + param_dict['ARTIFACTID'] = '00000000-0000-0000-8740-65e2827c9895' + parameters['params_dict'] = param_dict + + jwst.get_product(file_name='file_name_id') + dummyTapHandler.check_call('load_data', parameters) + + def test_get_products_list(self): + dummyTapHandler = DummyTapHandler() + jwst = JwstClass(tap_plus_handler=dummyTapHandler, data_handler=dummyTapHandler) + # default parameters + with pytest.raises(ValueError) as err: + jwst.get_product_list() + assert "Missing required argument: 'observation_id'" in err.value.args[0] + + # test with parameters + dummyTapHandler.reset() + + observation_id = "jw00777011001_02104_00001_nrcblong" + cal_level_condition = " AND m.calibrationlevel = m.max_cal_level" + prodtype_condition = "" + + query = (f"select distinct a.uri, a.artifactid, a.filename, " + f"a.contenttype, a.producttype, p.calibrationlevel, p.public " + f"FROM {conf.JWST_PLANE_TABLE} p JOIN {conf.JWST_ARTIFACT_TABLE} " + f"a ON (p.planeid=a.planeid) WHERE a.planeid " + f"IN {planeids};") + + parameters = {} + parameters['query'] = query + parameters['name'] = None + parameters['output_file'] = None + parameters['output_format'] = 'votable' + parameters['verbose'] = False + parameters['dump_to_file'] = False + parameters['upload_resource'] = None + parameters['upload_table_name'] = None + + jwst.get_product_list(observation_id=observation_id) + dummyTapHandler.check_call('launch_job', parameters) + + def test_get_obs_products(self): + dummyTapHandler = DummyTapHandler() + jwst = JwstClass(tap_plus_handler=dummyTapHandler, data_handler=dummyTapHandler) + # default parameters + with pytest.raises(ValueError) as err: + jwst.get_obs_products() + assert "Missing required argument: 'observation_id'" in err.value.args[0] + + # test with parameters + dummyTapHandler.reset() + + output_file_full_path_dir = os.getcwd() + os.sep + "temp_test_jwsttap_get_obs_products_1" + try: + os.makedirs(output_file_full_path_dir, exist_ok=True) + except OSError as err: + print(f"Creation of the directory {output_file_full_path_dir} failed: {err.strerror}") + raise err + + observation_id = 'jw00777011001_02104_00001_nrcblong' + + parameters = {} + parameters['verbose'] = False + + param_dict = {} + param_dict['RETRIEVAL_TYPE'] = 'OBSERVATION' + param_dict['DATA_RETRIEVAL_ORIGIN'] = 'ASTROQUERY' + param_dict['planeid'] = planeids + param_dict['calibrationlevel'] = 'ALL' + parameters['params_dict'] = param_dict + + # Test single product tar + file = data_path('single_product_retrieval.tar') + output_file_full_path = output_file_full_path_dir + os.sep + os.path.basename(file) + shutil.copy(file, output_file_full_path) + parameters['output_file'] = output_file_full_path + + expected_files = [] + extracted_file_1 = output_file_full_path_dir + os.sep + 'single_product_retrieval_1.fits' + expected_files.append(extracted_file_1) + try: + files_returned = (jwst.get_obs_products( + observation_id=observation_id, cal_level='ALL', + output_file=output_file_full_path)) + dummyTapHandler.check_call('load_data', parameters) + self.__check_extracted_files(files_expected=expected_files, + files_returned=files_returned) + finally: + shutil.rmtree(output_file_full_path_dir) + + # Test single file + output_file_full_path_dir = os.getcwd() + os.sep +\ + "temp_test_jwsttap_get_obs_products_2" + try: + os.makedirs(output_file_full_path_dir, exist_ok=True) + except OSError as err: + print(f"Creation of the directory {output_file_full_path_dir} failed: {err.strerror}") + raise err + + file = data_path('single_product_retrieval_1.fits') + output_file_full_path = output_file_full_path_dir + os.sep +\ + os.path.basename(file) + shutil.copy(file, output_file_full_path) + + parameters['output_file'] = output_file_full_path + + expected_files = [] + expected_files.append(output_file_full_path) + + try: + files_returned = (jwst.get_obs_products( + observation_id=observation_id, + output_file=output_file_full_path)) + dummyTapHandler.check_call('load_data', parameters) + self.__check_extracted_files(files_expected=expected_files, + files_returned=files_returned) + finally: + # self.__remove_folder_contents(folder=output_file_full_path_dir) + shutil.rmtree(output_file_full_path_dir) + + # Test single file zip + output_file_full_path_dir = os.getcwd() + os.sep + "temp_test_jwsttap_get_obs_products_3" + try: + os.makedirs(output_file_full_path_dir, exist_ok=True) + except OSError as err: + print(f"Creation of the directory {output_file_full_path_dir} failed: {err.strerror}") + raise err + + file = data_path('single_product_retrieval_3.fits.zip') + output_file_full_path = output_file_full_path_dir + os.sep +\ + os.path.basename(file) + shutil.copy(file, output_file_full_path) + + parameters['output_file'] = output_file_full_path + + expected_files = [] + extracted_file_1 = output_file_full_path_dir + os.sep + 'single_product_retrieval.fits' + expected_files.append(extracted_file_1) + + try: + files_returned = (jwst.get_obs_products( + observation_id=observation_id, + output_file=output_file_full_path)) + dummyTapHandler.check_call('load_data', parameters) + self.__check_extracted_files(files_expected=expected_files, + files_returned=files_returned) + finally: + # self.__remove_folder_contents(folder=output_file_full_path_dir) + shutil.rmtree(output_file_full_path_dir) + + # Test single file gzip + output_file_full_path_dir = (os.getcwd() + os.sep + "temp_test_jwsttap_get_obs_products_4") + try: + os.makedirs(output_file_full_path_dir, exist_ok=True) + except OSError as err: + print(f"Creation of the directory {output_file_full_path_dir} failed: {err.strerror}") + raise err + + file = data_path('single_product_retrieval_2.fits.gz') + output_file_full_path = output_file_full_path_dir + os.sep + os.path.basename(file) + shutil.copy(file, output_file_full_path) + + parameters['output_file'] = output_file_full_path + + expected_files = [] + extracted_file_1 = output_file_full_path_dir + os.sep + 'single_product_retrieval_2.fits.gz' + expected_files.append(extracted_file_1) + + try: + files_returned = (jwst.get_obs_products( + observation_id=observation_id, + output_file=output_file_full_path)) + dummyTapHandler.check_call('load_data', parameters) + self.__check_extracted_files(files_expected=expected_files, + files_returned=files_returned) + finally: + # self.__remove_folder_contents(folder=output_file_full_path_dir) + shutil.rmtree(output_file_full_path_dir) + + # Test tar with 3 files, a normal one, a gzip one and a zip one + output_file_full_path_dir = (os.getcwd() + os.sep + "temp_test_jwsttap_get_obs_products_5") + try: + os.makedirs(output_file_full_path_dir, exist_ok=True) + except OSError as err: + print(f"Creation of the directory {output_file_full_path_dir} failed: {err.strerror}") + raise err + + file = data_path('three_products_retrieval.tar') + output_file_full_path = output_file_full_path_dir + os.sep + os.path.basename(file) + shutil.copy(file, output_file_full_path) + + parameters['output_file'] = output_file_full_path + + expected_files = [] + extracted_file_1 = output_file_full_path_dir + os.sep + 'single_product_retrieval_1.fits' + expected_files.append(extracted_file_1) + extracted_file_2 = output_file_full_path_dir + os.sep + 'single_product_retrieval_2.fits.gz' + expected_files.append(extracted_file_2) + extracted_file_3 = output_file_full_path_dir + os.sep + 'single_product_retrieval_3.fits.zip' + expected_files.append(extracted_file_3) + + try: + files_returned = (jwst.get_obs_products( + observation_id=observation_id, + output_file=output_file_full_path)) + dummyTapHandler.check_call('load_data', parameters) + self.__check_extracted_files(files_expected=expected_files, + files_returned=files_returned) + finally: + # self.__remove_folder_contents(folder=output_file_full_path_dir) + shutil.rmtree(output_file_full_path_dir) + + def test_gunzip_file(self): + output_file_full_path_dir = (os.getcwd() + os.sep + "temp_test_jwsttap_gunzip") + try: + os.makedirs(output_file_full_path_dir, exist_ok=True) + except OSError as err: + print(f"Creation of the directory {output_file_full_path_dir} failed: {err.strerror}") + raise err + + file = data_path('single_product_retrieval_2.fits.gz') + output_file_full_path = output_file_full_path_dir + os.sep + os.path.basename(file) + shutil.copy(file, output_file_full_path) + + expected_files = [] + extracted_file_1 = output_file_full_path_dir + os.sep + "single_product_retrieval_2.fits" + expected_files.append(extracted_file_1) + + try: + extracted_file = (JwstClass.gzip_uncompress_and_rename_single_file( + output_file_full_path)) + if extracted_file != extracted_file_1: + raise ValueError(f"Extracted file not found: {extracted_file_1}") + finally: + # self.__remove_folder_contents(folder=output_file_full_path_dir) + shutil.rmtree(output_file_full_path_dir) + + def __check_results_column(self, results, columnName, description, unit, + dataType): + c = results[columnName] + assert c.description == description, \ + f"Wrong description for results column '{columnName}'. Expected: '{description}', "\ + f"found '{c.description}'" + assert c.unit == unit, \ + f"Wrong unit for results column '{columnName}'. Expected: '{unit}', found '{c.unit}'" + assert c.dtype == dataType, \ + f"Wrong dataType for results column '{columnName}'. Expected: '{dataType}', found '{c.dtype}'" + + def __remove_folder_contents(self, folder): + for root, dirs, files in os.walk(folder): + for f in files: + os.unlink(os.path.join(root, f)) + for d in dirs: + shutil.rmtree(os.path.join(root, d)) + + def __check_extracted_files(self, files_expected, files_returned): + if len(files_expected) != len(files_returned): + raise ValueError(f"Expected files size error. " + f"Found {len(files_returned)}, " + f"expected {len(files_expected)}") + for f in files_expected: + if not os.path.exists(f): + raise ValueError(f"Not found extracted file: " + f"{f}") + if f not in files_returned: + raise ValueError(f"Not found expected file: {f}") + + def test_query_target_error(self): + jwst = JwstClass() + simbad = Simbad() + ned = Ned() + vizier = Vizier() + # Testing default parameters + with pytest.raises(ValueError) as err: + jwst.query_target(target_name="M1", target_resolver="") + assert "This target resolver is not allowed" in err.value.args[0] + with pytest.raises(ValueError) as err: + jwst.query_target("TEST") + assert "This target name cannot be determined with this resolver: ALL" in err.value.args[0] + with pytest.raises(ValueError) as err: + jwst.query_target(target_name="M1", target_resolver="ALL") + assert err.value.args[0] in [f"This target name cannot be determined " + f"with this resolver: ALL", "Missing " + f"required argument: 'width'"] + + # Testing no valid coordinates from resolvers + simbad_file = data_path('test_query_by_target_name_simbad_ned_error.vot') + simbad_table = Table.read(simbad_file) + simbad.query_object = MagicMock(return_value=simbad_table) + ned_file = data_path('test_query_by_target_name_simbad_ned_error.vot') + ned_table = Table.read(ned_file) + ned.query_object = MagicMock(return_value=ned_table) + vizier_file = data_path('test_query_by_target_name_vizier_error.vot') + vizier_table = Table.read(vizier_file) + vizier.query_object = MagicMock(return_value=vizier_table) + + # coordinate_error = 'coordinate must be either a string or astropy.coordinates' + with pytest.raises(ValueError) as err: + jwst.query_target(target_name="M1", target_resolver="SIMBAD", + radius=units.Quantity(5, units.deg)) + assert 'This target name cannot be determined with this resolver: SIMBAD' in err.value.args[0] + + with pytest.raises(ValueError) as err: + jwst.query_target(target_name="M1", target_resolver="NED", + radius=units.Quantity(5, units.deg)) + assert 'This target name cannot be determined with this resolver: NED' in err.value.args[0] + + with pytest.raises(ValueError) as err: + jwst.query_target(target_name="M1", target_resolver="VIZIER", + radius=units.Quantity(5, units.deg)) + assert 'This target name cannot be determined with this resolver: VIZIER' in err.value.args[0] diff --git a/docs/esa/jwst.rst b/docs/esa/jwst.rst new file mode 100644 index 0000000000..d9eb36b2c1 --- /dev/null +++ b/docs/esa/jwst.rst @@ -0,0 +1,763 @@ +.. doctest-skip-all + +.. _astroquery.esa.jwst: + +********************************* +JWST TAP+ (`astroquery.esa.jwst`) +********************************* + +**THIS MODULE IS NOT OPERATIVE YET. METHODS WILL NOT WORK UNTIL eJWST ARCHIVE IS OFFICIALLY RELEASED** + +The James Webb Space Telescope (JWST) is a collaborative project between NASA, +ESA, and the Canadian Space Agency (CSA). Although radically different in +design, and emphasizing the infrared part of the electromagnetic spectrum, +JWST is widely seen as the successor to the Hubble Space Telescope (HST). +The JWST observatory consist of a deployable 6.6 meter passively cooled +telescope optimized for infrared wavelengths, and is operated in deep +space at the anti-Sun Earth-Sun Lagrangian point (L2). It carries four +scientific instruments: a near-infrared camera (NIRCam), a +near-infrared multi-object spectrograph (NIRSpec) covering the 0.6 - 5 μm +spectral region, a near-infrared slit-less spectrograph (NIRISS), and a +combined mid-infrared camera/spectrograph (MIRI) covering 5 - 28 μm. The JWST +focal plane (see image to the right) contains apertures for the science +instruments and the Fine Guidance Sensor (FGS). + +The scientific goals of the JWST mission can be sorted into four broad themes: +The birth of stars and proto-planetary systems Planetary systems and the +origins of life + +* The end of the dark ages: first light and re-ionization. +* The assembly of galaxies. +* The birth of stars and proto-planetary systems. +* Planetary systems and the origins of life. + +This package allows the access to the European Space Agency JWST Archive +(http://jwstdummyarchive.com/) + +ESA JWST Archive access is based on a TAP+ REST service. TAP+ is an extension of +Table Access Protocol (TAP: http://www.ivoa.net/documents/TAP/) specified by the +International Virtual Observatory Alliance (IVOA: http://www.ivoa.net). + +The TAP query language is Astronomical Data Query Language +(ADQL: http://www.ivoa.net/documents/ADQL/2.0), which is similar +to Structured Query Language (SQL), widely used to query databases. + +TAP provides two operation modes: Synchronous and Asynchronous: + +* Synchronous: the response to the request will be generated as soon as the + request received by the server. + (Do not use this method for queries that generate a big amount of results.) +* Asynchronous: the server starts a job that will execute the request. + The first response to the request is the required information (a link) + to obtain the job status. + Once the job is finished, the results can be retrieved. + +This module can use these two modes, usinc the 'async_job=False/True' tag in different functions. + +ESA JWST TAP+ server provides two access mode: public and authenticated: + +* Public: this is the standard TAP access. A user can execute ADQL queries and + upload tables to be used in a query 'on-the-fly' (these tables will be removed + once the query is executed). The results are available to any other user and + they will remain in the server for a limited space of time. + +* Authenticated: some functionalities are restricted to authenticated users only. + The results are saved in a private user space and they will remain in the + server for ever (they can be removed by the user). + + * ADQL queries and results are saved in a user private area. + + * Cross-match operations: a catalog cross-match operation can be executed. + Cross-match operations results are saved in a user private area. + + * Persistence of uploaded tables: a user can upload a table in a private space. + These tables can be used in queries as well as in cross-matches operations. + + +This python module provides an Astroquery API access. + + +======== +Examples +======== + +It is highly recommended checking the status of JWST TAP before executing this module. To do this: + +.. code-block:: python + + >>> from astroquery.esa.jwst import Jwst + >>> Jwst.get_status_messages() + +This method will retrieve the same warning messages shown in JWST Science Archive with information about +service degradation. + +--------------------------- +1. Non authenticated access +--------------------------- + +1.1. Query region +~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + >>> import astropy.units as u + >>> from astropy.coordinates import SkyCoord + >>> from astroquery.esa.jwst import Jwst + >>> + >>> coord=SkyCoord(ra=53, dec=-27, unit=(u.degree, u.degree), frame='icrs') + >>> width=u.Quantity(5, u.deg) + >>> height=u.Quantity(5, u.deg) + >>> r=Jwst.query_region(coordinate=coord, width=width, height=height) + >>> r + + Query finished. + dist obsid ... type typecode + ------------------ ------------------------------------ ... ----- -------- + 0.8042331552744052 00000000-0000-0000-8f43-c68be243b878 ... PRIME S + 0.8042331552744052 00000000-0000-0000-8f43-c68be243b878 ... PRIME S + 0.8042331552744052 00000000-0000-0000-94fc-23f102d345d3 ... PRIME S + 0.8042331552744052 00000000-0000-0000-94fc-23f102d345d3 ... PRIME S + 0.8042331552744052 00000000-0000-0000-a288-14744c2a684b ... PRIME S + 0.8042331552744052 00000000-0000-0000-a288-14744c2a684b ... PRIME S + 0.8042331552744052 00000000-0000-0000-b3cc-6aa1e2e509c2 ... PRIME S + 0.8042331552744052 00000000-0000-0000-b3cc-6aa1e2e509c2 ... PRIME S + 0.8042331552744052 00000000-0000-0000-b3eb-870a80410d40 ... PRIME S + 0.8042331552744052 00000000-0000-0000-b3eb-870a80410d40 ... PRIME S + 0.8042331552744052 00000000-0000-0000-babe-5c1ec63d3301 ... PRIME S + 0.8042331552744052 00000000-0000-0000-babe-5c1ec63d3301 ... PRIME S + + +1.2. Cone search +~~~~~~~~~~~~~~~~ + +.. code-block:: python + + >>> import astropy.units as u + >>> from astropy.coordinates import SkyCoord + >>> from astroquery.esa.jwst import Jwst + >>> + >>> coord=SkyCoord(ra=53, dec=-27, unit=(u.degree, u.degree), frame='icrs') + >>> radius=u.Quantity(5.0, u.deg) + >>> j=Jwst.cone_search(coordinate=coord, radius=radius, async_job=True) + >>> r=j.get_results() + >>> r + + dist obsid ... type typecode + ------------------ ------------------------------------ ... ----- -------- + 0.8042331552744052 00000000-0000-0000-8f43-c68be243b878 ... PRIME S + 0.8042331552744052 00000000-0000-0000-8f43-c68be243b878 ... PRIME S + 0.8042331552744052 00000000-0000-0000-94fc-23f102d345d3 ... PRIME S + 0.8042331552744052 00000000-0000-0000-94fc-23f102d345d3 ... PRIME S + 0.8042331552744052 00000000-0000-0000-a288-14744c2a684b ... PRIME S + 0.8042331552744052 00000000-0000-0000-a288-14744c2a684b ... PRIME S + 0.8042331552744052 00000000-0000-0000-b3cc-6aa1e2e509c2 ... PRIME S + 0.8042331552744052 00000000-0000-0000-b3cc-6aa1e2e509c2 ... PRIME S + 0.8042331552744052 00000000-0000-0000-b3eb-870a80410d40 ... PRIME S + 0.8042331552744052 00000000-0000-0000-b3eb-870a80410d40 ... PRIME S + 0.8042331552744052 00000000-0000-0000-babe-5c1ec63d3301 ... PRIME S + 0.8042331552744052 00000000-0000-0000-babe-5c1ec63d3301 ... PRIME S + +1.3. Query by target name +~~~~~~~~~~~~~~~~~~~~~~~~~ + +To provide the target coordinates based on its name and execute the query region method. +It uses three different catalogs to resolve the coordinates: SIMBAD, NED and VIZIER. An additional target +resolver is provider, ALL (which is also the default value), using all the aforementioned +catalogues in the defined order to obtain the required coordinates (using the following +element in the list if the target name cannot be resolved). + +.. code-block:: python + + >>> from astroquery.esa.jwst import Jwst + >>> import astropy.units as u + >>> + >>> target_name='M1' + >>> target_resolver='ALL' + >>> radius=u.Quantity(5, u.deg) + >>> r=Jwst.query_target(target_name=target_name, target_resolver=target_resolver, radius=radius) + >>> r + + dist observationid ... + ------------------ -------------------------------- ... + 3.4465676399769096 jw01179006001_xx100_00000_nircam ... + 3.4465676399769096 jw01179005001_xx100_00000_nircam ... + 3.4465676399769096 jw01179005001_xx103_00003_nircam ... + 3.4465676399769096 jw01179006001_xx101_00001_nircam ... + 3.4465676399769096 jw01179005001_xx102_00002_nircam ... + 3.4465676399769096 jw01179006001_xx105_00002_nircam ... + 3.4465676399769096 jw01179005001_xx106_00003_nircam ... + 3.4465676399769096 jw01179006001_xx102_00002_nircam ... + 3.4465676399769096 jw01179006001_xx103_00003_nircam ... + 3.4465676399769096 jw01179005001_xx101_00001_nircam ... + 3.4465676399769096 jw01179005001_xx104_00001_nircam ... + 3.4465676399769096 jw01179006001_xx104_00001_nircam ... + 3.4465676399769096 jw01179006001_xx106_00003_nircam ... + 3.4465676399769096 jw01179005001_xx105_00002_nircam ... + +This method uses the same parameters as query region, but also includes the target name and the catalogue +(target resolver) to retrieve the coordinates. + +.. code-block:: python + + >>> from astroquery.esa.jwst import Jwst + >>> import astropy.units as u + >>> + >>> target_name='LMC' + >>> target_resolver='NED' + >>> width=u.Quantity(5, u.deg) + >>> height=u.Quantity(5, u.deg) + >>> r=Jwst.query_target(target_name=target_name, target_resolver=target_resolver, width=width, height=height, async_job=True) + >>> r + + dist observationid ... + ---------------------- -------------------------------------- ... + 0.00010777991644807922 jw00322001003_02101_00001_nrca1 ... + 0.00010777991644807922 jw00322001003_02101_00001_nrcb2 ... + 0.00010777991644807922 jw96854009004_xxxxx_00003-00003_nircam ... + 0.00010777991644807922 jw00322001003_02101_00001_nrcblong ... + 0.00010777991644807922 jw00827011001_02101_00001_mirimage ... + 0.00010777991644807922 jw01039004001_xx101_00001_miri ... + 0.00010777991644807922 jw00322001003_02101_00001_nrcb1 ... + 0.00010777991644807922 jw00322001002_02101_00001_nrcb2 ... + 0.00010777991644807922 jw96854009001_xx102_00002_nircam ... + ... ... ... + +1.4 Getting data products +~~~~~~~~~~~~~~~~~~~~~~~~~ +To query the data products associated with a certain Observation ID + +.. code-block:: python + + >>> from astroquery.esa.jwst import Jwst + >>> product_list=Jwst.get_product_list(observation_id='jw00777011001_02104_00001_nrcblong') + >>> for row in product_list: + >>> print("filename: %s" % (row['filename'])) + + filename: jw00777011001_02104_00001_nrcblong_c1005_crf.fits + filename: jw00777011001_02104_00001_nrcblong_cal.fits + filename: jw00777011001_02104_00001_nrcblong_cal.jpg + filename: jw00777011001_02104_00001_nrcblong_cal_thumb.jpg + filename: jw00777011001_02104_00001_nrcblong_i2d.fits + filename: jw00777011001_02104_00001_nrcblong_o011_crf.fits + +You can filter by product type and calibration level (using a numerical value or the option 'ALL' -set by default- that will download +all the products associated to this observation_id with the same and lower levels). + +.. code-block:: python + + >>> from astroquery.esa.jwst import Jwst + >>> product_list=Jwst.get_product_list(observation_id='jw97012001001_02101_00001_guider1', product_type='science') + >>> for row in product_list: + >>> print("filename: %s" % (row['filename'])) + + filename: jw97012001001_02101_00001_guider1_cal.fits + filename: jw97012001001_02101_00001_guider1_uncal.fits + +To download a data product + +.. code-block:: python + + >>> from astroquery.esa.jwst import Jwst + >>> query="select a.artifactid, a.uri from jwst.artifact a, jwst.plane p where p.planeid=a.planeid and p.obsid='00000000-0000-0000-9c08-f5be8f3df805'" + >>> job=Jwst.launch_job(query, async_job=True) + >>> results=job.get_results() + >>> results + artifactid filename + ------------------------------------ ------------------------------------------------ + 00000000-0000-0000-a4f7-23ab64230444 jw00601004001_02102_00001_nrcb1_rate.fits + 00000000-0000-0000-b796-76a61aade312 jw00601004001_02102_00001_nrcb1_rateints.fits + 00000000-0000-0000-ad5e-7d388b43ca4b jw00601004001_02102_00001_nrcb1_trapsfilled.fits + 00000000-0000-0000-9335-09ff0e02f06b jw00601004001_02102_00001_nrcb1_uncal.fits + 00000000-0000-0000-864d-b03ced521884 jw00601004001_02102_00001_nrcb1_uncal.jpg + 00000000-0000-0000-9392-45ebdada66be jw00601004001_02102_00001_nrcb1_uncal_thumb.jpg + + + >>> output_file=Jwst.get_product(artifact_id='00000000-0000-0000-9335-09ff0e02f06b') + >>> output_file=Jwst.get_product(file_name='jw00601004001_02102_00001_nrcb1_uncal.fits') + +To download products by observation identifier, it is possible to use the get_obs_products function, with the same parameters +than get_product_list. + +.. code-block:: python + + >>> observation_id='jw00777011001_02104_00001_nrcblong' + >>> results=Jwst.get_obs_products(observation_id=observation_id, cal_level=2, product_type='science') + + INFO: {'RETRIEVAL_TYPE': 'OBSERVATION', 'DATA_RETRIEVAL_ORIGIN': 'ASTROQUERY', 'planeid': '00000000-0000-0000-879d-ae91fa2f43e2', 'calibrationlevel': 'SELECTED', 'product_type': 'science'} [astroquery.esa.jwst.core] + Retrieving data. + Done. + Product(s) saved at: ///\temp_20200706_131015\jw00777011001_02104_00001_nrcblong_all_products + Product = ///\temp_20200706_131015\jw00777\level_1\jw00777011001_02104_00001_nrcblong_uncal.fits + Product = ///\temp_20200706_131015\jw00777\level_2\jw00777011001_02104_00001_nrcblong_cal.fits + Product =///\temp_20200706_131015\jw00777\level_2\jw00777011001_02104_00001_nrcblong_i2d.fits + + +A temporary directory is created with the files and a list of the them is provided. + +When more than one product is found, a tar file is retrieved. This method extracts the products. + +This method is only intended to download the products with the same calibration level or below. If an upper level is requested: + +.. code-block:: python + + ValueError: Requesting upper levels is not allowed + +If proprietary data is requested and the user has not logged in: + +.. code-block:: python + + 403 Error 403: + Private file(s) requested: MAST token required for authentication. + +It is also possible to extract the products associated to an observation with upper calibration levels with get_related_observations. +Using the observation ID as input parameter, this function will retrieve the observations (IDs) that use it to create a composite observation. + +.. code-block:: python + + >>> observation_id='jw00777011001_02104_00001_nrcblong' + >>> results=Jwst.get_related_observations(observation_id=observation_id) + + [' jw00777-o011_t005_nircam_f277w-sub160', 'jw00777-c1005_t005_nircam_f277w-sub160'] + + +1.5 Getting public tables +~~~~~~~~~~~~~~~~~~~~~~~~~ + +To load only table names (TAP+ capability) + +.. code-block:: python + + >>> from astroquery.esa.jwst import Jwst + >>> tables=Jwst.load_tables(only_names=True) + >>> for table in (tables): + >>> print(table.name) + + public.dual + tap_schema.columns + tap_schema.key_columns + tap_schema.keys + tap_schema.schemas + tap_schema.tables + jwst.artifact + jwst.chunk + jwst.main + jwst.observation + jwst.observationmember + jwst.part + jwst.plane + jwst.plane_inputs + +To load table names (TAP compatible) + +.. code-block:: python + + >>> from astroquery.esa.jwst import Jwst + >>> tables=Jwst.load_tables() + >>> for table in (tables): + >>> print(table.name) + + public.dual + tap_schema.columns + tap_schema.key_columns + tap_schema.keys + tap_schema.schemas + tap_schema.tables + jwst.artifact + jwst.chunk + jwst.main + jwst.observation + jwst.observationmember + jwst.part + jwst.plane + jwst.plane_inputs + +To load only a table (TAP+ capability) + +.. code-block:: python + + >>> from astroquery.esa.jwst import Jwst + >>> table=Jwst.load_table('jwst.main') + >>> print(table) + + TAP Table name: jwst.main + Description: + Num. columns: 112 + + +Once a table is loaded, columns can be inspected + +.. code-block:: python + + >>> from astroquery.esa.jwst import Jwst + >>> table=Jwst.load_table('jwst.main') + >>> for column in (table.columns): + >>> print(column.name) + + obsid + planeid + public + calibrationlevel + dataproducttype + algorithm_name + collection + creatorid + energy_bandpassname + ... + time_exposure + time_resolution + time_samplesize + type + typecode + +1.6 Synchronous query +~~~~~~~~~~~~~~~~~~~~~ + +A synchronous query will not store the results at server side. These queries +must be used when the amount of data to be retrieve is 'small'. + +There is a limit of 2000 rows. If you need more than that, you must use +asynchronous queries. + +The results can be saved in memory (default) or in a file. + +Query without saving results in a file: + +.. code-block:: python + + >>> from astroquery.esa.jwst import Jwst + >>> + >>> job=Jwst.launch_job("SELECT TOP 100 \ + >>> instrument_name, observationuri, planeid, calibrationlevel, \ + >>> dataproducttype \ + >>> FROM jwst.main ORDER BY instrument_name, observationuri") + >>> + >>> print(job) + + Jobid: None + Phase: COMPLETED + Owner: None + Output file: sync_20170223111452.xml.gz + Results: None + + >>> r=job.get_results() + >>> r['planeid'] + + planeid + ------------------------------------ + 00000000-0000-0000-9d6d-f192fde74ce4 + 00000000-0000-0000-8a85-d34d6a411611 + 00000000-0000-0000-969c-a49226673efa + 00000000-0000-0000-8c07-c26c24bec2ee + 00000000-0000-0000-89d2-b42624493c84 + 00000000-0000-0000-800d-659917e7bb26 + 00000000-0000-0000-8cb6-748fa37d47e3 + 00000000-0000-0000-8573-92ad575b8fb4 + 00000000-0000-0000-8572-b7b226953a2c + 00000000-0000-0000-8d1d-765c362e3227 + ... + 00000000-0000-0000-b7d9-b4686ed37bf0 + 00000000-0000-0000-822f-08376ffe6f0b + 00000000-0000-0000-8a8e-8cd48bb4cd7a + 00000000-0000-0000-8a9d-3e1aae1281ba + 00000000-0000-0000-a2ac-1ac288320bf7 + 00000000-0000-0000-a20f-835a58ca7872 + 00000000-0000-0000-aa9c-541cc6e5ff87 + 00000000-0000-0000-8fe4-092c69639602 + 00000000-0000-0000-acfb-6e445e284609 + 00000000-0000-0000-96ff-efd5bbcd5afe + 00000000-0000-0000-8d90-2ca5ebac4a51 + Length = 37 rows + +Query saving results in a file: + +.. code-block:: python + + >>> from astroquery.esa.jwst import JWST + >>> job=Jwst.launch_job("SELECT TOP 100 \ + >>> instrument_name, observationuri, planeid, calibrationlevel, \ + >>> dataproducttype, target_ra, target_dec \ + >>> FROM jwst.main ORDER BY instrument_name, observationuri", \ + >>> dump_to_file=True) + >>> + >>> print(job) + + Jobid: None + Phase: COMPLETED + Owner: None + Output file: sync_20181116164108.xml.gz + Results: None + + >>> r=job.get_results() + >>> print(r['solution_id']) + + >>> r=job.get_results() + >>> print(r['planeid']) + + planeid + ------------------------------------ + 00000000-0000-0000-9d6d-f192fde74ce4 + 00000000-0000-0000-8a85-d34d6a411611 + 00000000-0000-0000-969c-a49226673efa + 00000000-0000-0000-8c07-c26c24bec2ee + 00000000-0000-0000-89d2-b42624493c84 + 00000000-0000-0000-800d-659917e7bb26 + 00000000-0000-0000-8cb6-748fa37d47e3 + 00000000-0000-0000-8573-92ad575b8fb4 + 00000000-0000-0000-8572-b7b226953a2c + 00000000-0000-0000-8d1d-765c362e3227 + ... + 00000000-0000-0000-b7d9-b4686ed37bf0 + 00000000-0000-0000-822f-08376ffe6f0b + 00000000-0000-0000-8a8e-8cd48bb4cd7a + 00000000-0000-0000-8a9d-3e1aae1281ba + 00000000-0000-0000-a2ac-1ac288320bf7 + 00000000-0000-0000-a20f-835a58ca7872 + 00000000-0000-0000-aa9c-541cc6e5ff87 + 00000000-0000-0000-8fe4-092c69639602 + 00000000-0000-0000-acfb-6e445e284609 + 00000000-0000-0000-96ff-efd5bbcd5afe + 00000000-0000-0000-8d90-2ca5ebac4a51 + Length = 37 rows + + +1.7 Synchronous query on an 'on-the-fly' uploaded table +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A table can be uploaded to the server in order to be used in a query. + +.. code-block:: python + + >>> from astroquery.esa.jwst import Jwst + >>> upload_resource='mytable.xml.gz' + >>> j=Jwst.launch_job(query="SELECT * from tap_upload.table_test", \ + >>> upload_resource=upload_resource, \ + >>> upload_table_name="table_test", verbose=True) + >>> r=j.get_results() + >>> r.pprint() + + source_id alpha delta + --------- ----- ----- + a 1.0 2.0 + b 3.0 4.0 + c 5.0 6.0 + + +1.8 Asynchronous query +~~~~~~~~~~~~~~~~~~~~~~ + +Asynchronous queries save results at server side. These queries can be accessed at any time. For anonymous users, results are kept for three days. + +The results can be saved in memory (default) or in a file. + +Query without saving results in a file: + +.. code-block:: python + + >>> from astroquery.esa.jwst import Jwst + >>> job=Jwst.launch_job("select top 100 * from jwst.main", async_job=True) + >>> print(job) + + Jobid: 1542383562372I + Phase: COMPLETED + Owner: None + Output file: async_20181116165244.vot + Results: None + + >>> r=job.get_results() + >>> r['planeid'] + + solution_id + ------------------- + 1635378410781933568 + 1635378410781933568 + 1635378410781933568 + 1635378410781933568 + 1635378410781933568 + 1635378410781933568 + 1635378410781933568 + 1635378410781933568 + 1635378410781933568 + 1635378410781933568 + ... + 1635378410781933568 + 1635378410781933568 + 1635378410781933568 + 1635378410781933568 + 1635378410781933568 + 1635378410781933568 + 1635378410781933568 + 1635378410781933568 + 1635378410781933568 + 1635378410781933568 + 1635378410781933568 + Length = 100 rows + +Query saving results in a file: + +.. code-block:: python + + >>> from astroquery.esa.jwst import Jwst + >>> + >>> job=Jwst.launch_job("select top 100 * from jwst.main", dump_to_file=True) + >>> + >>> print(job) + + Jobid: None + Phase: COMPLETED + Owner: None + Output file: 1635853688471D-result.vot.gz + Results: None + + + >>> r=job.get_results() + >>> r['solution_id'] + + planeid + ------------------------------------ + 00000000-0000-0000-9d6d-f192fde74ce4 + 00000000-0000-0000-8a85-d34d6a411611 + 00000000-0000-0000-969c-a49226673efa + 00000000-0000-0000-8c07-c26c24bec2ee + 00000000-0000-0000-89d2-b42624493c84 + 00000000-0000-0000-800d-659917e7bb26 + 00000000-0000-0000-8cb6-748fa37d47e3 + 00000000-0000-0000-8573-92ad575b8fb4 + 00000000-0000-0000-8572-b7b226953a2c + 00000000-0000-0000-8d1d-765c362e3227 + ... + 00000000-0000-0000-b7d9-b4686ed37bf0 + 00000000-0000-0000-822f-08376ffe6f0b + 00000000-0000-0000-8a8e-8cd48bb4cd7a + 00000000-0000-0000-8a9d-3e1aae1281ba + 00000000-0000-0000-a2ac-1ac288320bf7 + 00000000-0000-0000-a20f-835a58ca7872 + 00000000-0000-0000-aa9c-541cc6e5ff87 + 00000000-0000-0000-8fe4-092c69639602 + 00000000-0000-0000-acfb-6e445e284609 + 00000000-0000-0000-96ff-efd5bbcd5afe + 00000000-0000-0000-8d90-2ca5ebac4a51 + Length = 37 rows + + +1.9 Asynchronous job removal +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To remove asynchronous + +.. code-block:: python + + >>> from astroquery.esa.jwst import Jwst + >>> job=Jwst.remove_jobs(["job_id_1","job_id_2",...]) + + +----------------------- +2. Authenticated access +----------------------- + +Authenticated users are able to access to TAP+ capabilities (shared tables, persistent jobs, etc.) +In order to authenticate a user, ``login``, ``login_gui`` or ``login_token_gui`` methods must be called. After a successful +authentication, the user will be authenticated until ``logout`` method is called. + +All previous methods (``query_object``, ``cone_search``, ``load_table``, ``load_tables``, ``launch_job``) explained for +non authenticated users are applicable for authenticated ones. + +The main differences are: + +* Asynchronous results are kept at server side for ever (until the user decides to remove one of them). +* Users can access to shared tables. +* It is also possible to set a token after logging using ``set_token`` function. + + +2.1. Login/Logout +~~~~~~~~~~~~~~~~~ + +Using the graphic interface: + + +*Note: Tkinter module is required to use login_gui method.* + +.. code-block:: python + + >>> from astroquery.esa.jwst import Jwst + >>> from astroquery.esa.jwst import Jwst + >>> Jwst.login_gui() + + +Using the command line: + +.. code-block:: python + + >>> from astroquery.esa.jwst import Jwst + >>> Jwst.login(user='userName', password='userPassword') + + +It is possible to use a file where the credentials are stored: + +*The file must containing user and password in two different lines.* + +.. code-block:: python + + >>> from astroquery.esa.jwst import Jwst + >>> Jwst.login(credentials_file='my_credentials_file') + +MAST tokens can also be used in command line functions: + +.. code-block:: python + + >>> from astroquery.esa.jwst import Jwst + >>> Jwst.login(user='userName', password='userPassword', token='mastToken') + +If the user is logged in and a MAST token has not been included or must be changed, it can be +specified using the ``set_token`` function. + +.. code-block:: python + + >>> from astroquery.esa.jwst import Jwst + >>> Jwst.login(user='userName', password='userPassword') + >>> Jwst.set_token(token='mastToken') + +To perform a logout: + + +.. code-block:: python + + >>> from astroquery.esa.jwst import Jwst + >>> Jwst.logout() + + + +2.2. Listing shared tables +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + >>> from astroquery.esa.jwst import Jwst + >>> tables=Jwst.load_tables(only_names=True, include_shared_tables=True) + >>> for table in (tables): + >>> print(table.name) + + public.dual + tap_schema.columns + tap_schema.key_columns + tap_schema.keys + tap_schema.schemas + tap_schema.tables + jwst.artifact + jwst.chunk + jwst.main + jwst.observation + jwst.observationmember + jwst.part + jwst.plane + jwst.plane_inputs + ... + user_schema_1.table1 + user_schema_2.table1 + ... + + +Reference/API +============= + +.. automodapi:: astroquery.esa.jwst + :no-inheritance-diagram: diff --git a/docs/index.rst b/docs/index.rst index 017f25559e..131da84360 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -180,6 +180,7 @@ The following modules have been completed using a common API: cds/cds.rst esa/hubble.rst esa/iso.rst + esa/jwst.rst esa/xmm_newton.rst esasky/esasky.rst eso/eso.rst @@ -292,6 +293,7 @@ generally return a table listing the available data first. cadc/cadc.rst casda/casda.rst esa/hubble.rst + esa/jwst.rst eso/eso.rst fermi/fermi.rst gaia/gaia.rst