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.
+
+
+
+
+
+ 1
+ MESSIER 001
+ 83.63321
+ 22.01446
+ SNR
+
+
+
+
+
+ 136
+ 2
+ 61
+ 18
+ 0
+ 2
+ 0
+
+
+
+
+
+
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 1
+ 05 34 31.94
+ +22 00 52.2
+ 6
+ 6
+
+
+ 0
+ C
+ R
+ 2011A&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.076
+ 20151087
+ 3
+ 6
+ 0.822
+ 0.328
+ 15.468
+ 0.068
+ 16.290
+ 0.320
+ 15.648
+ 0.000
+
+
+ 14.602
+ 0.000
+
+
+ 25168645
+ 83.655449
+ 21.999445
+ 1.187
+ 0.909
+ 20151087
+ 6
+ 27
+ 0.930
+ 0.056
+ 14.773
+ 0.031
+ 15.702
+ 0.047
+ 15.332
+ 0.170
+ 14.651
+ 0.183
+ 14.072
+ 0.036
+
+
+ 25169359
+ 83.625453
+ 21.982948
+ 0.421
+ 0.323
+ 20151087
+ 6
+ 30
+ 0.902
+ 0.068
+ 13.885
+ 0.043
+ 14.787
+ 0.054
+ 14.242
+ 0.067
+ 13.536
+ 0.055
+ 13.330
+ 0.034
+
+
+ 25169364
+ 83.634518
+ 22.009752
+ 0.982
+ 0.621
+ 20121088
+ 2
+ 3
+ 0.693
+ 0.199
+ 15.461
+ 0.000
+ 16.154
+ 0.199
+
+
+ 15.099
+ 0.000
+
+
+
+
+ 25169365
+ 83.630254
+ 22.017264
+ 0.733
+ 1.240
+ 20151087
+ 4
+ 11
+ 0.678
+ 0.024
+ 14.622
+ 0.023
+ 15.300
+ 0.006
+ 15.054
+ 0.064
+ 14.279
+ 0.000
+ 13.756
+ 0.095
+
+
+ 25169366
+ 83.631052
+ 22.016343
+ 1.459
+ 1.385
+ 20151087
+ 4
+ 7
+ 0.880
+ 0.000
+ 14.487
+ 0.000
+ 15.367
+ 0.000
+ 15.049
+ 0.038
+ 14.383
+ 0.094
+
+
+
+
+ 25169367
+ 83.642783
+ 22.033328
+ 0.455
+ 0.802
+ 20151087
+ 6
+ 30
+ 0.869
+ 0.060
+ 13.969
+ 0.026
+ 14.838
+ 0.055
+ 14.364
+ 0.061
+ 13.585
+ 0.049
+ 13.387
+ 0.058
+
+
+ 25169368
+ 83.659578
+ 22.029752
+ 1.151
+ 0.817
+ 20151087
+ 4
+ 17
+ 0.644
+ 0.104
+ 14.839
+ 0.080
+ 15.483
+ 0.066
+ 15.100
+ 0.022
+ 14.548
+ 0.139
+ 14.296
+ 0.011
+
+
+ 25169369
+ 83.667646
+ 22.021452
+ 1.180
+ 0.453
+ 20151087
+ 3
+ 9
+ 1.512
+ 0.000
+ 15.483
+ 0.000
+ 16.995
+ 0.000
+ 16.115
+ 0.013
+ 15.103
+ 0.147
+ 14.798
+ 0.000
+
+
+ 25169370
+ 83.620278
+ 22.026738
+ 0.746
+ 1.356
+ 20151087
+ 2
+ 6
+ 0.691
+ 0.115
+ 15.125
+ 0.112
+ 15.816
+ 0.027
+ 15.673
+ 0.000
+ 15.053
+ 0.000
+
+
+
+
+ 25169372
+ 83.610487
+ 22.037842
+ 0.534
+ 1.403
+ 20151088
+ 2
+ 7
+ 0.891
+ 0.042
+ 14.357
+ 0.034
+ 15.248
+ 0.024
+ 14.857
+ 0.000
+ 14.141
+ 0.000
+ 13.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.076
+ 20151087
+ 3
+ 6
+ 0.822
+ 0.328
+ 15.468
+ 0.068
+ 16.290
+ 0.320
+ 15.648
+ 0.000
+
+
+ 14.602
+ 0.000
+
+
+ 25168645
+ 83.655449
+ 21.999445
+ 1.187
+ 0.909
+ 20151087
+ 6
+ 27
+ 0.930
+ 0.056
+ 14.773
+ 0.031
+ 15.702
+ 0.047
+ 15.332
+ 0.170
+ 14.651
+ 0.183
+ 14.072
+ 0.036
+
+
+ 25169359
+ 83.625453
+ 21.982948
+ 0.421
+ 0.323
+ 20151087
+ 6
+ 30
+ 0.902
+ 0.068
+ 13.885
+ 0.043
+ 14.787
+ 0.054
+ 14.242
+ 0.067
+ 13.536
+ 0.055
+ 13.330
+ 0.034
+
+
+ 25169364
+ 83.634518
+ 22.009752
+ 0.982
+ 0.621
+ 20121088
+ 2
+ 3
+ 0.693
+ 0.199
+ 15.461
+ 0.000
+ 16.154
+ 0.199
+
+
+ 15.099
+ 0.000
+
+
+
+
+ 25169365
+ 83.630254
+ 22.017264
+ 0.733
+ 1.240
+ 20151087
+ 4
+ 11
+ 0.678
+ 0.024
+ 14.622
+ 0.023
+ 15.300
+ 0.006
+ 15.054
+ 0.064
+ 14.279
+ 0.000
+ 13.756
+ 0.095
+
+
+ 25169366
+ 83.631052
+ 22.016343
+ 1.459
+ 1.385
+ 20151087
+ 4
+ 7
+ 0.880
+ 0.000
+ 14.487
+ 0.000
+ 15.367
+ 0.000
+ 15.049
+ 0.038
+ 14.383
+ 0.094
+
+
+
+
+ 25169367
+ 83.642783
+ 22.033328
+ 0.455
+ 0.802
+ 20151087
+ 6
+ 30
+ 0.869
+ 0.060
+ 13.969
+ 0.026
+ 14.838
+ 0.055
+ 14.364
+ 0.061
+ 13.585
+ 0.049
+ 13.387
+ 0.058
+
+
+ 25169368
+ 83.659578
+ 22.029752
+ 1.151
+ 0.817
+ 20151087
+ 4
+ 17
+ 0.644
+ 0.104
+ 14.839
+ 0.080
+ 15.483
+ 0.066
+ 15.100
+ 0.022
+ 14.548
+ 0.139
+ 14.296
+ 0.011
+
+
+ 25169369
+ 83.667646
+ 22.021452
+ 1.180
+ 0.453
+ 20151087
+ 3
+ 9
+ 1.512
+ 0.000
+ 15.483
+ 0.000
+ 16.995
+ 0.000
+ 16.115
+ 0.013
+ 15.103
+ 0.147
+ 14.798
+ 0.000
+
+
+ 25169370
+ 83.620278
+ 22.026738
+ 0.746
+ 1.356
+ 20151087
+ 2
+ 6
+ 0.691
+ 0.115
+ 15.125
+ 0.112
+ 15.816
+ 0.027
+ 15.673
+ 0.000
+ 15.053
+ 0.000
+
+
+
+
+ 25169372
+ 83.610487
+ 22.037842
+ 0.534
+ 1.403
+ 20151088
+ 2
+ 7
+ 0.891
+ 0.042
+ 14.357
+ 0.034
+ 15.248
+ 0.024
+ 14.857
+ 0.000
+ 14.141
+ 0.000
+ 13.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 1
+ 05 34 31.94
+ +22 00 52.2
+ 6
+ 6
+
+
+ 0
+ C
+ R
+ 2011A&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