files: # Licensed under a 3-clause BSD style license - see LICENSE.rst
  """
  ======================
  eHST Astroquery Module
  ======================
  
  European Space Astronomy Centre (ESAC)
  European Space Agency (ESA)
  
  """
  from urllib.parse import urlencode
  
  from astropy import units
  from astropy.coordinates import SkyCoord
  from astropy.coordinates import Angle
  
  from astroquery.utils.tap import TapPlus
  from astroquery.query import BaseQuery
  import json
  import warnings
  from astropy.utils.exceptions import AstropyDeprecationWarning
  from astropy.utils.decorators import deprecated
  
  from . import conf
  from astroquery import log
  
  __all__ = ['ESAHubble', 'ESAHubbleClass']
  
  
  class ESAHubbleClass(BaseQuery):
      """
      Class to init ESA Hubble Module and communicate with eHST TAP
      """
      TIMEOUT = conf.TIMEOUT
      calibration_levels = {"AUXILIARY": 0, "RAW": 1, "CALIBRATED": 2,
                            "PRODUCT": 3}
      product_types = ["SCIENCE", "PREVIEW", "THUMBNAIL" or "AUXILIARY"]
      copying_string = "Copying file to {0}..."
  
      def __init__(self, *, tap_handler=None, show_messages=True):
          if tap_handler is None:
              self._tap = TapPlus(url=conf.EHST_TAP_SERVER,
                                  data_context='data', client_id="ASTROQUERY")
          else:
              self._tap = tap_handler
          if show_messages:
              self.get_status_messages()
  
      def download_product(self, observation_id, *, calibration_level=None,
                           filename=None, verbose=False, product_type=None):
          """
          Download products from EHST based on their observation ID and the
          calibration level or the product type.
  
          Parameters
          ----------
          observation_id : string
              id of the observation to be downloaded, mandatory
              The identifier of the observation we want to retrieve, regardless
              of whether it is simple or composite.
          calibration_level : string
              calibration level, optional
              The identifier of the data reduction/processing applied to the
              data. By default, the most scientifically relevant level will be
              chosen. RAW, CALIBRATED, PRODUCT or AUXILIARY
          filename : string
              file name to be used to store the artifact, optional, default
              None
              File name for the observation.
          verbose : bool
              optional, default 'False'
              flag to display information about the process
          product_type : string
              type of product retrieval, optional
              SCIENCE, PREVIEW, THUMBNAIL or AUXILIARY
              ------------
              Deprecation Warning: PRODUCT, SCIENCE_PRODUCT or POSTCARD
              are no longer supported.
              ------------
  
          Returns
          -------
          None. It downloads the observation indicated
          """
          if product_type and product_type in ['PRODUCT', 'SCIENCE_PRODUCT', 'POSTCARD']:
              warnings.warn(
                  "PRODUCT, SCIENCE_PRODUCT or POSTCARD product types are no longer supported. "
                  "Please use SCIENCE, PREVIEW, THUMBNAIL or AUXILIARY instead.",
                  AstropyDeprecationWarning)
  
          params = {"OBSERVATIONID": observation_id,
                    "TAPCLIENT": "ASTROQUERY",
                    "RETRIEVAL_TYPE": "OBSERVATION"}
  
          if filename is None:
              filename = observation_id + ".tar"
  
          if calibration_level:
              params["CALIBRATIONLEVEL"] = calibration_level
  
          # Product type check to ensure backwards compatibility
          product_type = self.__set_product_type(product_type)
          if product_type:
              self.__validate_product_type(product_type)
              params["PRODUCTTYPE"] = product_type
  
          filename = self._get_product_filename(product_type, filename)
          self._tap.load_data(params, filename, verbose=verbose)
  
          return filename
  
      def __set_product_type(self, product_type):
          if product_type:
              if 'SCIENCE_PRODUCT' in product_type:
                  return 'SCIENCE'
              elif 'PRODUCT' in product_type:
                  return None
              elif 'POSTCARD' in product_type:
                  return 'PREVIEW'
          return product_type
  
      def get_member_observations(self, observation_id):
          """
          Returns the related members of simple and composite observations
  
          Parameters
          ----------
          observation_id : str
              Observation identifier.
  
          Returns
          -------
          A list of strings with the observation_id of the associated
          observations
          """
          observation_type = self.get_observation_type(observation_id)
  
          if 'Composite' in observation_type:
              oids = self._select_related_members(observation_id)
          elif 'Simple' in observation_type:
              oids = self._select_related_composite(observation_id)
          else:
              raise ValueError("Invalid observation id")
          return oids
  
      def get_hap_hst_link(self, observation_id):
          """
          Returns the related members of hap and hst observations
  
          Parameters
          ----------
          observation_id : string
             id of the observation to be downloaded, mandatory
             The identifier of the observation we want to retrieve, regardless
             of whether it is simple or composite.
  
          Returns
          -------
          A list of strings with the observation_id of the associated
          observations
          """
          observation_type = self.get_observation_type(observation_id)
          if 'Composite' in observation_type:
              raise ValueError("HAP-HST link is only available for simple observations. Input observation is Composite.")
          elif 'HAP' in observation_type:
              oids = self._select_related_members(observation_id)
          elif 'HST' in observation_type:
              query = ("select observation_id from ehst.observation where obs_type='HAP Simple' "
                       f"and members like '%{observation_id}%'")
              job = self.query_tap(query=query)
              oids = job["observation_id"].pformat(show_name=False)
          else:
              raise ValueError("Invalid observation id")
          return oids
  
      def get_observation_type(self, observation_id):
          """
          Returns the type of an observation
  
          Parameters
          ----------
          observation_id : string
             id of the observation to be downloaded, mandatory
             The identifier of the observation we want to retrieve, regardless
             of whether it is simple or composite.
  
          Returns
          -------
          String with the observation type
          """
          if observation_id is None:
              raise ValueError("Please input an observation id")
  
          query = f"select obs_type from ehst.observation where observation_id='{observation_id}'"
          job = self.query_tap(query=query)
          if any(job["obs_type"]):
              obs_type = self._get_decoded_string(string=job["obs_type"][0])
          else:
              raise ValueError("Invalid Observation ID")
          return obs_type
  
      def _select_related_members(self, observation_id):
          query = f"select members from ehst.observation where observation_id='{observation_id}'"
          job = self.query_tap(query=query)
          oids = self._get_decoded_string(string=job["members"][0]).replace("caom:HST/", "").split(" ")
          return oids
  
      def _select_related_composite(self, observation_id):
          query = f"select observation_id from ehst.observation where members like '%{observation_id}%'"
          job = self.query_tap(query=query)
          oids = job["observation_id"].pformat(show_name=False)
          return oids
  
      def __validate_product_type(self, product_type):
          if (product_type not in self.product_types):
              raise ValueError("This product_type is not allowed")
  
      def _get_product_filename(self, product_type, filename):
          if (product_type == "PRODUCT"):
              return filename
          elif (product_type == "SCIENCE"):
              log.info("This is a SCIENCE_PRODUCT, the filename will be "
                       f"renamed to {filename}.fits.gz")
              return f"{filename}.fits.gz"
          elif (product_type == "THUMBNAIL" or product_type == "PREVIEW"):
              log.info("This is a POSTCARD, the filename will be "
                       f"renamed to {filename}.jpg")
              return f"{filename}.jpg"
  
          return filename
  
      def get_artifact(self, artifact_id, *, filename=None, verbose=False):
          """
          Download artifacts from EHST. Artifact is a single Hubble product file.
  
          Parameters
          ----------
          artifact_id : string
              id of the artifact to be downloaded, mandatory
              The identifier of the physical product (file) we want to retrieve.
          filename : string
              file name to be used to store the artifact, optional, default None
              File name for the artifact
          verbose : bool
              optional, default 'False'
              flag to display information about the process
  
          Returns
          -------
          None. It downloads the artifact indicated
          """
  
          params = {"RETRIEVAL_TYPE": "PRODUCT", "ARTIFACTID": artifact_id, "TAPCLIENT": "ASTROQUERY"}
          if filename is None:
              filename = artifact_id
  
          self._tap.load_data(params, filename, verbose=verbose)
  
          return filename
  
      def get_postcard(self, observation_id, *, calibration_level="RAW",
                       resolution=256, filename=None, verbose=False):
          """
          Download postcards from EHST
  
          Parameters
          ----------
          observation_id : string
              id of the observation for which download the postcard, mandatory
              The identifier of the observation we want to retrieve, regardless
              of whether it is simple or composite.
          calibration_level : string
              calibration level, optional, default 'RAW'
              The identifier of the data reduction/processing applied to the
              data. By default, the most scientifically relevant level will be
              chosen. RAW, CALIBRATED, PRODUCT or AUXILIARY
          resolution : integer
              postcard resolution, optional, default 256
              Resolution of the retrieved postcard. 256 or 1024
          filename : string
              file name to be used to store the postcard, optional, default None
              File name for the artifact
          verbose : bool
              optional, default 'False'
              Flag to display information about the process
  
          Returns
          -------
          None. It downloads the observation postcard indicated
          """
  
          params = {"RETRIEVAL_TYPE": "OBSERVATION",
                    "OBSERVATIONID": observation_id,
                    "PRODUCTTYPE": "PREVIEW",
                    "TAPCLIENT": "ASTROQUERY"}
          if calibration_level:
              params["CALIBRATIONLEVEL"] = calibration_level
  
          if resolution:
              params["PRODUCTTYPE"] = self.__get_product_type_by_resolution(resolution)
  
          if filename is None:
              filename = observation_id
  
          self._tap.load_data(params, filename, verbose=verbose)
  
          return filename
  
      def __get_product_type_by_resolution(self, resolution):
          if resolution == 256:
              return 'THUMBNAIL'
          elif resolution == 1024:
              return 'PREVIEW'
          else:
              raise ValueError("Resolution must be 256 or 1024")
  
      def cone_search(self, coordinates, radius, *, filename=None,
                      output_format='votable', cache=True,
                      async_job=False, verbose=False):
          """
          To execute a cone search defined by a coordinate and a radius
  
          Parameters
          ----------
          coordinates : astropy.coordinate, mandatory
              coordinates of the center in the cone search
          radius : float or quantity
              radius in arcmin (int, float) or quantity of the cone_search
          filename : str, default None
              Path and name of the file to store the results.
              If the filename is defined, the file will be
              automatically saved
          output_format : string
              results format. Options are:
              'votable': str, binary VOTable format
              'csv': str, comma-separated values format
          async_job : bool, optional, default 'False'
              executes the query (job) in asynchronous/synchronous mode (default
              synchronous)
          cache : bool
              optional, default 'True'
              Flag to save the results in the local cache
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          Returns
          -------
          astropy.table.Table with the result of the cone_search
          """
          coord = self._getCoordInput(coordinates)
          if isinstance(radius, int) or isinstance(radius, float):
              radius_in_grades = Angle(radius, units.arcmin).deg
          else:
              radius_in_grades = radius.to(units.deg).value
          ra = coord.ra.deg
          dec = coord.dec.deg
          query = ("select o.observation_id, "
                   "o.start_time, o.end_time, o.start_time_mjd, "
                   "o.end_time_mjd, o.exposure_duration, o.release_date, "
                   "o.run_id, o.program_id, o.set_id, o.collection, "
                   "o.members_number, o.instrument_configuration, "
                   "o.instrument_name, o.obs_type, o.target_moving, "
                   "o.target_name, o.target_description, o.proposal_id, "
                   "o.pi_name, prop.title, pl.metadata_provenance, "
                   "pl.data_product_type, pl.software_version, pos.ra, "
                   "pos.dec, pos.gal_lat, pos.gal_lon, pos.ecl_lat, "
                   "pos.ecl_lon, pos.fov_size, en.wave_central, "
                   "en.wave_bandwidth, en.wave_max, en.wave_min, "
                   "en.filter from ehst.observation o join ehst.proposal "
                   "prop on o.proposal_id=prop.proposal_id join ehst.plane "
                   "pl on pl.observation_id=o.observation_id join "
                   "ehst.position pos on pos.plane_id = pl.plane_id join "
                   "ehst.energy en on en.plane_id=pl.plane_id where "
                   "pl.main_science_plane='true' and 1=CONTAINS(POINT('ICRS', "
                   f"pos.ra, pos.dec),CIRCLE('ICRS', {str(ra)}, {str(dec)}, {str(radius_in_grades)})) order "
                   "by prop.proposal_id desc")
  
          if verbose:
              log.info(query)
          table = self.query_tap(query=query, async_job=async_job,
                                 output_file=filename,
                                 output_format=output_format,
                                 verbose=verbose)
          return table
  
      def cone_search_criteria(self, radius, *, target=None,
                               coordinates=None,
                               calibration_level=None,
                               data_product_type=None,
                               intent=None,
                               obs_collection=None,
                               instrument_name=None,
                               filters=None,
                               async_job=True,
                               filename=None,
                               output_format='votable',
                               save=False,
                               cache=True,
                               verbose=False):
          """
          To execute a cone search defined by a coordinate (an
          astropy.coordinate element or a target name which is resolved),
          a radius and a set of criteria to filter the results. This function
          comprises the outputs of query_target, cone_search and query_criteria
          methods.
  
          Parameters
          ----------
          radius : float or quantity
              radius in arcmin (int, float) or quantity of the cone_search
          target : str, mandatory if no coordinates is provided
              name of the target, that will act as center in the cone search
          coordinates : astropy.coordinate, mandatory if no target is provided
              coordinates of the center in the cone search
          calibration_level : str or int, optional
              The identifier of the data reduction/processing applied to the
              data. RAW (1), CALIBRATED (2), PRODUCT (3) or AUXILIARY (0)
          data_product_type : str, optional
              High level description of the product.
              image, spectrum or timeseries.
          intent : str, optional
              The intent of the original observer in acquiring this observation.
              SCIENCE or CALIBRATION
          collection : list of str, optional
              List of collections that are available in eHST catalogue.
              HLA, HST
          instrument_name : list of str, optional
              Name(s) of the instrument(s) used to generate the dataset
          filters : list of str, optional
              Name(s) of the filter(s) used to generate the dataset
          async_job : bool, optional, default 'False'
              executes the query (job) in asynchronous/synchronous mode (default
              synchronous)
          filename : str, default None
              Path and name of the file to store the results.
              If the filename is defined, the file will be
              automatically saved
          output_format : string
              results format. Options are:
              'votable': str, binary VOTable format
              'csv': str, comma-separated values format
          save : bool
              optional, default 'False'
              Flag to save the result in a file. If the filename
              is not defined, it will use a formatted name to save
              the file
          cache : bool
              optional, default 'True'
              Flag to save the results in the local cache
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          Returns
          -------
          astropy.table.Table with the result of the cone_search
          """
          crit_query = self.query_criteria(calibration_level=calibration_level,
                                           data_product_type=data_product_type,
                                           intent=intent,
                                           obs_collection=obs_collection,
                                           instrument_name=instrument_name,
                                           filters=filters,
                                           async_job=True,
                                           get_query=True)
          if crit_query.endswith(")"):
              crit_query = crit_query[:-1] + " AND "
          else:
              crit_query = crit_query + " WHERE ("
  
          if target and coordinates:
              raise TypeError("Please use only target or coordinates as"
                              "parameter.")
          if target:
              coord = self._query_tap_target(target)
          else:
              coord = self._getCoordInput(coordinates)
  
          ra = coord.ra.deg
          dec = coord.dec.deg
  
          if isinstance(radius, int) or isinstance(radius, float):
              radius_in_grades = Angle(radius, units.arcmin).deg
          else:
              radius_in_grades = radius.to(units.deg).value
          cone_query = "1=CONTAINS(POINT('ICRS', ra, dec)," \
                       "CIRCLE('ICRS', {0}, {1}, {2}))". \
              format(str(ra), str(dec), str(radius_in_grades))
          query = "{}{})".format(crit_query, cone_query)
          if verbose:
              log.info(query)
  
          table = self.query_tap(query=query, async_job=async_job,
                                 output_file=filename,
                                 output_format=output_format,
                                 verbose=verbose)
          return table
  
      def _query_tap_target(self, target):
          try:
              params = {"TARGET_NAME": target,
                        "RESOLVER_TYPE": "ALL",
                        "FORMAT": "json",
                        "TAPCLIENT": "ASTROQUERY"}
  
              subContext = conf.EHST_TARGET_ACTION
              connHandler = self._tap._TapPlus__getconnhandler()
              data = urlencode(params)
              target_response = connHandler.execute_secure(subContext, data, True)
              for line in target_response:
                  target_result = json.loads(line.decode("utf-8"))
                  if target_result['objects']:
                      ra = target_result['objects'][0]['raDegrees']
                      dec = target_result['objects'][0]['decDegrees']
              return SkyCoord(ra=ra, dec=dec, unit="deg")
          except (ValueError, KeyError):
              raise ValueError("This target cannot be resolved")
  
      def query_metadata(self, *, output_format='votable', verbose=False):
          return
  
      def query_target(self, name, *, filename=None, output_format='votable',
                       verbose=False, async_job=False, radius=7):
          """
          It executes a query over EHST and download the xml with the results.
  
          Parameters
          ----------
          name : string
              target name to be requested, mandatory
          filename : string
              file name to be used to store the metadata, optional, default None
          output_format : string
              optional, default 'votable'
              output format of the query
          verbose : bool
              optional, default 'False'
              Flag to display information about the process
          async_job : bool, optional, default 'False'
              executes the query (job) in asynchronous/synchronous mode (default
              synchronous)
          radius : int
              optional, default 7
              radius in arcmin (int, float) or quantity of the cone_search
  
          Returns
          -------
          Table with the result of the query. It downloads metadata as a file.
          """
          coordinates = self._query_tap_target(name)
          table = self.cone_search(coordinates, radius, filename=filename, output_format=output_format,
                                   verbose=verbose, async_job=async_job)
  
          return table
  
      def query_tap(self, query, *, async_job=False, output_file=None,
                    output_format="votable", verbose=False):
          """Launches a synchronous or asynchronous job to query the HST tap
  
                  Parameters
                  ----------
                  query : str, mandatory
                      query (adql) to be executed
                  async_job : bool, optional, default 'False'
                      executes the query (job) in asynchronous/synchronous mode (default
                      synchronous)
                  output_file : str, optional, default None
                      file name where the results are saved if dumpToFile is True.
                      If this parameter is not provided, the jobid is used instead
                  output_format : str, optional, default 'votable'
                      results format
                  verbose : bool, optional, default 'False'
                      flag to display information about the process
  
                  Returns
                  -------
                  A table object
                  """
          if async_job:
              job = self._tap.launch_job_async(query=query,
                                               output_file=output_file,
                                               output_format=output_format,
                                               verbose=verbose,
                                               dump_to_file=output_file is not None)
          else:
              job = self._tap.launch_job(query=query, output_file=output_file,
                                         output_format=output_format,
                                         verbose=verbose,
                                         dump_to_file=output_file is not None)
          table = job.get_results()
          return table
  
      @deprecated(since="0.4.7", alternative="query_tap")
      def query_hst_tap(self, query, *, async_job=False, output_file=None,
                        output_format="votable", verbose=False):
          """Launches a synchronous or asynchronous job to query the HST tap
  
          Parameters
          ----------
          query : str, mandatory
              query (adql) to be executed
          async_job : bool, optional, default 'False'
              executes the query (job) in asynchronous/synchronous mode (default
              synchronous)
          output_file : str, optional, default None
              file name where the results are saved if dumpToFile is True.
              If this parameter is not provided, the jobid is used instead
          output_format : str, optional, default 'votable'
              results format
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          Returns
          -------
          A table object
          """
          self.query_tap(query=query, async_job=False, output_file=None,
                         output_format="votable", verbose=False)
  
      def query_criteria(self, *, calibration_level=None,
                         data_product_type=None, intent=None,
                         obs_collection=None, instrument_name=None,
                         filters=None, async_job=True, output_file=None,
                         output_format="votable", verbose=False,
                         get_query=False):
          """
          Launches a synchronous or asynchronous job to query the HST tap
          using calibration level, data product type, intent, collection,
          instrument name, and filters as criteria to create and execute the
          associated query.
  
          Parameters
          ----------
          calibration_level : str or int, optional
              The identifier of the data reduction/processing applied to the
              data. RAW (1), CALIBRATED (2), PRODUCT (3) or AUXILIARY (0)
          data_product_type : str, optional
              High level description of the product.
              image, spectrum or timeseries.
          intent : str, optional
              The intent of the original observer in acquiring this observation.
              SCIENCE or CALIBRATION
          collection : list of str, optional
              List of collections that are available in eHST catalogue.
              HLA, HST
          instrument_name : list of str, optional
              Name(s) of the instrument(s) used to generate the dataset
          filters : list of str, optional
              Name(s) of the filter(s) used to generate the dataset
          async_job : bool, optional, default 'True'
              executes the query (job) in asynchronous/synchronous mode (default
              synchronous)
          output_file : str, optional, default None
              file name where the results are saved if dumpToFile is True.
              If this parameter is not provided, the jobid is used instead
          output_format : str, optional, default 'votable'
              results format
          verbose : bool, optional, default 'False'
              flag to display information about the process
          get_query : bool, optional, default 'False'
              flag to return the query associated to the criteria as the result
              of this function.
  
          Returns
          -------
          A table object
          """
  
          parameters = []
          if calibration_level is not None:
              parameters.append("calibration_level={}".format(
                  self.__get_calibration_level(calibration_level)))
          if data_product_type is not None:
              if isinstance(data_product_type, str):
                  parameters.append("data_product_type LIKE '%{}%'".format(
                      data_product_type))
              else:
                  raise ValueError("data_product_type must be a string")
          if intent is not None:
              if isinstance(intent, str):
                  parameters.append("intent LIKE '%{}%'".format(intent.lower()))
              else:
                  raise ValueError("intent must be a string")
          if self.__check_list_strings(obs_collection):
              parameters.append("(collection LIKE '%{}%')".format(
                  "%' OR collection LIKE '%".join(obs_collection)
              ))
          if self.__check_list_strings(instrument_name):
              parameters.append("(instrument_name LIKE '%{}%')".format(
                  "%' OR instrument_name LIKE '%".join(instrument_name)
              ))
          if self.__check_list_strings(filters):
              parameters.append("(instrument_configuration LIKE '%{}%')"
                                .format("%' OR instrument_configuration "
                                        "LIKE '%".join(filters)))
          query = "select * from ehst.archive"
          if parameters:
              query += " where({})".format(" AND ".join(parameters))
          if verbose:
              log.info(query)
          if get_query:
              return query
          table = self.query_tap(query=query, async_job=async_job,
                                 output_file=output_file,
                                 output_format=output_format,
                                 verbose=verbose)
          return table
  
      def __get_calibration_level(self, calibration_level):
          condition = ""
          if (calibration_level is not None):
              if isinstance(calibration_level, int):
                  if calibration_level < 4:
                      condition = calibration_level
                  else:
                      raise KeyError("Calibration level must be between 0 and 3")
              elif isinstance(calibration_level, str):
                  condition = self.calibration_levels[calibration_level]
              else:
                  raise KeyError("Calibration level must be either "
                                 "a string or an integer")
          return condition
  
      def __check_list_strings(self, list):
          if list is None:
              return False
          if list and all(isinstance(elem, str) for elem in list):
              return True
          else:
              raise ValueError("One of the lists is empty or there are "
                               "elements that are not strings")
  
      def get_tables(self, *, only_names=True, verbose=False):
          """Get the available table in EHST TAP service
  
          Parameters
          ----------
          only_names : bool, TAP+ only, optional, default 'False'
              True to load table names only
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          Returns
          -------
          A list of tables
          """
  
          tables = self._tap.load_tables(only_names=only_names,
                                         include_shared_tables=False,
                                         verbose=verbose)
          if only_names is True:
              table_names = []
              for t in tables:
                  table_names.append(t.name)
              return table_names
          else:
              return tables
  
      def get_status_messages(self):
          """Retrieve the messages to inform users about
          the status of eHST TAP
          """
  
          try:
              subContext = conf.EHST_MESSAGES
              connHandler = self._tap._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:])
          except OSError:
              print("Status messages could not be retrieved")
  
      def get_columns(self, table_name, *, only_names=True, verbose=False):
          """Get the available columns for a table in EHST TAP service
  
          Parameters
          ----------
          table_name : string, mandatory, default None
              table name of which, columns will be returned
          only_names : bool, TAP+ only, optional, default 'False'
              True to load table names only
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          Returns
          -------
          A list of columns
          """
  
          tables = self._tap.load_tables(only_names=False,
                                         include_shared_tables=False,
                                         verbose=verbose)
          columns = None
          for t in tables:
              if str(t.name) == str(table_name):
                  columns = t.columns
                  break
  
          if columns is None:
              raise ValueError("table name specified is not found in "
                               "EHST TAP service")
  
          if only_names is True:
              column_names = []
              for c in columns:
                  column_names.append(c.name)
              return column_names
          else:
              return columns
  
      def _getCoordInput(self, value):
          if not (isinstance(value, str)
                  or isinstance(value, SkyCoord)):
              raise ValueError("Coordinates"
                               + " must be either a string or astropy.coordinates")
          if isinstance(value, str):
              return SkyCoord(value)
          else:
              return value
  
      def _get_decoded_string(self, string):
          try:
              return string.decode('utf-8')
          except (UnicodeDecodeError, AttributeError):
              return string
  
  
  ESAHubble = ESAHubbleClass()
  
files: astroquery/esa/hubble/core.py
files: # Licensed under a 3-clause BSD style license - see LICENSE.rst
  """
  ======================
  eHST Astroquery Module
  ======================
  
  European Space Astronomy Centre (ESAC)
  European Space Agency (ESA)
  
  """
  
  from astropy import units
  from astropy.coordinates import SkyCoord
  from astropy.coordinates import Angle
  
  from astroquery.utils.tap import TapPlus
  from astroquery.query import BaseQuery
  import json
  import warnings
  from astropy.utils.exceptions import AstropyDeprecationWarning
  from astropy.utils.decorators import deprecated
  
  from . import conf
  from astroquery import log
  
  __all__ = ['ESAHubble', 'ESAHubbleClass']
  
  
  class ESAHubbleClass(BaseQuery):
      """
      Class to init ESA Hubble Module and communicate with eHST TAP
      """
      TIMEOUT = conf.TIMEOUT
      calibration_levels = {"AUXILIARY": 0, "RAW": 1, "CALIBRATED": 2,
                            "PRODUCT": 3}
      product_types = ["SCIENCE", "PREVIEW", "THUMBNAIL" or "AUXILIARY"]
      copying_string = "Copying file to {0}..."
  
      def __init__(self, *, tap_handler=None, show_messages=True):
          if tap_handler is None:
              self._tap = TapPlus(url=conf.EHST_TAP_SERVER,
                                  data_context='data', client_id="ASTROQUERY")
          else:
              self._tap = tap_handler
          if show_messages:
              self.get_status_messages()
  
      def download_product(self, observation_id, *, calibration_level=None,
                           filename=None, verbose=False, product_type=None):
          """
          Download products from EHST based on their observation ID and the
          calibration level or the product type.
  
          Parameters
          ----------
          observation_id : string
              id of the observation to be downloaded, mandatory
              The identifier of the observation we want to retrieve, regardless
              of whether it is simple or composite.
          calibration_level : string
              calibration level, optional
              The identifier of the data reduction/processing applied to the
              data. By default, the most scientifically relevant level will be
              chosen. RAW, CALIBRATED, PRODUCT or AUXILIARY
          filename : string
              file name to be used to store the artifact, optional, default
              None
              File name for the observation.
          verbose : bool
              optional, default 'False'
              flag to display information about the process
          product_type : string
              type of product retrieval, optional
              SCIENCE, PREVIEW, THUMBNAIL or AUXILIARY
              ------------
              Deprecation Warning: PRODUCT, SCIENCE_PRODUCT or POSTCARD
              are no longer supported.
              ------------
  
          Returns
          -------
          None. It downloads the observation indicated
          """
          if product_type and product_type in ['PRODUCT', 'SCIENCE_PRODUCT', 'POSTCARD']:
              warnings.warn(
                  "PRODUCT, SCIENCE_PRODUCT or POSTCARD product types are no longer supported. "
                  "Please use SCIENCE, PREVIEW, THUMBNAIL or AUXILIARY instead.",
                  AstropyDeprecationWarning)
  
          params = {"OBSERVATIONID": observation_id,
                    "TAPCLIENT": "ASTROQUERY",
                    "RETRIEVAL_TYPE": "OBSERVATION"}
  
          if filename is None:
              filename = observation_id + ".tar"
  
          if calibration_level:
              params["CALIBRATIONLEVEL"] = calibration_level
  
          # Product type check to ensure backwards compatibility
          product_type = self.__set_product_type(product_type)
          if product_type:
              self.__validate_product_type(product_type)
              params["PRODUCTTYPE"] = product_type
  
          filename = self._get_product_filename(product_type, filename)
          self._tap.load_data(params, filename, verbose=verbose)
  
          return filename
  
      def __set_product_type(self, product_type):
          if product_type:
              if 'SCIENCE_PRODUCT' in product_type:
                  return 'SCIENCE'
              elif 'PRODUCT' in product_type:
                  return None
              elif 'POSTCARD' in product_type:
                  return 'PREVIEW'
          return product_type
  
      def get_member_observations(self, observation_id):
          """
          Returns the related members of simple and composite observations
  
          Parameters
          ----------
          observation_id : str
              Observation identifier.
  
          Returns
          -------
          A list of strings with the observation_id of the associated
          observations
          """
          observation_type = self.get_observation_type(observation_id)
  
          if 'Composite' in observation_type:
              oids = self._select_related_members(observation_id)
          elif 'Simple' in observation_type:
              oids = self._select_related_composite(observation_id)
          else:
              raise ValueError("Invalid observation id")
          return oids
  
      def get_hap_hst_link(self, observation_id):
          """
          Returns the related members of hap and hst observations
  
          Parameters
          ----------
          observation_id : string
             id of the observation to be downloaded, mandatory
             The identifier of the observation we want to retrieve, regardless
             of whether it is simple or composite.
  
          Returns
          -------
          A list of strings with the observation_id of the associated
          observations
          """
          observation_type = self.get_observation_type(observation_id)
          if 'Composite' in observation_type:
              raise ValueError("HAP-HST link is only available for simple observations. Input observation is Composite.")
          elif 'HAP' in observation_type:
              oids = self._select_related_members(observation_id)
          elif 'HST' in observation_type:
              query = ("select observation_id from ehst.observation where obs_type='HAP Simple' "
                       f"and members like '%{observation_id}%'")
              job = self.query_tap(query=query)
              oids = job["observation_id"].pformat(show_name=False)
          else:
              raise ValueError("Invalid observation id")
          return oids
  
      def get_observation_type(self, observation_id):
          """
          Returns the type of an observation
  
          Parameters
          ----------
          observation_id : string
             id of the observation to be downloaded, mandatory
             The identifier of the observation we want to retrieve, regardless
             of whether it is simple or composite.
  
          Returns
          -------
          String with the observation type
          """
          if observation_id is None:
              raise ValueError("Please input an observation id")
  
          query = f"select obs_type from ehst.observation where observation_id='{observation_id}'"
          job = self.query_tap(query=query)
          if any(job["obs_type"]):
              obs_type = self._get_decoded_string(string=job["obs_type"][0])
          else:
              raise ValueError("Invalid Observation ID")
          return obs_type
  
      def _select_related_members(self, observation_id):
          query = f"select members from ehst.observation where observation_id='{observation_id}'"
          job = self.query_tap(query=query)
          oids = self._get_decoded_string(string=job["members"][0]).replace("caom:HST/", "").split(" ")
          return oids
  
      def _select_related_composite(self, observation_id):
          query = f"select observation_id from ehst.observation where members like '%{observation_id}%'"
          job = self.query_tap(query=query)
          oids = job["observation_id"].pformat(show_name=False)
          return oids
  
      def __validate_product_type(self, product_type):
          if (product_type not in self.product_types):
              raise ValueError("This product_type is not allowed")
  
      def _get_product_filename(self, product_type, filename):
          if (product_type == "PRODUCT"):
              return filename
          elif (product_type == "SCIENCE"):
              log.info("This is a SCIENCE_PRODUCT, the filename will be "
                       f"renamed to {filename}.fits.gz")
              return f"{filename}.fits.gz"
          elif (product_type == "THUMBNAIL" or product_type == "PREVIEW"):
              log.info("This is a POSTCARD, the filename will be "
                       f"renamed to {filename}.jpg")
              return f"{filename}.jpg"
  
          return filename
  
      def get_artifact(self, artifact_id, *, filename=None, verbose=False):
          """
          Download artifacts from EHST. Artifact is a single Hubble product file.
  
          Parameters
          ----------
          artifact_id : string
              id of the artifact to be downloaded, mandatory
              The identifier of the physical product (file) we want to retrieve.
          filename : string
              file name to be used to store the artifact, optional, default None
              File name for the artifact
          verbose : bool
              optional, default 'False'
              flag to display information about the process
  
          Returns
          -------
          None. It downloads the artifact indicated
          """
  
          params = {"RETRIEVAL_TYPE": "PRODUCT", "ARTIFACTID": artifact_id, "TAPCLIENT": "ASTROQUERY"}
          if filename is None:
              filename = artifact_id
  
          self._tap.load_data(params, filename, verbose=verbose)
  
          return filename
  
      def get_postcard(self, observation_id, *, calibration_level="RAW",
                       resolution=256, filename=None, verbose=False):
          """
          Download postcards from EHST
  
          Parameters
          ----------
          observation_id : string
              id of the observation for which download the postcard, mandatory
              The identifier of the observation we want to retrieve, regardless
              of whether it is simple or composite.
          calibration_level : string
              calibration level, optional, default 'RAW'
              The identifier of the data reduction/processing applied to the
              data. By default, the most scientifically relevant level will be
              chosen. RAW, CALIBRATED, PRODUCT or AUXILIARY
          resolution : integer
              postcard resolution, optional, default 256
              Resolution of the retrieved postcard. 256 or 1024
          filename : string
              file name to be used to store the postcard, optional, default None
              File name for the artifact
          verbose : bool
              optional, default 'False'
              Flag to display information about the process
  
          Returns
          -------
          None. It downloads the observation postcard indicated
          """
  
          params = {"RETRIEVAL_TYPE": "OBSERVATION",
                    "OBSERVATIONID": observation_id,
                    "PRODUCTTYPE": "PREVIEW",
                    "TAPCLIENT": "ASTROQUERY"}
          if calibration_level:
              params["CALIBRATIONLEVEL"] = calibration_level
  
          if resolution:
              params["PRODUCTTYPE"] = self.__get_product_type_by_resolution(resolution)
  
          if filename is None:
              filename = observation_id
  
          self._tap.load_data(params, filename, verbose=verbose)
  
          return filename
  
      def __get_product_type_by_resolution(self, resolution):
          if resolution == 256:
              return 'THUMBNAIL'
          elif resolution == 1024:
              return 'PREVIEW'
          else:
              raise ValueError("Resolution must be 256 or 1024")
  
      def cone_search(self, coordinates, radius, *, filename=None,
                      output_format='votable', cache=True,
                      async_job=False, verbose=False):
          """
          To execute a cone search defined by a coordinate and a radius
  
          Parameters
          ----------
          coordinates : astropy.coordinate, mandatory
              coordinates of the center in the cone search
          radius : float or quantity
              radius in arcmin (int, float) or quantity of the cone_search
          filename : str, default None
              Path and name of the file to store the results.
              If the filename is defined, the file will be
              automatically saved
          output_format : string
              results format. Options are:
              'votable': str, binary VOTable format
              'csv': str, comma-separated values format
          async_job : bool, optional, default 'False'
              executes the query (job) in asynchronous/synchronous mode (default
              synchronous)
          cache : bool
              optional, default 'True'
              Flag to save the results in the local cache
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          Returns
          -------
          astropy.table.Table with the result of the cone_search
          """
          coord = self._getCoordInput(coordinates)
          if isinstance(radius, int) or isinstance(radius, float):
              radius_in_grades = Angle(radius, units.arcmin).deg
          else:
              radius_in_grades = radius.to(units.deg).value
          ra = coord.ra.deg
          dec = coord.dec.deg
          query = ("select o.observation_id, "
                   "o.start_time, o.end_time, o.start_time_mjd, "
                   "o.end_time_mjd, o.exposure_duration, o.release_date, "
                   "o.run_id, o.program_id, o.set_id, o.collection, "
                   "o.members_number, o.instrument_configuration, "
                   "o.instrument_name, o.obs_type, o.target_moving, "
                   "o.target_name, o.target_description, o.proposal_id, "
                   "o.pi_name, prop.title, pl.metadata_provenance, "
                   "pl.data_product_type, pl.software_version, pos.ra, "
                   "pos.dec, pos.gal_lat, pos.gal_lon, pos.ecl_lat, "
                   "pos.ecl_lon, pos.fov_size, en.wave_central, "
                   "en.wave_bandwidth, en.wave_max, en.wave_min, "
                   "en.filter from ehst.observation o join ehst.proposal "
                   "prop on o.proposal_id=prop.proposal_id join ehst.plane "
                   "pl on pl.observation_id=o.observation_id join "
                   "ehst.position pos on pos.plane_id = pl.plane_id join "
                   "ehst.energy en on en.plane_id=pl.plane_id where "
                   "pl.main_science_plane='true' and 1=CONTAINS(POINT('ICRS', "
                   f"pos.ra, pos.dec),CIRCLE('ICRS', {str(ra)}, {str(dec)}, {str(radius_in_grades)})) order "
                   "by prop.proposal_id desc")
  
          if verbose:
              log.info(query)
          table = self.query_tap(query=query, async_job=async_job,
                                 output_file=filename,
                                 output_format=output_format,
                                 verbose=verbose)
          return table
  
      def cone_search_criteria(self, radius, *, target=None,
                               coordinates=None,
                               calibration_level=None,
                               data_product_type=None,
                               intent=None,
                               obs_collection=None,
                               instrument_name=None,
                               filters=None,
                               async_job=True,
                               filename=None,
                               output_format='votable',
                               save=False,
                               cache=True,
                               verbose=False):
          """
          To execute a cone search defined by a coordinate (an
          astropy.coordinate element or a target name which is resolved),
          a radius and a set of criteria to filter the results. This function
          comprises the outputs of query_target, cone_search and query_criteria
          methods.
  
          Parameters
          ----------
          radius : float or quantity
              radius in arcmin (int, float) or quantity of the cone_search
          target : str, mandatory if no coordinates is provided
              name of the target, that will act as center in the cone search
          coordinates : astropy.coordinate, mandatory if no target is provided
              coordinates of the center in the cone search
          calibration_level : str or int, optional
              The identifier of the data reduction/processing applied to the
              data. RAW (1), CALIBRATED (2), PRODUCT (3) or AUXILIARY (0)
          data_product_type : str, optional
              High level description of the product.
              image, spectrum or timeseries.
          intent : str, optional
              The intent of the original observer in acquiring this observation.
              SCIENCE or CALIBRATION
          collection : list of str, optional
              List of collections that are available in eHST catalogue.
              HLA, HST
          instrument_name : list of str, optional
              Name(s) of the instrument(s) used to generate the dataset
          filters : list of str, optional
              Name(s) of the filter(s) used to generate the dataset
          async_job : bool, optional, default 'False'
              executes the query (job) in asynchronous/synchronous mode (default
              synchronous)
          filename : str, default None
              Path and name of the file to store the results.
              If the filename is defined, the file will be
              automatically saved
          output_format : string
              results format. Options are:
              'votable': str, binary VOTable format
              'csv': str, comma-separated values format
          save : bool
              optional, default 'False'
              Flag to save the result in a file. If the filename
              is not defined, it will use a formatted name to save
              the file
          cache : bool
              optional, default 'True'
              Flag to save the results in the local cache
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          Returns
          -------
          astropy.table.Table with the result of the cone_search
          """
          crit_query = self.query_criteria(calibration_level=calibration_level,
                                           data_product_type=data_product_type,
                                           intent=intent,
                                           obs_collection=obs_collection,
                                           instrument_name=instrument_name,
                                           filters=filters,
                                           async_job=True,
                                           get_query=True)
          if crit_query.endswith(")"):
              crit_query = crit_query[:-1] + " AND "
          else:
              crit_query = crit_query + " WHERE ("
  
          if target and coordinates:
              raise TypeError("Please use only target or coordinates as"
                              "parameter.")
          if target:
              coord = self._query_tap_target(target)
          else:
              coord = self._getCoordInput(coordinates)
  
          ra = coord.ra.deg
          dec = coord.dec.deg
  
          if isinstance(radius, int) or isinstance(radius, float):
              radius_in_grades = Angle(radius, units.arcmin).deg
          else:
              radius_in_grades = radius.to(units.deg).value
          cone_query = "1=CONTAINS(POINT('ICRS', ra, dec)," \
                       "CIRCLE('ICRS', {0}, {1}, {2}))". \
              format(str(ra), str(dec), str(radius_in_grades))
          query = "{}{})".format(crit_query, cone_query)
          if verbose:
              log.info(query)
  
          table = self.query_tap(query=query, async_job=async_job,
                                 output_file=filename,
                                 output_format=output_format,
                                 verbose=verbose)
          return table
  
      def _query_tap_target(self, target):
          try:
              params = {"TARGET_NAME": target,
                        "RESOLVER_TYPE": "ALL",
                        "FORMAT": "json",
                        "TAPCLIENT": "ASTROQUERY"}
  
              subContext = conf.EHST_TARGET_ACTION
              connHandler = self._tap._TapPlus__getconnhandler()
              data = connHandler.url_encode(params)
              target_response = connHandler.execute_secure(subContext, data, True)
              for line in target_response:
                  target_result = json.loads(line.decode("utf-8"))
                  if target_result['objects']:
                      ra = target_result['objects'][0]['raDegrees']
                      dec = target_result['objects'][0]['decDegrees']
              return SkyCoord(ra=ra, dec=dec, unit="deg")
          except (ValueError, KeyError):
              raise ValueError("This target cannot be resolved")
  
      def query_metadata(self, *, output_format='votable', verbose=False):
          return
  
      def query_target(self, name, *, filename=None, output_format='votable',
                       verbose=False, async_job=False, radius=7):
          """
          It executes a query over EHST and download the xml with the results.
  
          Parameters
          ----------
          name : string
              target name to be requested, mandatory
          filename : string
              file name to be used to store the metadata, optional, default None
          output_format : string
              optional, default 'votable'
              output format of the query
          verbose : bool
              optional, default 'False'
              Flag to display information about the process
          async_job : bool, optional, default 'False'
              executes the query (job) in asynchronous/synchronous mode (default
              synchronous)
          radius : int
              optional, default 7
              radius in arcmin (int, float) or quantity of the cone_search
  
          Returns
          -------
          Table with the result of the query. It downloads metadata as a file.
          """
          coordinates = self._query_tap_target(name)
          table = self.cone_search(coordinates, radius, filename=filename, output_format=output_format,
                                   verbose=verbose, async_job=async_job)
  
          return table
  
      def query_tap(self, query, *, async_job=False, output_file=None,
                    output_format="votable", verbose=False):
          """Launches a synchronous or asynchronous job to query the HST tap
  
                  Parameters
                  ----------
                  query : str, mandatory
                      query (adql) to be executed
                  async_job : bool, optional, default 'False'
                      executes the query (job) in asynchronous/synchronous mode (default
                      synchronous)
                  output_file : str, optional, default None
                      file name where the results are saved if dumpToFile is True.
                      If this parameter is not provided, the jobid is used instead
                  output_format : str, optional, default 'votable'
                      results format
                  verbose : bool, optional, default 'False'
                      flag to display information about the process
  
                  Returns
                  -------
                  A table object
                  """
          if async_job:
              job = self._tap.launch_job_async(query=query,
                                               output_file=output_file,
                                               output_format=output_format,
                                               verbose=verbose,
                                               dump_to_file=output_file is not None)
          else:
              job = self._tap.launch_job(query=query, output_file=output_file,
                                         output_format=output_format,
                                         verbose=verbose,
                                         dump_to_file=output_file is not None)
          table = job.get_results()
          return table
  
      @deprecated(since="0.4.7", alternative="query_tap")
      def query_hst_tap(self, query, *, async_job=False, output_file=None,
                        output_format="votable", verbose=False):
          """Launches a synchronous or asynchronous job to query the HST tap
  
          Parameters
          ----------
          query : str, mandatory
              query (adql) to be executed
          async_job : bool, optional, default 'False'
              executes the query (job) in asynchronous/synchronous mode (default
              synchronous)
          output_file : str, optional, default None
              file name where the results are saved if dumpToFile is True.
              If this parameter is not provided, the jobid is used instead
          output_format : str, optional, default 'votable'
              results format
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          Returns
          -------
          A table object
          """
          self.query_tap(query=query, async_job=False, output_file=None,
                         output_format="votable", verbose=False)
  
      def query_criteria(self, *, calibration_level=None,
                         data_product_type=None, intent=None,
                         obs_collection=None, instrument_name=None,
                         filters=None, async_job=True, output_file=None,
                         output_format="votable", verbose=False,
                         get_query=False):
          """
          Launches a synchronous or asynchronous job to query the HST tap
          using calibration level, data product type, intent, collection,
          instrument name, and filters as criteria to create and execute the
          associated query.
  
          Parameters
          ----------
          calibration_level : str or int, optional
              The identifier of the data reduction/processing applied to the
              data. RAW (1), CALIBRATED (2), PRODUCT (3) or AUXILIARY (0)
          data_product_type : str, optional
              High level description of the product.
              image, spectrum or timeseries.
          intent : str, optional
              The intent of the original observer in acquiring this observation.
              SCIENCE or CALIBRATION
          collection : list of str, optional
              List of collections that are available in eHST catalogue.
              HLA, HST
          instrument_name : list of str, optional
              Name(s) of the instrument(s) used to generate the dataset
          filters : list of str, optional
              Name(s) of the filter(s) used to generate the dataset
          async_job : bool, optional, default 'True'
              executes the query (job) in asynchronous/synchronous mode (default
              synchronous)
          output_file : str, optional, default None
              file name where the results are saved if dumpToFile is True.
              If this parameter is not provided, the jobid is used instead
          output_format : str, optional, default 'votable'
              results format
          verbose : bool, optional, default 'False'
              flag to display information about the process
          get_query : bool, optional, default 'False'
              flag to return the query associated to the criteria as the result
              of this function.
  
          Returns
          -------
          A table object
          """
  
          parameters = []
          if calibration_level is not None:
              parameters.append("calibration_level={}".format(
                  self.__get_calibration_level(calibration_level)))
          if data_product_type is not None:
              if isinstance(data_product_type, str):
                  parameters.append("data_product_type LIKE '%{}%'".format(
                      data_product_type))
              else:
                  raise ValueError("data_product_type must be a string")
          if intent is not None:
              if isinstance(intent, str):
                  parameters.append("intent LIKE '%{}%'".format(intent.lower()))
              else:
                  raise ValueError("intent must be a string")
          if self.__check_list_strings(obs_collection):
              parameters.append("(collection LIKE '%{}%')".format(
                  "%' OR collection LIKE '%".join(obs_collection)
              ))
          if self.__check_list_strings(instrument_name):
              parameters.append("(instrument_name LIKE '%{}%')".format(
                  "%' OR instrument_name LIKE '%".join(instrument_name)
              ))
          if self.__check_list_strings(filters):
              parameters.append("(instrument_configuration LIKE '%{}%')"
                                .format("%' OR instrument_configuration "
                                        "LIKE '%".join(filters)))
          query = "select * from ehst.archive"
          if parameters:
              query += " where({})".format(" AND ".join(parameters))
          if verbose:
              log.info(query)
          if get_query:
              return query
          table = self.query_tap(query=query, async_job=async_job,
                                 output_file=output_file,
                                 output_format=output_format,
                                 verbose=verbose)
          return table
  
      def __get_calibration_level(self, calibration_level):
          condition = ""
          if (calibration_level is not None):
              if isinstance(calibration_level, int):
                  if calibration_level < 4:
                      condition = calibration_level
                  else:
                      raise KeyError("Calibration level must be between 0 and 3")
              elif isinstance(calibration_level, str):
                  condition = self.calibration_levels[calibration_level]
              else:
                  raise KeyError("Calibration level must be either "
                                 "a string or an integer")
          return condition
  
      def __check_list_strings(self, list):
          if list is None:
              return False
          if list and all(isinstance(elem, str) for elem in list):
              return True
          else:
              raise ValueError("One of the lists is empty or there are "
                               "elements that are not strings")
  
      def get_tables(self, *, only_names=True, verbose=False):
          """Get the available table in EHST TAP service
  
          Parameters
          ----------
          only_names : bool, TAP+ only, optional, default 'False'
              True to load table names only
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          Returns
          -------
          A list of tables
          """
  
          tables = self._tap.load_tables(only_names=only_names,
                                         include_shared_tables=False,
                                         verbose=verbose)
          if only_names is True:
              table_names = []
              for t in tables:
                  table_names.append(t.name)
              return table_names
          else:
              return tables
  
      def get_status_messages(self):
          """Retrieve the messages to inform users about
          the status of eHST TAP
          """
  
          try:
              subContext = conf.EHST_MESSAGES
              connHandler = self._tap._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:])
          except OSError:
              print("Status messages could not be retrieved")
  
      def get_columns(self, table_name, *, only_names=True, verbose=False):
          """Get the available columns for a table in EHST TAP service
  
          Parameters
          ----------
          table_name : string, mandatory, default None
              table name of which, columns will be returned
          only_names : bool, TAP+ only, optional, default 'False'
              True to load table names only
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          Returns
          -------
          A list of columns
          """
  
          tables = self._tap.load_tables(only_names=False,
                                         include_shared_tables=False,
                                         verbose=verbose)
          columns = None
          for t in tables:
              if str(t.name) == str(table_name):
                  columns = t.columns
                  break
  
          if columns is None:
              raise ValueError("table name specified is not found in "
                               "EHST TAP service")
  
          if only_names is True:
              column_names = []
              for c in columns:
                  column_names.append(c.name)
              return column_names
          else:
              return columns
  
      def _getCoordInput(self, value):
          if not (isinstance(value, str)
                  or isinstance(value, SkyCoord)):
              raise ValueError("Coordinates"
                               + " must be either a string or astropy.coordinates")
          if isinstance(value, str):
              return SkyCoord(value)
          else:
              return value
  
      def _get_decoded_string(self, string):
          try:
              return string.decode('utf-8')
          except (UnicodeDecodeError, AttributeError):
              return string
  
  
  ESAHubble = ESAHubbleClass()
  
files: astroquery/esa/hubble/core.py
files: # 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 datetime import datetime
  from urllib.parse import urlencode
  
  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)
      """
  
      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, show_messages=True):
          if tap_plus_handler is None:
              self.__jwsttap = TapPlus(url=conf.JWST_TAP_SERVER,
                                       data_context='data', client_id='ASTROQUERY')
          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
  
          if show_messages:
              self.get_status_messages()
  
      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 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
          data = urlencode({"token": token})
          connHandler = self.__jwsttap._TapPlus__getconnhandler()
          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
          """
  
          try:
              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:])
          except OSError:
              print("Status messages could not be retrieved")
  
      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:
              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['TAPCLIENT'] = '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['TAPCLIENT'] = '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()
  
files: astroquery/esa/jwst/core.py
files: # 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 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)
      """
  
      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, show_messages=True):
          if tap_plus_handler is None:
              self.__jwsttap = TapPlus(url=conf.JWST_TAP_SERVER,
                                       data_context='data', client_id='ASTROQUERY')
          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
  
          if show_messages:
              self.get_status_messages()
  
      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 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
          """
  
          try:
              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:])
          except OSError:
              print("Status messages could not be retrieved")
  
      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:
              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['TAPCLIENT'] = '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['TAPCLIENT'] = '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()
  
files: astroquery/esa/jwst/core.py
files: # Licensed under a 3-clause BSD style license - see LICENSE.rst
  """
  =============
  Gaia TAP plus
  =============
  
  @author: Juan Carlos Segovia
  @contact: juan.carlos.segovia@sciops.esa.int
  
  European Space Astronomy Centre (ESAC)
  European Space Agency (ESA)
  
  Created on 30 jun. 2016
  
  
  """
  CONTENT_TYPE_POST_DEFAULT = "application/x-www-form-urlencoded"
  
  
  class DummyTapHandler:
  
      def __init__(self):
          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 execute_upload(self, data,
                         content_type=CONTENT_TYPE_POST_DEFAULT,
                         verbose=False):
          pass
  
      def check_method(self, method):
          if method == self.__invokedMethod:
              return
          else:
              raise Exception(f"Method '{method}' not invoked. (Invoked method is "
                              f"{self.__invokedMethod}')")
  
      def check_parameters(self, parameters, method_name):
          if parameters is None:
              return len(self.__parameters) == 0
          num_params_in_call = len(self.__parameters)
          num_params_expected = len(parameters)
          print("Num parameters found in call: {}, expected: {}".
                format(num_params_in_call, num_params_expected))
          if num_params_expected != num_params_in_call:
              raise Exception("Wrong number of parameters for method '%s'. \
              Found: %d. Expected %d",
                              (method_name,
                               len(self.__parameters),
                               len(parameters)))
          for key in parameters:
              if key in self.__parameters:
                  # check value
                  if self.__parameters[key] != parameters[key]:
                      k = self.__parameters[key]
                      raise Exception(f"Wrong '{method_name}' parameter value for "
                                      f"method '{key}'. Found: '{k}'. "
                                      f"Expected: '{parameters[key]}'")
              else:
                  raise Exception(f"Parameter '{key}' not found 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
  
      def load_table(self, table, verbose=False):
          self.__invokedMethod = 'load_table'
          self.__parameters['table'] = table
          self.__parameters['verbose'] = verbose
  
      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
  
      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
  
      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
  
      def search_async_jobs(self, jobfilter=None, verbose=False):
          self.__invokedMethod = 'search_async_jobs'
          self.__parameters['jobfilter'] = jobfilter
          self.__parameters['verbose'] = verbose
  
      def list_async_jobs(self, verbose=False):
          self.__invokedMethod = 'list_async_jobs'
          self.__parameters['verbose'] = verbose
  
      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
  
      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
  
      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
  
      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
  
      def get_images(self, coordinate):
          self.__invokedMethod = 'get_images'
          self.__parameters['coordinate'] = coordinate
  
      def get_images_async(self, coordinate):
          self.__invokedMethod = 'get_images_sync'
          self.__parameters['coordinate'] = coordinate
  
      def cone_search(self, coordinate, radius, output_file=None,
                      output_format="votable", verbose=False,
                      dump_to_file=False, columns=[]):
          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
          self.__parameters['columns'] = columns
  
      def cone_search_async(self, coordinate, radius, background=False,
                            output_file=None, output_format="votable",
                            verbose=False, dump_to_file=False, columns=[]):
          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
          self.__parameters['columns'] = columns
  
      def remove_jobs(self, jobs_list, verbose=False):
          self.__invokedMethod = 'remove_jobs'
          self.__parameters['jobs_list'] = jobs_list
          self.__parameters['verbose'] = verbose
  
      def save_results(self, job, verbose=False):
          self.__invokedMethod = 'save_results'
          self.__parameters['job'] = job
          self.__parameters['verbose'] = verbose
  
      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
  
      def login_gui(self, verbose=False):
          self.__invokedMethod = 'login_gui'
          self.__parameters['verbose'] = verbose
  
      def logout(self, verbose=False):
          self.__invokedMethod = 'logout'
          self.__parameters['verbose'] = verbose
  
      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
  
      def get_datalinks(self, ids, verbose=False):
          self.__invokedMethod = 'get_datalinks'
          self.__parameters['ids'] = ids
          self.__parameters['verbose'] = verbose
  
      def upload_table(self, upload_resource=None, table_name=None,
                       table_description=None,
                       format=None, verbose=False):
          self.__invokedMethod = 'update_table'
          self.__parameters['resource'] = upload_resource
          self.__parameters['table_name'] = table_name
          self.__parameters['table_desc'] = table_description
          self.__parameters['format'] = format
          self.__parameters['verbose'] = verbose
  
      def upload_table_from_job(self, job=None, table_name=None,
                                table_description=None, verbose=False):
          self.__invokedMethod = 'upload_table_from_job'
          self.__parameters['job'] = job
          self.__parameters['table_name'] = table_name
          self.__parameters['table_description'] = table_description
          self.__parameters['verbose'] = verbose
  
      def delete_user_table(self, table_name=None, force_removal=False,
                            verbose=False):
          self.__invokedMethod = 'delete_user_table'
          self.__parameters['table_name'] = table_name
          self.__parameters['force_removal'] = force_removal
          self.__parameters['verbose'] = verbose
  
      def load_groups(self, verbose=False):
          self.__invokedMethod = 'load_groups'
          self.__parameters['verbose'] = verbose
  
      def load_group(self, group_name=None, verbose=False):
          self.__invokedMethod = 'load_group'
          self.__parameters['group_name'] = group_name
          self.__parameters['verbose'] = verbose
  
      def load_shared_items(self, verbose=False):
          self.__invokedMethod = 'load_shared_items'
          self.__parameters['verbose'] = verbose
  
      def share_table(self, group_name=None,
                      table_name=None,
                      description=None,
                      verbose=False):
          self.__invokedMethod = 'share_table'
          self.__parameters['group_name'] = group_name
          self.__parameters['table_name'] = table_name
          self.__parameters['description'] = description
          self.__parameters['verbose'] = verbose
  
      def share_table_stop(self,
                           group_name=None,
                           table_name=None,
                           verbose=False):
          self.__invokedMethod = 'share_table_stop'
          self.__parameters['table_name'] = table_name
          self.__parameters['group_name'] = group_name
          self.__parameters['verbose'] = verbose
  
      def share_group_create(self,
                             group_name=None,
                             description=None,
                             verbose=False):
          self.__invokedMethod = 'share_group_create'
          self.__parameters['group_name'] = group_name
          self.__parameters['description'] = description
          self.__parameters['verbose'] = verbose
  
      def share_group_delete(self,
                             group_name=None,
                             verbose=False):
          self.__invokedMethod = 'share_group_delete'
          self.__parameters['group_name'] = group_name
          self.__parameters['verbose'] = verbose
  
      def share_group_add_user(self,
                               group_name=None,
                               user_id=None,
                               verbose=False):
          self.__invokedMethod = 'share_group_add_user'
          self.__parameters['group_name'] = group_name
          self.__parameters['user_id'] = user_id
          self.__parameters['verbose'] = verbose
  
      def share_group_delete_user(self,
                                  group_name=None,
                                  user_id=None,
                                  verbose=False):
          self.__invokedMethod = 'share_group_delete_user'
          self.__parameters['group_name'] = group_name
          self.__parameters['user_id'] = user_id
          self.__parameters['verbose'] = verbose
  
      def is_valid_user(self, user_id=None, verbose=False):
          self.__invokedMethod = 'is_valid_user'
          self.__parameters['user_id'] = user_id
          self.__parameters['verbose'] = verbose
  
files: astroquery/gaia/tests/DummyTapHandler.py
files: # Licensed under a 3-clause BSD style license - see LICENSE.rst
  """
  =============
  Gaia TAP plus
  =============
  
  @author: Juan Carlos Segovia
  @contact: juan.carlos.segovia@sciops.esa.int
  
  European Space Astronomy Centre (ESAC)
  European Space Agency (ESA)
  
  Created on 30 jun. 2016
  
  
  """
  from urllib.parse import urlencode
  
  CONTENT_TYPE_POST_DEFAULT = "application/x-www-form-urlencoded"
  
  
  class DummyTapHandler:
  
      def __init__(self):
          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 execute_upload(self, data,
                         content_type=CONTENT_TYPE_POST_DEFAULT,
                         verbose=False):
          pass
  
      def check_method(self, method):
          if method == self.__invokedMethod:
              return
          else:
              raise Exception(f"Method '{method}' not invoked. (Invoked method is "
                              f"{self.__invokedMethod}')")
  
      def check_parameters(self, parameters, method_name):
          if parameters is None:
              return len(self.__parameters) == 0
          num_params_in_call = len(self.__parameters)
          num_params_expected = len(parameters)
          print("Num parameters found in call: {}, expected: {}".
                format(num_params_in_call, num_params_expected))
          if num_params_expected != num_params_in_call:
              raise Exception("Wrong number of parameters for method '%s'. \
              Found: %d. Expected %d",
                              (method_name,
                               len(self.__parameters),
                               len(parameters)))
          for key in parameters:
              if key in self.__parameters:
                  # check value
                  if self.__parameters[key] != parameters[key]:
                      k = self.__parameters[key]
                      raise Exception(f"Wrong '{method_name}' parameter value for "
                                      f"method '{key}'. Found: '{k}'. "
                                      f"Expected: '{parameters[key]}'")
              else:
                  raise Exception(f"Parameter '{key}' not found 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
  
      def load_table(self, table, verbose=False):
          self.__invokedMethod = 'load_table'
          self.__parameters['table'] = table
          self.__parameters['verbose'] = verbose
  
      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
  
      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
  
      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
  
      def search_async_jobs(self, jobfilter=None, verbose=False):
          self.__invokedMethod = 'search_async_jobs'
          self.__parameters['jobfilter'] = jobfilter
          self.__parameters['verbose'] = verbose
  
      def list_async_jobs(self, verbose=False):
          self.__invokedMethod = 'list_async_jobs'
          self.__parameters['verbose'] = verbose
  
      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
  
      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
  
      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
  
      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
  
      def get_images(self, coordinate):
          self.__invokedMethod = 'get_images'
          self.__parameters['coordinate'] = coordinate
  
      def get_images_async(self, coordinate):
          self.__invokedMethod = 'get_images_sync'
          self.__parameters['coordinate'] = coordinate
  
      def cone_search(self, coordinate, radius, output_file=None,
                      output_format="votable", verbose=False,
                      dump_to_file=False, columns=[]):
          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
          self.__parameters['columns'] = columns
  
      def cone_search_async(self, coordinate, radius, background=False,
                            output_file=None, output_format="votable",
                            verbose=False, dump_to_file=False, columns=[]):
          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
          self.__parameters['columns'] = columns
  
      def remove_jobs(self, jobs_list, verbose=False):
          self.__invokedMethod = 'remove_jobs'
          self.__parameters['jobs_list'] = jobs_list
          self.__parameters['verbose'] = verbose
  
      def save_results(self, job, verbose=False):
          self.__invokedMethod = 'save_results'
          self.__parameters['job'] = job
          self.__parameters['verbose'] = verbose
  
      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
  
      def login_gui(self, verbose=False):
          self.__invokedMethod = 'login_gui'
          self.__parameters['verbose'] = verbose
  
      def logout(self, verbose=False):
          self.__invokedMethod = 'logout'
          self.__parameters['verbose'] = verbose
  
      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
  
      def get_datalinks(self, ids, verbose=False):
          self.__invokedMethod = 'get_datalinks'
          self.__parameters['ids'] = ids
          self.__parameters['verbose'] = verbose
  
      def upload_table(self, upload_resource=None, table_name=None,
                       table_description=None,
                       format=None, verbose=False):
          self.__invokedMethod = 'update_table'
          self.__parameters['resource'] = upload_resource
          self.__parameters['table_name'] = table_name
          self.__parameters['table_desc'] = table_description
          self.__parameters['format'] = format
          self.__parameters['verbose'] = verbose
  
      def upload_table_from_job(self, job=None, table_name=None,
                                table_description=None, verbose=False):
          self.__invokedMethod = 'upload_table_from_job'
          self.__parameters['job'] = job
          self.__parameters['table_name'] = table_name
          self.__parameters['table_description'] = table_description
          self.__parameters['verbose'] = verbose
  
      def delete_user_table(self, table_name=None, force_removal=False,
                            verbose=False):
          self.__invokedMethod = 'delete_user_table'
          self.__parameters['table_name'] = table_name
          self.__parameters['force_removal'] = force_removal
          self.__parameters['verbose'] = verbose
  
      def load_groups(self, verbose=False):
          self.__invokedMethod = 'load_groups'
          self.__parameters['verbose'] = verbose
  
      def load_group(self, group_name=None, verbose=False):
          self.__invokedMethod = 'load_group'
          self.__parameters['group_name'] = group_name
          self.__parameters['verbose'] = verbose
  
      def load_shared_items(self, verbose=False):
          self.__invokedMethod = 'load_shared_items'
          self.__parameters['verbose'] = verbose
  
      def share_table(self, group_name=None,
                      table_name=None,
                      description=None,
                      verbose=False):
          self.__invokedMethod = 'share_table'
          self.__parameters['group_name'] = group_name
          self.__parameters['table_name'] = table_name
          self.__parameters['description'] = description
          self.__parameters['verbose'] = verbose
  
      def share_table_stop(self,
                           group_name=None,
                           table_name=None,
                           verbose=False):
          self.__invokedMethod = 'share_table_stop'
          self.__parameters['table_name'] = table_name
          self.__parameters['group_name'] = group_name
          self.__parameters['verbose'] = verbose
  
      def share_group_create(self,
                             group_name=None,
                             description=None,
                             verbose=False):
          self.__invokedMethod = 'share_group_create'
          self.__parameters['group_name'] = group_name
          self.__parameters['description'] = description
          self.__parameters['verbose'] = verbose
  
      def share_group_delete(self,
                             group_name=None,
                             verbose=False):
          self.__invokedMethod = 'share_group_delete'
          self.__parameters['group_name'] = group_name
          self.__parameters['verbose'] = verbose
  
      def share_group_add_user(self,
                               group_name=None,
                               user_id=None,
                               verbose=False):
          self.__invokedMethod = 'share_group_add_user'
          self.__parameters['group_name'] = group_name
          self.__parameters['user_id'] = user_id
          self.__parameters['verbose'] = verbose
  
      def share_group_delete_user(self,
                                  group_name=None,
                                  user_id=None,
                                  verbose=False):
          self.__invokedMethod = 'share_group_delete_user'
          self.__parameters['group_name'] = group_name
          self.__parameters['user_id'] = user_id
          self.__parameters['verbose'] = verbose
  
      def is_valid_user(self, user_id=None, verbose=False):
          self.__invokedMethod = 'is_valid_user'
          self.__parameters['user_id'] = user_id
          self.__parameters['verbose'] = verbose
  
      def url_encode(self, data):
          return urlencode(data)
  
files: astroquery/gaia/tests/DummyTapHandler.py
files: # Licensed under a 3-clause BSD style license - see LICENSE.rst
  """
  =============
  TAP plus
  =============
  
  @author: Juan Carlos Segovia
  @contact: juan.carlos.segovia@sciops.esa.int
  
  European Space Astronomy Centre (ESAC)
  European Space Agency (ESA)
  
  Created on 30 jun. 2016
  
  
  """
  
  try:
      # python 3
      import http.client as httplib
  except ImportError:
      # python 2
      import httplib
  import mimetypes
  import time
  
  from astroquery.utils.tap.xmlparser import utils
  from astroquery.utils.tap import taputils
  from astroquery import version
  
  import requests
  
  __all__ = ['TapConn']
  
  CONTENT_TYPE_POST_DEFAULT = "application/x-www-form-urlencoded"
  
  
  class TapConn:
      """TAP plus connection class
      Provides low level HTTP connection capabilities
      """
  
      def __init__(self, ishttps,
                   host,
                   server_context=None,
                   port=80,
                   sslport=443,
                   connhandler=None,
                   tap_context=None,
                   upload_context=None,
                   table_edit_context=None,
                   data_context=None,
                   datalink_context=None):
          """Constructor
  
          Parameters
          ----------
          ishttps: bool, mandatory
              'True' is the protocol to use is HTTPS
          host : str, mandatory
              host name
          server_context : str, mandatory
              server context
          tap_context : str, optional
              tap context
          upload_context : str, optional
              upload context
          table_edit_context : str, optional
              table edit context
          data_context : str, optional
              data context
          datalink_context : str, optional
              datalink context
          port : int, optional, default 80
              HTTP port
          sslport : int, optional, default 443
              HTTPS port
          connhandler connection handler object, optional, default None
              HTTP(s) connection hander (creator). If no handler is provided, a
              new one is created.
          """
          self.__interna_init()
          self.__isHttps = ishttps
          self.__connHost = host
          self.__connPort = port
          self.__connPortSsl = sslport
          if server_context is not None:
              if server_context.startswith("/"):
                  self.__serverContext = server_context
              else:
                  self.__serverContext = f"/{server_context}"
          else:
              self.__serverContext = ""
          self.__tapContext = self.__create_context(tap_context)
          self.__dataContext = self.__create_context(data_context)
          self.__datalinkContext = self.__create_context(datalink_context)
          self.__uploadContext = self.__create_context(upload_context)
          self.__tableEditContext = self.__create_context(table_edit_context)
          if connhandler is None:
              self.__connectionHandler = ConnectionHandler(self.__connHost,
                                                           self.__connPort,
                                                           self.__connPortSsl)
          else:
              self.__connectionHandler = connhandler
  
      def __create_context(self, context):
          if context is not None and context != "":
              if str(context).startswith("/"):
                  return f"{self.__serverContext}{context}"
              else:
                  return f"{self.__serverContext}/{context}"
          else:
              return self.__serverContext
  
      def __interna_init(self):
          self.__connectionHandler = None
          self.__isHttps = False
          self.__connHost = ""
          self.__connPort = 80
          self.__connPortSsl = 443
          self.__serverContext = None
          self.__tapContext = None
          self.__postHeaders = {
              "Content-type": CONTENT_TYPE_POST_DEFAULT,
              "Accept": "text/plain",
              "User-Agent": "astroquery/{vers}".format(vers=version.version),
          }
          self.__getHeaders = {}
          self.__cookie = None
          self.__currentStatus = 0
          self.__currentReason = ""
  
      def __get_tap_context(self, subContext):
          return f"{self.__tapContext}/{subContext}"
  
      def __get_data_context(self, encodedData=None):
          if self.__dataContext is None:
              raise ValueError("data_context must be specified at TAP object "
                               + "creation for this action to be performed")
          if encodedData is not None:
              return f"{self.__dataContext}?{encodedData}"
          else:
              return self.__dataContext
  
      def __get_datalink_context(self, subContext, encodedData=None):
          if self.__datalinkContext is None:
              raise ValueError("datalink_context must be specified at TAP "
                               + "object creation for this action to be "
                               + "performed")
          if encodedData is not None:
              return f"{self.__datalinkContext}/{subContext}?{encodedData}"
  
          else:
              return f"{self.__datalinkContext}/{subContext}"
  
      def __get_upload_context(self):
          if self.__uploadContext is None:
              raise ValueError("upload_context must be specified at TAP "
                               + "object creation for this action to be "
                               + "performed")
          return self.__uploadContext
  
      def __get_table_edit_context(self):
          if self.__tableEditContext is None:
              raise ValueError("table_edit_context must be specified at TAP "
                               + "object creation for this action to be "
                               + "performed")
          return self.__tableEditContext
  
      def __get_server_context(self, subContext):
          return f"{self.__serverContext}/{subContext}"
  
      def execute_tapget(self, subcontext, verbose=False):
          """Executes a TAP GET request
          The connection is done through HTTP or HTTPS depending on the login
          status (logged in -> HTTPS)
  
          Parameters
          ----------
          subcontext : str, mandatory
              context to be added to host+serverContext+tapContext, usually the
              TAP list name
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          Returns
          -------
          An HTTP(s) response object
          """
          if subcontext.startswith("http"):
              # absolute url
              return self.__execute_get(subcontext, verbose)
          else:
              context = self.__get_tap_context(subcontext)
              return self.__execute_get(context, verbose)
  
      def execute_dataget(self, query, verbose=False):
          """Executes a data GET request
          The connection is done through HTTP or HTTPS depending on the login
          status (logged in -> HTTPS)
  
          Parameters
          ----------
          query : str, mandatory
              URL encoded data (query string)
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          Returns
          -------
          An HTTP(s) response object
          """
          context = self.__get_data_context(query)
          return self.__execute_get(context, verbose)
  
      def execute_datalinkget(self, subcontext, query, verbose=False):
          """Executes a datalink GET request
          The connection is done through HTTP or HTTPS depending on the login
          status (logged in -> HTTPS)
  
          Parameters
          ----------
          subcontext : str, mandatory
              datalink subcontext
          query : str, mandatory
              URL encoded data (query string)
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          Returns
          -------
          An HTTP(s) response object
          """
          context = self.__get_datalink_context(subcontext, query)
          return self.__execute_get(context, verbose)
  
      def __execute_get(self, context, verbose=False):
          conn = self.__get_connection(verbose)
          if verbose:
              print(f"host = {conn.host}:{conn.port}")
              print(f"context = {context}")
          conn.request("GET", context, None, self.__getHeaders)
          response = conn.getresponse()
          self.__currentReason = response.reason
          self.__currentStatus = response.status
          return response
  
      def execute_tappost(self, subcontext, data,
                          content_type=CONTENT_TYPE_POST_DEFAULT,
                          verbose=False):
          """Executes a POST request
          The connection is done through HTTP or HTTPS depending on the login
          status (logged in -> HTTPS)
  
          Parameters
          ----------
          subcontext : str, mandatory
              context to be added to host+serverContext+tapContext, usually the
              TAP list name
          data : str, mandatory
              POST data
          content_type: str, optional, default: application/x-www-form-urlencoded
              HTTP(s) content-type header value
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          Returns
          -------
          An HTTP(s) response object
          """
          context = self.__get_tap_context(subcontext)
          return self.__execute_post(context, data, content_type, verbose)
  
      def execute_datapost(self, data,
                           content_type=CONTENT_TYPE_POST_DEFAULT,
                           verbose=False):
          """Executes a POST request
          The connection is done through HTTP or HTTPS depending on the login
          status (logged in -> HTTPS)
  
          Parameters
          ----------
          data : str, mandatory
              POST data
          content_type: str, optional, default: application/x-www-form-urlencoded
              HTTP(s) content-type header value
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          Returns
          -------
          An HTTP(s) response object
          """
          context = self.__get_data_context()
          return self.__execute_post(context, data, content_type, verbose)
  
      def execute_datalinkpost(self, subcontext, data,
                               content_type=CONTENT_TYPE_POST_DEFAULT,
                               verbose=False):
          """Executes a POST request
          The connection is done through HTTP or HTTPS depending on the login
          status (logged in -> HTTPS)
  
          Parameters
          ----------
          subcontext : str, mandatory
              datalink subcontext (e.g. 'capabilities', 'availability',
              'links', etc.)
          data : str, mandatory
              POST data
          content_type: str, optional, default: application/x-www-form-urlencoded
              HTTP(s) content-type header value
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          Returns
          -------
          An HTTP(s) response object
          """
          context = self.__get_datalink_context(subcontext)
          return self.__execute_post(context, data, content_type, verbose)
  
      def execute_upload(self, data,
                         content_type=CONTENT_TYPE_POST_DEFAULT,
                         verbose=False):
          """Executes a POST upload request
          The connection is done through HTTP or HTTPS depending on the login
          status (logged in -> HTTPS)
  
          Parameters
          ----------
          data : str, mandatory
              POST data
          content_type: str, optional, default: application/x-www-form-urlencoded
              HTTP(s) content-type header value
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          Returns
          -------
          An HTTP(s) response object
          """
          context = self.__get_upload_context()
          return self.__execute_post(context, data, content_type, verbose)
  
      def execute_share(self, data, verbose=False):
          """Executes a POST upload request
          The connection is done through HTTP or HTTPS depending on the login
          status (logged in -> HTTPS)
  
          Parameters
          ----------
          data : str, mandatory
              POST data
          content_type: str, optional, default: application/x-www-form-urlencoded
              HTTP(s) content-type header value
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          Returns
          -------
          An HTTP(s) response object
          """
          context = self.__get_tap_context("share")
          return self.__execute_post(context,
                                     data,
                                     content_type=CONTENT_TYPE_POST_DEFAULT,
                                     verbose=verbose)
  
      def execute_table_edit(self, data,
                             content_type=CONTENT_TYPE_POST_DEFAULT,
                             verbose=False):
          """Executes a POST upload request
          The connection is done through HTTP or HTTPS depending on the login
          status (logged in -> HTTPS)
  
          Parameters
          ----------
          data : str, mandatory
              POST data
          content_type: str, optional, default: application/x-www-form-urlencoded
              HTTP(s) content-type header value
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          Returns
          -------
          An HTTP(s) response object
          """
          context = self.__get_table_edit_context()
          return self.__execute_post(context, data, content_type, verbose)
  
      def execute_table_tool(self, data,
                             content_type=CONTENT_TYPE_POST_DEFAULT,
                             verbose=False):
          """Executes a POST upload request
          The connection is done through HTTP or HTTPS depending on the login
          status (logged in -> HTTPS)
  
          Parameters
          ----------
          data : str, mandatory
              POST data
          content_type: str, optional, default: application/x-www-form-urlencoded
              HTTP(s) content-type header value
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          Returns
          -------
          An HTTP(s) response object
          """
          context = self.__get_table_edit_context()
          return self.__execute_post(context, data, content_type, verbose)
  
      def __execute_post(self, context, data,
                         content_type=CONTENT_TYPE_POST_DEFAULT,
                         verbose=False):
          conn = self.__get_connection(verbose)
          if verbose:
              print(f"host = {conn.host}:{conn.port}")
              print(f"context = {context}")
              print(f"Content-type = {content_type}")
          self.__postHeaders["Content-type"] = content_type
          conn.request("POST", context, data, self.__postHeaders)
          response = conn.getresponse()
          self.__currentReason = response.reason
          self.__currentStatus = response.status
          return response
  
      def execute_secure(self, subcontext, data, verbose=False):
          """Executes a secure POST request
          The connection is done through HTTPS
  
          Parameters
          ----------
          subcontext : str, mandatory
              context to be added to host+serverContext+tapContext
          data : str, mandatory
              POST data
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          Returns
          -------
          An HTTPS response object
          """
          conn = self.__get_connection_secure(verbose)
          context = self.__get_server_context(subcontext)
          self.__postHeaders["Content-type"] = CONTENT_TYPE_POST_DEFAULT
          conn.request("POST", context, data, self.__postHeaders)
          response = conn.getresponse()
          self.__currentReason = response.reason
          self.__currentStatus = response.status
          return response
  
      def get_response_status(self):
          """Returns the latest connection status
  
          Returns
          -------
          The current (latest) HTTP(s) response status
          """
          return self.__currentStatus
  
      def get_response_reason(self):
          """Returns the latest connection reason (message)
  
          Returns
          -------
          The current (latest) HTTP(s) response reason
          """
          return self.__currentReason
  
      def find_header(self, headers, key):
          """Searches for the specified keyword
  
          Parameters
          ----------
          headers : HTTP(s) headers object, mandatory
              HTTP(s) response headers
          key : str, mandatory
              header key to be searched for
  
          Returns
          -------
          The requested header value or None if the header is not found
          """
          return taputils.taputil_find_header(headers, key)
  
      def dump_to_file(self, output, response):
          """Writes the connection response into the specified output
  
          Parameters
          ----------
          output : file, mandatory
              output file
          response : HTTP(s) response object, mandatory
              HTTP(s) response object
          """
          with open(output, "wb") as f:
              while True:
                  data = response.read(4096)
                  if len(data) < 1:
                      break
                  f.write(data)
              f.close()
  
      def get_suitable_extension_by_format(self, output_format):
          """Returns the suitable extension for a file based on the output format
  
          Parameters
          ----------
          output_format : output format, mandatory
  
          Returns
          -------
          The suitable file extension based on the output format
          """
          if output_format is None:
              return ".vot"
          ext = ""
          outputFormat = output_format.lower()
          if "vot" in outputFormat:
              ext += ".vot"
          elif "xml" in outputFormat:
              ext += ".xml"
          elif "json" in outputFormat:
              ext += ".json"
          elif "plain" in outputFormat:
              ext += ".txt"
          elif "csv" in outputFormat:
              ext += ".csv"
          elif "ascii" in outputFormat:
              ext += ".ascii"
          return ext
  
      def get_suitable_extension(self, headers):
          """Returns the suitable extension for a file based on the headers
          received
  
          Parameters
          ----------
          headers : HTTP(s) response headers object, mandatory
              HTTP(s) response headers
  
          Returns
          -------
          The suitable file extension based on the HTTP(s) headers
          """
          if headers is None:
              return ""
          ext = ""
          contentType = self.find_header(headers, 'Content-Type')
          if contentType is not None:
              contentType = contentType.lower()
              if "xml" in contentType:
                  ext += ".xml"
              elif "json" in contentType:
                  ext += ".json"
              elif "plain" in contentType:
                  ext += ".txt"
              elif "csv" in contentType:
                  ext += ".csv"
              elif "ascii" in contentType:
                  ext += ".ascii"
          contentEncoding = self.find_header(headers, 'Content-Encoding')
          if contentEncoding is not None:
              if "gzip" == contentEncoding.lower():
                  ext += ".gz"
          return ext
  
      def get_file_from_header(self, headers):
          """Returns the file name returned in header Content-Disposition
          Usually, that header contains the following:
          Content-Disposition: attachment;filename="1591707060129DEV-aandres1591707060227.tar.gz"
          This method returns the value of 'filename'
  
          Parameters
          ----------
          headers: HTTP response headers list
  
          Returns
          -------
          The value of 'filename' in Content-Disposition header
          """
          content_disposition = self.find_header(headers, 'Content-Disposition')
          if content_disposition is not None:
              p = content_disposition.find('filename="')
              if p >= 0:
                  filename = content_disposition[p+10:len(content_disposition)-1]
                  content_encoding = self.find_header(headers, 'Content-Encoding')
                  if content_encoding is not None:
                      if "gzip" == content_encoding.lower():
                          filename += ".gz"
                      elif "zip" == content_encoding.lower():
                          filename += ".zip"
                  return filename
          return None
  
      def set_cookie(self, cookie):
          """Sets the login cookie
          When a cookie is set, GET and POST requests are done using HTTPS
  
          Parameters
          ----------
          cookie : str, mandatory
              login cookie
          """
          self.__cookie = cookie
          self.__postHeaders['Cookie'] = cookie
          self.__getHeaders['Cookie'] = cookie
  
      def unset_cookie(self):
          """Removes the login cookie
          When a cookie is not set, GET and POST requests are done using HTTP
          """
          self.__cookie = None
          self.__postHeaders.pop('Cookie')
          self.__getHeaders.pop('Cookie')
  
      def get_host_url(self):
          """Returns the host+port+serverContext
  
          Returns
          -------
          A string composed of: 'host:port/server_context'
          """
          return f'{self.__connHost}:{self.__connPort}{self.__get_tap_context("")}'
  
      def get_host_url_secure(self):
          """Returns the host+portSsl+serverContext
  
          Returns
          -------
          A string composed of: 'host:portSsl/server_context'
          """
          return f'{self.__connHost}:{self.__connPortSsl}{self.__get_tap_context("")}'
  
      def check_launch_response_status(self, response, debug,
                                       expected_response_status,
                                       raise_exception=True):
          """Checks the response status code
          Returns True if the response status code is the
          expected_response_status argument
  
          Parameters
          ----------
          response : HTTP(s) response object, mandatory
              HTTP(s) response
          debug : bool, mandatory
              flag to display information about the process
          expected_response_status : int, mandatory
              expected response status code
          raise_exception : boolean, optional, default True
              if 'True' and the response status is not the
              expected one, an exception is raised.
  
          Returns
          -------
          'True' if the HTTP(s) response status is the provided
          'expected_response_status' argument
          """
          isError = False
          if response.status != expected_response_status:
              if debug:
                  print(f"ERROR: {response.status}: {response.reason}")
              isError = True
          if isError and raise_exception:
              errMsg = taputils.get_http_response_error(response)
              print(response.status, errMsg)
              raise requests.exceptions.HTTPError(errMsg)
          else:
              return isError
  
      def __get_connection(self, verbose=False):
          return self.__connectionHandler.get_connection(self.__isHttps,
                                                         self.__cookie,
                                                         verbose)
  
      def __get_connection_secure(self, verbose=False):
          return self.__connectionHandler.get_connection_secure(verbose)
  
      def encode_multipart(self, fields, files):
          """Encodes a multipart form request
  
          Parameters
          ----------
          fields : dictionary, mandatory
              dictionary with keywords and values
          files : array with key, filename and value, mandatory
              array with key, filename, value
  
          Returns
          -------
          The suitable content-type and the body for the request
          """
          timeMillis = int(round(time.time() * 1000))
          boundary = f'==={timeMillis}==='
          CRLF = '\r\n'
          multiparItems = []
          for key in fields:
              multiparItems.append(f'--{boundary}{CRLF}')
              multiparItems.append(
                  f'Content-Disposition: form-data; name="{key}"{CRLF}')
              multiparItems.append(CRLF)
              multiparItems.append(f'{fields[key]}{CRLF}')
          for (key, filename, value) in files:
              multiparItems.append(f'--{boundary}{CRLF}')
              multiparItems.append(
                  f'Content-Disposition: form-data; name="{key}"; filename="{filename}"{CRLF}')
              multiparItems.append(
                  f'Content-Type: {mimetypes.guess_extension(filename)}{CRLF}')
              multiparItems.append(CRLF)
              multiparItems.append(value)
              multiparItems.append(CRLF)
          multiparItems.append(f'--{boundary}--{CRLF}')
          multiparItems.append(CRLF)
          body = utils.util_create_string_from_buffer(multiparItems)
          contentType = f'multipart/form-data; boundary={boundary}'
          return contentType, body.encode('utf-8')
  
      def __str__(self):
          return f"\tHost: {self.__connHost}\n\tUse HTTPS: {self.__isHttps}" \
              f"\n\tPort: {self.__connPort}\n\tSSL Port: {self.__connPortSsl}"
  
  
  class ConnectionHandler:
      def __init__(self, host, port, sslport):
          self.__connHost = host
          self.__connPort = port
          self.__connPortSsl = sslport
  
      def get_connection(self, ishttps=False, cookie=None, verbose=False):
          if (ishttps) or (cookie is not None):
              if verbose:
                  print("------>https")
              return self.get_connection_secure(verbose)
          else:
              if verbose:
                  print("------>http")
              return httplib.HTTPConnection(self.__connHost, self.__connPort)
  
      def get_connection_secure(self, verbose):
          return httplib.HTTPSConnection(self.__connHost, self.__connPortSsl)
  
files: astroquery/utils/tap/conn/tapconn.py
files: # Licensed under a 3-clause BSD style license - see LICENSE.rst
  """
  =============
  TAP plus
  =============
  
  @author: Juan Carlos Segovia
  @contact: juan.carlos.segovia@sciops.esa.int
  
  European Space Astronomy Centre (ESAC)
  European Space Agency (ESA)
  
  Created on 30 jun. 2016
  
  
  """
  
  try:
      # python 3
      import http.client as httplib
  except ImportError:
      # python 2
      import httplib
  import mimetypes
  import time
  
  from urllib.parse import urlencode
  
  from astroquery.utils.tap.xmlparser import utils
  from astroquery.utils.tap import taputils
  from astroquery import version
  
  import requests
  
  __all__ = ['TapConn']
  
  CONTENT_TYPE_POST_DEFAULT = "application/x-www-form-urlencoded"
  
  
  class TapConn:
      """TAP plus connection class
      Provides low level HTTP connection capabilities
      """
  
      def __init__(self, ishttps,
                   host,
                   server_context=None,
                   port=80,
                   sslport=443,
                   connhandler=None,
                   tap_context=None,
                   upload_context=None,
                   table_edit_context=None,
                   data_context=None,
                   datalink_context=None):
          """Constructor
  
          Parameters
          ----------
          ishttps: bool, mandatory
              'True' is the protocol to use is HTTPS
          host : str, mandatory
              host name
          server_context : str, mandatory
              server context
          tap_context : str, optional
              tap context
          upload_context : str, optional
              upload context
          table_edit_context : str, optional
              table edit context
          data_context : str, optional
              data context
          datalink_context : str, optional
              datalink context
          port : int, optional, default 80
              HTTP port
          sslport : int, optional, default 443
              HTTPS port
          connhandler connection handler object, optional, default None
              HTTP(s) connection hander (creator). If no handler is provided, a
              new one is created.
          """
          self.__interna_init()
          self.__isHttps = ishttps
          self.__connHost = host
          self.__connPort = port
          self.__connPortSsl = sslport
          if server_context is not None:
              if server_context.startswith("/"):
                  self.__serverContext = server_context
              else:
                  self.__serverContext = f"/{server_context}"
          else:
              self.__serverContext = ""
          self.__tapContext = self.__create_context(tap_context)
          self.__dataContext = self.__create_context(data_context)
          self.__datalinkContext = self.__create_context(datalink_context)
          self.__uploadContext = self.__create_context(upload_context)
          self.__tableEditContext = self.__create_context(table_edit_context)
          if connhandler is None:
              self.__connectionHandler = ConnectionHandler(self.__connHost,
                                                           self.__connPort,
                                                           self.__connPortSsl)
          else:
              self.__connectionHandler = connhandler
  
      def __create_context(self, context):
          if context is not None and context != "":
              if str(context).startswith("/"):
                  return f"{self.__serverContext}{context}"
              else:
                  return f"{self.__serverContext}/{context}"
          else:
              return self.__serverContext
  
      def __interna_init(self):
          self.__connectionHandler = None
          self.__isHttps = False
          self.__connHost = ""
          self.__connPort = 80
          self.__connPortSsl = 443
          self.__serverContext = None
          self.__tapContext = None
          self.__postHeaders = {
              "Content-type": CONTENT_TYPE_POST_DEFAULT,
              "Accept": "text/plain",
              "User-Agent": "astroquery/{vers}".format(vers=version.version),
          }
          self.__getHeaders = {}
          self.__cookie = None
          self.__currentStatus = 0
          self.__currentReason = ""
  
      def __get_tap_context(self, subContext):
          return f"{self.__tapContext}/{subContext}"
  
      def __get_data_context(self, encodedData=None):
          if self.__dataContext is None:
              raise ValueError("data_context must be specified at TAP object "
                               + "creation for this action to be performed")
          if encodedData is not None:
              return f"{self.__dataContext}?{encodedData}"
          else:
              return self.__dataContext
  
      def __get_datalink_context(self, subContext, encodedData=None):
          if self.__datalinkContext is None:
              raise ValueError("datalink_context must be specified at TAP "
                               + "object creation for this action to be "
                               + "performed")
          if encodedData is not None:
              return f"{self.__datalinkContext}/{subContext}?{encodedData}"
  
          else:
              return f"{self.__datalinkContext}/{subContext}"
  
      def __get_upload_context(self):
          if self.__uploadContext is None:
              raise ValueError("upload_context must be specified at TAP "
                               + "object creation for this action to be "
                               + "performed")
          return self.__uploadContext
  
      def __get_table_edit_context(self):
          if self.__tableEditContext is None:
              raise ValueError("table_edit_context must be specified at TAP "
                               + "object creation for this action to be "
                               + "performed")
          return self.__tableEditContext
  
      def __get_server_context(self, subContext):
          return f"{self.__serverContext}/{subContext}"
  
      def execute_tapget(self, subcontext, verbose=False):
          """Executes a TAP GET request
          The connection is done through HTTP or HTTPS depending on the login
          status (logged in -> HTTPS)
  
          Parameters
          ----------
          subcontext : str, mandatory
              context to be added to host+serverContext+tapContext, usually the
              TAP list name
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          Returns
          -------
          An HTTP(s) response object
          """
          if subcontext.startswith("http"):
              # absolute url
              return self.__execute_get(subcontext, verbose)
          else:
              context = self.__get_tap_context(subcontext)
              return self.__execute_get(context, verbose)
  
      def execute_dataget(self, query, verbose=False):
          """Executes a data GET request
          The connection is done through HTTP or HTTPS depending on the login
          status (logged in -> HTTPS)
  
          Parameters
          ----------
          query : str, mandatory
              URL encoded data (query string)
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          Returns
          -------
          An HTTP(s) response object
          """
          context = self.__get_data_context(query)
          return self.__execute_get(context, verbose)
  
      def execute_datalinkget(self, subcontext, query, verbose=False):
          """Executes a datalink GET request
          The connection is done through HTTP or HTTPS depending on the login
          status (logged in -> HTTPS)
  
          Parameters
          ----------
          subcontext : str, mandatory
              datalink subcontext
          query : str, mandatory
              URL encoded data (query string)
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          Returns
          -------
          An HTTP(s) response object
          """
          context = self.__get_datalink_context(subcontext, query)
          return self.__execute_get(context, verbose)
  
      def __execute_get(self, context, verbose=False):
          conn = self.__get_connection(verbose)
          if verbose:
              print(f"host = {conn.host}:{conn.port}")
              print(f"context = {context}")
          conn.request("GET", context, None, self.__getHeaders)
          response = conn.getresponse()
          self.__currentReason = response.reason
          self.__currentStatus = response.status
          return response
  
      def execute_tappost(self, subcontext, data,
                          content_type=CONTENT_TYPE_POST_DEFAULT,
                          verbose=False):
          """Executes a POST request
          The connection is done through HTTP or HTTPS depending on the login
          status (logged in -> HTTPS)
  
          Parameters
          ----------
          subcontext : str, mandatory
              context to be added to host+serverContext+tapContext, usually the
              TAP list name
          data : str, mandatory
              POST data
          content_type: str, optional, default: application/x-www-form-urlencoded
              HTTP(s) content-type header value
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          Returns
          -------
          An HTTP(s) response object
          """
          context = self.__get_tap_context(subcontext)
          return self.__execute_post(context, data, content_type, verbose)
  
      def execute_datapost(self, data,
                           content_type=CONTENT_TYPE_POST_DEFAULT,
                           verbose=False):
          """Executes a POST request
          The connection is done through HTTP or HTTPS depending on the login
          status (logged in -> HTTPS)
  
          Parameters
          ----------
          data : str, mandatory
              POST data
          content_type: str, optional, default: application/x-www-form-urlencoded
              HTTP(s) content-type header value
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          Returns
          -------
          An HTTP(s) response object
          """
          context = self.__get_data_context()
          return self.__execute_post(context, data, content_type, verbose)
  
      def execute_datalinkpost(self, subcontext, data,
                               content_type=CONTENT_TYPE_POST_DEFAULT,
                               verbose=False):
          """Executes a POST request
          The connection is done through HTTP or HTTPS depending on the login
          status (logged in -> HTTPS)
  
          Parameters
          ----------
          subcontext : str, mandatory
              datalink subcontext (e.g. 'capabilities', 'availability',
              'links', etc.)
          data : str, mandatory
              POST data
          content_type: str, optional, default: application/x-www-form-urlencoded
              HTTP(s) content-type header value
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          Returns
          -------
          An HTTP(s) response object
          """
          context = self.__get_datalink_context(subcontext)
          return self.__execute_post(context, data, content_type, verbose)
  
      def execute_upload(self, data,
                         content_type=CONTENT_TYPE_POST_DEFAULT,
                         verbose=False):
          """Executes a POST upload request
          The connection is done through HTTP or HTTPS depending on the login
          status (logged in -> HTTPS)
  
          Parameters
          ----------
          data : str, mandatory
              POST data
          content_type: str, optional, default: application/x-www-form-urlencoded
              HTTP(s) content-type header value
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          Returns
          -------
          An HTTP(s) response object
          """
          context = self.__get_upload_context()
          return self.__execute_post(context, data, content_type, verbose)
  
      def execute_share(self, data, verbose=False):
          """Executes a POST upload request
          The connection is done through HTTP or HTTPS depending on the login
          status (logged in -> HTTPS)
  
          Parameters
          ----------
          data : str, mandatory
              POST data
          content_type: str, optional, default: application/x-www-form-urlencoded
              HTTP(s) content-type header value
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          Returns
          -------
          An HTTP(s) response object
          """
          context = self.__get_tap_context("share")
          return self.__execute_post(context,
                                     data,
                                     content_type=CONTENT_TYPE_POST_DEFAULT,
                                     verbose=verbose)
  
      def execute_table_edit(self, data,
                             content_type=CONTENT_TYPE_POST_DEFAULT,
                             verbose=False):
          """Executes a POST upload request
          The connection is done through HTTP or HTTPS depending on the login
          status (logged in -> HTTPS)
  
          Parameters
          ----------
          data : str, mandatory
              POST data
          content_type: str, optional, default: application/x-www-form-urlencoded
              HTTP(s) content-type header value
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          Returns
          -------
          An HTTP(s) response object
          """
          context = self.__get_table_edit_context()
          return self.__execute_post(context, data, content_type, verbose)
  
      def execute_table_tool(self, data,
                             content_type=CONTENT_TYPE_POST_DEFAULT,
                             verbose=False):
          """Executes a POST upload request
          The connection is done through HTTP or HTTPS depending on the login
          status (logged in -> HTTPS)
  
          Parameters
          ----------
          data : str, mandatory
              POST data
          content_type: str, optional, default: application/x-www-form-urlencoded
              HTTP(s) content-type header value
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          Returns
          -------
          An HTTP(s) response object
          """
          context = self.__get_table_edit_context()
          return self.__execute_post(context, data, content_type, verbose)
  
      def __execute_post(self, context, data,
                         content_type=CONTENT_TYPE_POST_DEFAULT,
                         verbose=False):
          conn = self.__get_connection(verbose)
          if verbose:
              print(f"host = {conn.host}:{conn.port}")
              print(f"context = {context}")
              print(f"Content-type = {content_type}")
          self.__postHeaders["Content-type"] = content_type
          conn.request("POST", context, data, self.__postHeaders)
          response = conn.getresponse()
          self.__currentReason = response.reason
          self.__currentStatus = response.status
          return response
  
      def execute_secure(self, subcontext, data, verbose=False):
          """Executes a secure POST request
          The connection is done through HTTPS
  
          Parameters
          ----------
          subcontext : str, mandatory
              context to be added to host+serverContext+tapContext
          data : str, mandatory
              POST data
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          Returns
          -------
          An HTTPS response object
          """
          conn = self.__get_connection_secure(verbose)
          context = self.__get_server_context(subcontext)
          self.__postHeaders["Content-type"] = CONTENT_TYPE_POST_DEFAULT
          conn.request("POST", context, data, self.__postHeaders)
          response = conn.getresponse()
          self.__currentReason = response.reason
          self.__currentStatus = response.status
          return response
  
      def get_response_status(self):
          """Returns the latest connection status
  
          Returns
          -------
          The current (latest) HTTP(s) response status
          """
          return self.__currentStatus
  
      def get_response_reason(self):
          """Returns the latest connection reason (message)
  
          Returns
          -------
          The current (latest) HTTP(s) response reason
          """
          return self.__currentReason
  
      def url_encode(self, data):
          """Encodes the provided dictionary
  
          Parameters
          ----------
          data : dictionary, mandatory
              dictionary to be encoded
          """
          return urlencode(data)
  
      def find_header(self, headers, key):
          """Searches for the specified keyword
  
          Parameters
          ----------
          headers : HTTP(s) headers object, mandatory
              HTTP(s) response headers
          key : str, mandatory
              header key to be searched for
  
          Returns
          -------
          The requested header value or None if the header is not found
          """
          return taputils.taputil_find_header(headers, key)
  
      def dump_to_file(self, output, response):
          """Writes the connection response into the specified output
  
          Parameters
          ----------
          output : file, mandatory
              output file
          response : HTTP(s) response object, mandatory
              HTTP(s) response object
          """
          with open(output, "wb") as f:
              while True:
                  data = response.read(4096)
                  if len(data) < 1:
                      break
                  f.write(data)
              f.close()
  
      def get_suitable_extension_by_format(self, output_format):
          """Returns the suitable extension for a file based on the output format
  
          Parameters
          ----------
          output_format : output format, mandatory
  
          Returns
          -------
          The suitable file extension based on the output format
          """
          if output_format is None:
              return ".vot"
          ext = ""
          outputFormat = output_format.lower()
          if "vot" in outputFormat:
              ext += ".vot"
          elif "xml" in outputFormat:
              ext += ".xml"
          elif "json" in outputFormat:
              ext += ".json"
          elif "plain" in outputFormat:
              ext += ".txt"
          elif "csv" in outputFormat:
              ext += ".csv"
          elif "ascii" in outputFormat:
              ext += ".ascii"
          return ext
  
      def get_suitable_extension(self, headers):
          """Returns the suitable extension for a file based on the headers
          received
  
          Parameters
          ----------
          headers : HTTP(s) response headers object, mandatory
              HTTP(s) response headers
  
          Returns
          -------
          The suitable file extension based on the HTTP(s) headers
          """
          if headers is None:
              return ""
          ext = ""
          contentType = self.find_header(headers, 'Content-Type')
          if contentType is not None:
              contentType = contentType.lower()
              if "xml" in contentType:
                  ext += ".xml"
              elif "json" in contentType:
                  ext += ".json"
              elif "plain" in contentType:
                  ext += ".txt"
              elif "csv" in contentType:
                  ext += ".csv"
              elif "ascii" in contentType:
                  ext += ".ascii"
          contentEncoding = self.find_header(headers, 'Content-Encoding')
          if contentEncoding is not None:
              if "gzip" == contentEncoding.lower():
                  ext += ".gz"
          return ext
  
      def get_file_from_header(self, headers):
          """Returns the file name returned in header Content-Disposition
          Usually, that header contains the following:
          Content-Disposition: attachment;filename="1591707060129DEV-aandres1591707060227.tar.gz"
          This method returns the value of 'filename'
  
          Parameters
          ----------
          headers: HTTP response headers list
  
          Returns
          -------
          The value of 'filename' in Content-Disposition header
          """
          content_disposition = self.find_header(headers, 'Content-Disposition')
          if content_disposition is not None:
              p = content_disposition.find('filename="')
              if p >= 0:
                  filename = content_disposition[p+10:len(content_disposition)-1]
                  content_encoding = self.find_header(headers, 'Content-Encoding')
                  if content_encoding is not None:
                      if "gzip" == content_encoding.lower():
                          filename += ".gz"
                      elif "zip" == content_encoding.lower():
                          filename += ".zip"
                  return filename
          return None
  
      def set_cookie(self, cookie):
          """Sets the login cookie
          When a cookie is set, GET and POST requests are done using HTTPS
  
          Parameters
          ----------
          cookie : str, mandatory
              login cookie
          """
          self.__cookie = cookie
          self.__postHeaders['Cookie'] = cookie
          self.__getHeaders['Cookie'] = cookie
  
      def unset_cookie(self):
          """Removes the login cookie
          When a cookie is not set, GET and POST requests are done using HTTP
          """
          self.__cookie = None
          self.__postHeaders.pop('Cookie')
          self.__getHeaders.pop('Cookie')
  
      def get_host_url(self):
          """Returns the host+port+serverContext
  
          Returns
          -------
          A string composed of: 'host:port/server_context'
          """
          return f'{self.__connHost}:{self.__connPort}{self.__get_tap_context("")}'
  
      def get_host_url_secure(self):
          """Returns the host+portSsl+serverContext
  
          Returns
          -------
          A string composed of: 'host:portSsl/server_context'
          """
          return f'{self.__connHost}:{self.__connPortSsl}{self.__get_tap_context("")}'
  
      def check_launch_response_status(self, response, debug,
                                       expected_response_status,
                                       raise_exception=True):
          """Checks the response status code
          Returns True if the response status code is the
          expected_response_status argument
  
          Parameters
          ----------
          response : HTTP(s) response object, mandatory
              HTTP(s) response
          debug : bool, mandatory
              flag to display information about the process
          expected_response_status : int, mandatory
              expected response status code
          raise_exception : boolean, optional, default True
              if 'True' and the response status is not the
              expected one, an exception is raised.
  
          Returns
          -------
          'True' if the HTTP(s) response status is the provided
          'expected_response_status' argument
          """
          isError = False
          if response.status != expected_response_status:
              if debug:
                  print(f"ERROR: {response.status}: {response.reason}")
              isError = True
          if isError and raise_exception:
              errMsg = taputils.get_http_response_error(response)
              print(response.status, errMsg)
              raise requests.exceptions.HTTPError(errMsg)
          else:
              return isError
  
      def __get_connection(self, verbose=False):
          return self.__connectionHandler.get_connection(self.__isHttps,
                                                         self.__cookie,
                                                         verbose)
  
      def __get_connection_secure(self, verbose=False):
          return self.__connectionHandler.get_connection_secure(verbose)
  
      def encode_multipart(self, fields, files):
          """Encodes a multipart form request
  
          Parameters
          ----------
          fields : dictionary, mandatory
              dictionary with keywords and values
          files : array with key, filename and value, mandatory
              array with key, filename, value
  
          Returns
          -------
          The suitable content-type and the body for the request
          """
          timeMillis = int(round(time.time() * 1000))
          boundary = f'==={timeMillis}==='
          CRLF = '\r\n'
          multiparItems = []
          for key in fields:
              multiparItems.append(f'--{boundary}{CRLF}')
              multiparItems.append(
                  f'Content-Disposition: form-data; name="{key}"{CRLF}')
              multiparItems.append(CRLF)
              multiparItems.append(f'{fields[key]}{CRLF}')
          for (key, filename, value) in files:
              multiparItems.append(f'--{boundary}{CRLF}')
              multiparItems.append(
                  f'Content-Disposition: form-data; name="{key}"; filename="{filename}"{CRLF}')
              multiparItems.append(
                  f'Content-Type: {mimetypes.guess_extension(filename)}{CRLF}')
              multiparItems.append(CRLF)
              multiparItems.append(value)
              multiparItems.append(CRLF)
          multiparItems.append(f'--{boundary}--{CRLF}')
          multiparItems.append(CRLF)
          body = utils.util_create_string_from_buffer(multiparItems)
          contentType = f'multipart/form-data; boundary={boundary}'
          return contentType, body.encode('utf-8')
  
      def __str__(self):
          return f"\tHost: {self.__connHost}\n\tUse HTTPS: {self.__isHttps}" \
              f"\n\tPort: {self.__connPort}\n\tSSL Port: {self.__connPortSsl}"
  
  
  class ConnectionHandler:
      def __init__(self, host, port, sslport):
          self.__connHost = host
          self.__connPort = port
          self.__connPortSsl = sslport
  
      def get_connection(self, ishttps=False, cookie=None, verbose=False):
          if (ishttps) or (cookie is not None):
              if verbose:
                  print("------>https")
              return self.get_connection_secure(verbose)
          else:
              if verbose:
                  print("------>http")
              return httplib.HTTPConnection(self.__connHost, self.__connPort)
  
      def get_connection_secure(self, verbose):
          return httplib.HTTPSConnection(self.__connHost, self.__connPortSsl)
  
files: astroquery/utils/tap/conn/tapconn.py
files: # Licensed under a 3-clause BSD style license - see LICENSE.rst
  """
  =============
  TAP plus
  =============
  
  @author: Juan Carlos Segovia
  @contact: juan.carlos.segovia@sciops.esa.int
  
  European Space Astronomy Centre (ESAC)
  European Space Agency (ESA)
  
  Created on 30 jun. 2016
  
  
  """
  from astroquery.utils.tap import taputils
  
  import requests
  
  
  class DummyConnHandler:
  
      def __init__(self):
          self.request = None
          self.data = None
          self.fileExt = ".ext"
          self.defaultResponse = None
          self.responses = {}
          self.errorFileOutput = None
          self.errorReceivedResponse = None
          self.contentType = None
          self.verbose = None
          self.query = None
          self.fileOutput = None
  
      def set_default_response(self, defaultResponse):
          self.defaultResponse = defaultResponse
  
      def get_default_response(self):
          return self.defaultResponse
  
      def get_last_request(self):
          return self.request
  
      def get_last_data(self):
          return self.data
  
      def get_last_query(self):
          return self.query
  
      def get_error_file_output(self):
          return self.errorFileOutput
  
      def get_error_received_response(self):
          return self.errorReceivedResponse
  
      def set_response(self, request, response):
          self.responses[str(request)] = response
  
      def execute_tapget(self, request=None, verbose=False):
          return self.__execute_get(request, verbose)
  
      def execute_dataget(self, query, verbose=False):
          return self.__execute_get(query)
  
      def execute_datalinkget(self, subcontext, query, verbose=False):
          self.query = query
          return self.__execute_get(subcontext, verbose)
  
      def __execute_get(self, request, verbose):
          self.request = request
          self.verbose = verbose
          return self.__get_response(request)
  
      def execute_tappost(self, subcontext=None, data=None,
                          content_type=None, verbose=False):
          return self.__execute_post(subcontext, data, content_type, verbose)
  
      def execute_datapost(self, data=None, content_type=None, verbose=False):
          return self.__execute_post("", data, content_type, verbose)
  
      def execute_datalinkpost(self, subcontext=None, data=None,
                               content_type=None, verbose=False):
          return self.__execute_post(subcontext, data, content_type, verbose)
  
      def __execute_post(self, subcontext=None, data=None,
                         content_type=None, verbose=False):
          self.data = data
          self.contentType = content_type
          self.verbose = verbose
          sortedKey = self.__create_sorted_dict_key(data)
          if subcontext.find('?') == -1:
              self.request = f"{subcontext}?{sortedKey}"
          else:
              if subcontext.endswith('?'):
                  self.request = f"{subcontext}{sortedKey}"
              else:
                  self.request = f"{subcontext}&{sortedKey}"
          return self.__get_response(self.request)
  
      def dump_to_file(self, fileOutput, response):
          self.errorFileOutput = fileOutput
          self.errorReceivedResponse = response
          print(f"DummyConnHandler - dump to file: file: '{fileOutput}', \
          response status: {response.status}, response msg: {response.reason}")
  
      def __get_response(self, responseid):
          try:
              return self.responses[str(responseid)]
          except KeyError as e:
              if self.defaultResponse is not None:
                  return self.defaultResponse
              else:
                  print(f"\nNot found response for key\n\t'{responseid}'")
                  print("Available keys: ")
                  if self.responses is None:
                      print("\tNone available")
                  else:
                      for k in self.responses.keys():
                          print(f"\t'{k}'")
                  raise (e)
  
      def __create_sorted_dict_key(self, data):
          dictTmp = {}
          items = data.split('&')
          for i in (items):
              subItems = i.split('=')
              dictTmp[subItems[0]] = subItems[1]
          # sort dict
          return taputils.taputil_create_sorted_dict_key(dictTmp)
  
      def check_launch_response_status(self, response, debug,
                                       expected_response_status,
                                       raise_exception=True):
          isError = False
          if response.status != expected_response_status:
              if debug:
                  print(f"ERROR: {response.status}: {response.reason}")
              isError = True
          if isError and raise_exception:
              errMsg = taputils.get_http_response_error(response)
              print(response.status, errMsg)
              raise requests.exceptions.HTTPError(errMsg)
          else:
              return isError
  
      def get_suitable_extension(self, headers):
          return self.fileExt
  
      def set_suitable_extension(self, ext):
          self.fileExt = ext
  
      def get_suitable_extension_by_format(self, output_format):
          return self.fileExt
  
      def get_file_from_header(self, headers):
          return self.fileOutput
  
      def find_header(self, headers, key):
          return taputils.taputil_find_header(headers, key)
  
      def execute_table_edit(self, data,
                             content_type="application/x-www-form-urlencoded",
                             verbose=False):
          return self.__execute_post(subcontext="tableEdit", data=data,
                                     content_type=content_type, verbose=verbose)
  
      def execute_table_tool(self, data,
                             content_type="application/x-www-form-urlencoded",
                             verbose=False):
          return self.__execute_post(subcontext="TableTool", data=data,
                                     content_type=content_type, verbose=verbose)
  
files: astroquery/utils/tap/conn/tests/DummyConnHandler.py
files: # Licensed under a 3-clause BSD style license - see LICENSE.rst
  """
  =============
  TAP plus
  =============
  
  @author: Juan Carlos Segovia
  @contact: juan.carlos.segovia@sciops.esa.int
  
  European Space Astronomy Centre (ESAC)
  European Space Agency (ESA)
  
  Created on 30 jun. 2016
  
  
  """
  from astroquery.utils.tap import taputils
  
  from urllib.parse import urlencode
  
  import requests
  
  
  class DummyConnHandler:
  
      def __init__(self):
          self.request = None
          self.data = None
          self.fileExt = ".ext"
          self.defaultResponse = None
          self.responses = {}
          self.errorFileOutput = None
          self.errorReceivedResponse = None
          self.contentType = None
          self.verbose = None
          self.query = None
          self.fileOutput = None
  
      def set_default_response(self, defaultResponse):
          self.defaultResponse = defaultResponse
  
      def get_default_response(self):
          return self.defaultResponse
  
      def get_last_request(self):
          return self.request
  
      def get_last_data(self):
          return self.data
  
      def get_last_query(self):
          return self.query
  
      def get_error_file_output(self):
          return self.errorFileOutput
  
      def get_error_received_response(self):
          return self.errorReceivedResponse
  
      def set_response(self, request, response):
          self.responses[str(request)] = response
  
      def execute_tapget(self, request=None, verbose=False):
          return self.__execute_get(request, verbose)
  
      def execute_dataget(self, query, verbose=False):
          return self.__execute_get(query)
  
      def execute_datalinkget(self, subcontext, query, verbose=False):
          self.query = query
          return self.__execute_get(subcontext, verbose)
  
      def __execute_get(self, request, verbose):
          self.request = request
          self.verbose = verbose
          return self.__get_response(request)
  
      def execute_tappost(self, subcontext=None, data=None,
                          content_type=None, verbose=False):
          return self.__execute_post(subcontext, data, content_type, verbose)
  
      def execute_datapost(self, data=None, content_type=None, verbose=False):
          return self.__execute_post("", data, content_type, verbose)
  
      def execute_datalinkpost(self, subcontext=None, data=None,
                               content_type=None, verbose=False):
          return self.__execute_post(subcontext, data, content_type, verbose)
  
      def __execute_post(self, subcontext=None, data=None,
                         content_type=None, verbose=False):
          self.data = data
          self.contentType = content_type
          self.verbose = verbose
          sortedKey = self.__create_sorted_dict_key(data)
          if subcontext.find('?') == -1:
              self.request = f"{subcontext}?{sortedKey}"
          else:
              if subcontext.endswith('?'):
                  self.request = f"{subcontext}{sortedKey}"
              else:
                  self.request = f"{subcontext}&{sortedKey}"
          return self.__get_response(self.request)
  
      def dump_to_file(self, fileOutput, response):
          self.errorFileOutput = fileOutput
          self.errorReceivedResponse = response
          print(f"DummyConnHandler - dump to file: file: '{fileOutput}', \
          response status: {response.status}, response msg: {response.reason}")
  
      def __get_response(self, responseid):
          try:
              return self.responses[str(responseid)]
          except KeyError as e:
              if self.defaultResponse is not None:
                  return self.defaultResponse
              else:
                  print(f"\nNot found response for key\n\t'{responseid}'")
                  print("Available keys: ")
                  if self.responses is None:
                      print("\tNone available")
                  else:
                      for k in self.responses.keys():
                          print(f"\t'{k}'")
                  raise (e)
  
      def __create_sorted_dict_key(self, data):
          dictTmp = {}
          items = data.split('&')
          for i in (items):
              subItems = i.split('=')
              dictTmp[subItems[0]] = subItems[1]
          # sort dict
          return taputils.taputil_create_sorted_dict_key(dictTmp)
  
      def check_launch_response_status(self, response, debug,
                                       expected_response_status,
                                       raise_exception=True):
          isError = False
          if response.status != expected_response_status:
              if debug:
                  print(f"ERROR: {response.status}: {response.reason}")
              isError = True
          if isError and raise_exception:
              errMsg = taputils.get_http_response_error(response)
              print(response.status, errMsg)
              raise requests.exceptions.HTTPError(errMsg)
          else:
              return isError
  
      def url_encode(self, data):
          return urlencode(data)
  
      def get_suitable_extension(self, headers):
          return self.fileExt
  
      def set_suitable_extension(self, ext):
          self.fileExt = ext
  
      def get_suitable_extension_by_format(self, output_format):
          return self.fileExt
  
      def get_file_from_header(self, headers):
          return self.fileOutput
  
      def find_header(self, headers, key):
          return taputils.taputil_find_header(headers, key)
  
      def execute_table_edit(self, data,
                             content_type="application/x-www-form-urlencoded",
                             verbose=False):
          return self.__execute_post(subcontext="tableEdit", data=data,
                                     content_type=content_type, verbose=verbose)
  
      def execute_table_tool(self, data,
                             content_type="application/x-www-form-urlencoded",
                             verbose=False):
          return self.__execute_post(subcontext="TableTool", data=data,
                                     content_type=content_type, verbose=verbose)
  
files: astroquery/utils/tap/conn/tests/DummyConnHandler.py
files: # Licensed under a 3-clause BSD style license - see LICENSE.rst
  """
  =============
  TAP plus
  =============
  
  @author: Juan Carlos Segovia
  @contact: juan.carlos.segovia@sciops.esa.int
  
  European Space Astronomy Centre (ESAC)
  European Space Agency (ESA)
  
  Created on 30 jun. 2016
  Modified on 1 jun. 2021 by mhsarmiento
  """
  from urllib.parse import urlencode
  
  from astroquery.utils.tap import taputils
  from astroquery.utils.tap.conn.tapconn import TapConn
  from astroquery.utils.tap.xmlparser.tableSaxParser import TableSaxParser
  from astroquery.utils.tap.model.job import Job
  from astroquery.utils.tap.gui.login import LoginDialog
  from astroquery.utils.tap.xmlparser.jobSaxParser import JobSaxParser
  from astroquery.utils.tap.xmlparser.jobListSaxParser import JobListSaxParser
  from astroquery.utils.tap.xmlparser.groupSaxParser import GroupSaxParser
  from astroquery.utils.tap.xmlparser.sharedItemsSaxParser import SharedItemsSaxParser  # noqa
  from astroquery.utils.tap.xmlparser import utils
  from astroquery.utils.tap.model.filter import Filter
  import requests
  from astroquery import log
  import getpass
  import os
  from astropy.table.table import Table
  import tempfile
  
  __all__ = ['Tap', 'TapPlus']
  
  VERSION = "20200428.1"
  
  
  class Tap:
      """TAP class
      Provides TAP capabilities
      """
  
      def __init__(self, url=None,
                   host=None,
                   server_context=None,
                   tap_context=None,
                   port=80, sslport=443,
                   default_protocol_is_https=False,
                   connhandler=None,
                   upload_context=None,
                   table_edit_context=None,
                   data_context=None,
                   datalink_context=None,
                   verbose=False):
          """Constructor
  
          Parameters
          ----------
          url : str, mandatory if no host is specified, default None
              TAP URL
          host : str, optional, default None
              host name
          server_context : str, mandatory, default None
              server context
          tap_context : str, mandatory, default None
              tap context
          upload_context : str, optional, default None
              upload context
          table_edit_context : str, mandatory, default None
              context for all actions to be performed over a existing table
          data_context : str, optional, default None
              data context
          datalink_context : str, optional, default None
              datalink context
          port : int, optional, default '80'
              HTTP port
          sslport : int, optional, default '443'
              HTTPS port
          default_protocol_is_https : bool, optional, default False
              Specifies whether the default protocol to be used is HTTPS
          connhandler : connection handler object, optional, default None
              HTTP(s) connection hander (creator). If no handler is provided, a
              new one is created.
          verbose : bool, optional, default 'False'
              flag to display information about the process
          """
          self.__internalInit()
          if url is not None:
              protocol, host, port, server, tap = self.__parseUrl(url)
              if server_context is None:
                  server_context = server
              if tap_context is None:
                  tap_context = tap
              if protocol == "http":
                  tap = TapConn(ishttps=False,
                                host=host,
                                server_context=server_context,
                                tap_context=tap_context,
                                upload_context=upload_context,
                                table_edit_context=table_edit_context,
                                data_context=data_context,
                                datalink_context=datalink_context,
                                port=port,
                                sslport=sslport)
                  self.__connHandler = tap
              else:
                  # https port -> sslPort
                  tap = TapConn(ishttps=True,
                                host=host,
                                server_context=server_context,
                                tap_context=tap_context,
                                upload_context=upload_context,
                                table_edit_context=table_edit_context,
                                data_context=data_context,
                                datalink_context=datalink_context,
                                port=port,
                                sslport=port)
                  self.__connHandler = tap
          else:
              tap = TapConn(ishttps=default_protocol_is_https,
                            host=host,
                            server_context=server_context,
                            tap_context=tap_context,
                            upload_context=upload_context,
                            table_edit_context=table_edit_context,
                            data_context=data_context,
                            datalink_context=datalink_context,
                            port=port,
                            sslport=sslport)
              self.__connHandler = tap
          # if connectionHandler is set, use it (useful for testing)
          if connhandler is not None:
              self.__connHandler = connhandler
          if verbose:
              print(f"Created TAP+ (v{VERSION}) - Connection:\n{self.__connHandler}")
  
      def __internalInit(self):
          self.__connHandler = None
          self.tap_client_id = f"aqtappy1-{VERSION}"
  
      def load_tables(self, verbose=False):
          """Loads all public tables
  
          Parameters
          ----------
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          Returns
          -------
          A list of table objects
          """
          return self.__load_tables(verbose=verbose)
  
      def load_table(self, table, verbose=False):
          """Loads the specified table
  
          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
          """
          if table is None:
              raise ValueError("Table name is required")
          if verbose:
              print(f"Retrieving table '{table}'")
          response = self.__connHandler.execute_tapget(f"tables?tables={table}",
                                                       verbose=verbose)
          if verbose:
              print(response.status, response.reason)
          self.__connHandler.check_launch_response_status(response,
                                                          verbose,
                                                          200)
          if verbose:
              print(f"Parsing table '{table}'...")
          tsp = TableSaxParser()
          tsp.parseData(response)
          if verbose:
              print("Done.")
          return tsp.get_table()
  
      def __load_tables(self, only_names=False, include_shared_tables=False,
                        verbose=False):
          """Loads all public tables
  
          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
          """
          # share_info=true&share_accessible=true&only_tables=true
          flags = ""
          addedItem = False
          if only_names:
              flags = "only_tables=true"
              addedItem = True
          if include_shared_tables:
              if addedItem:
                  flags += "&"
              flags += "share_accessible=true"
              addedItem = True
          log.info("Retrieving tables...")
          if flags != "":
              response = self.__connHandler.execute_tapget(f"tables?{flags}", verbose=verbose)
          else:
              response = self.__connHandler.execute_tapget("tables",
                                                           verbose=verbose)
          if verbose:
              print(response.status, response.reason)
          isError = self.__connHandler.check_launch_response_status(response,
                                                                    verbose,
                                                                    200)
          if isError:
              log.info(f"{response.status} {response.reason}")
              raise requests.exceptions.HTTPError(response.reason)
              return None
          log.info("Parsing tables...")
          tsp = TableSaxParser()
          tsp.parseData(response)
          log.info("Done.")
          return tsp.get_tables()
  
      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, maxrec=None):
          """Launches a synchronous job
  
          Parameters
          ----------
          query : str, mandatory
              query to be executed
          name : str, optional, default None
              custom name defined by the user for the job that is going to be created
          output_file : str, optional, default None
              file name where the results are saved if dumpToFile is True.
              If this parameter is not provided, the jobid is used instead
          output_format : str, optional, default 'votable'
              results format
          verbose : bool, optional, default 'False'
              flag to display information about the process
          dump_to_file : bool, optional, default 'False'
              if True, the results are saved in a file instead of using memory
          upload_resource : str, optional, default None
              resource to be uploaded to UPLOAD_SCHEMA
          upload_table_name : str, optional, default None
              resource temporary table name associated to the uploaded resource.
              This argument is required if upload_resource is provided.
          maxrec : int, optional, default None
              maximum number of rows to return (TAP ``MAXREC`` parameter)
  
          Returns
          -------
          A Job object
          """
          output_file_updated = taputils.get_suitable_output_file_name_for_current_output_format(output_file,
                                                                                                 output_format)
          query = taputils.set_top_in_query(query, 2000)
          if verbose:
              print(f"Launched query: '{query}'")
          if upload_resource is not None:
              if upload_table_name is None:
                  raise ValueError("Table name is required when a resource is uploaded")
              response = self.__launchJobMultipart(query=query,
                                                   uploadResource=upload_resource,
                                                   uploadTableName=upload_table_name,
                                                   outputFormat=output_format,
                                                   context="sync",
                                                   verbose=verbose,
                                                   name=name,
                                                   maxrec=maxrec)
          else:
              response = self.__launchJob(query=query,
                                          outputFormat=output_format,
                                          context="sync",
                                          verbose=verbose,
                                          name=name,
                                          maxrec=maxrec)
          # handle redirection
          if response.status == 303:
              # redirection
              if verbose:
                  print("Redirection found")
              location = self.__connHandler.find_header(
                  response.getheaders(),
                  "location")
              if location is None:
                  raise requests.exceptions.HTTPError("No location found "
                                                      "after redirection was "
                                                      "received (303)")
              if verbose:
                  print(f"Redirect to {location}")
              subcontext = self.__extract_sync_subcontext(location)
              response = self.__connHandler.execute_tapget(subcontext,
                                                           verbose=verbose)
          job = Job(async_job=False, query=query, connhandler=self.__connHandler)
          isError = self.__connHandler.check_launch_response_status(response,
                                                                    verbose,
                                                                    200,
                                                                    False)
          headers = response.getheaders()
          suitableOutputFile = taputils.get_suitable_output_file(self.__connHandler,
                                                                 False,
                                                                 output_file_updated,
                                                                 headers,
                                                                 isError,
                                                                 output_format)
          job.outputFile = suitableOutputFile
          job.outputFileUser = output_file
          job.parameters['format'] = output_format
          job.set_response_status(response.status, response.reason)
          job.set_phase('PENDING')
          if isError:
              job.failed = True
              job.set_phase('ERROR')
              responseBytes = response.read()
              responseStr = responseBytes.decode('utf-8')
              if dump_to_file:
                  if verbose:
                      print(f"Saving error to: {suitableOutputFile}")
                  self.__connHandler.dump_to_file(suitableOutputFile,
                                                  responseStr)
              raise requests.exceptions.HTTPError(
                  taputils.parse_http_response_error(responseStr,
                                                     response.status))
          else:
              if verbose:
                  print("Retrieving sync. results...")
              if dump_to_file:
                  if verbose:
                      print(f"Saving results to: {suitableOutputFile}")
                  self.__connHandler.dump_to_file(suitableOutputFile, response)
              else:
                  results = utils.read_http_response(response, output_format)
                  job.set_results(results)
              if verbose:
                  print("Query finished.")
              job._phase = 'COMPLETED'
          return 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,
                           autorun=True, maxrec=None):
          """Launches an asynchronous job
  
          Parameters
          ----------
          query : str, mandatory
              query to be executed
          name : str, optional, default None
              custom name defined by the user for the job that is going to be created
          output_file : str, optional, default None
              file name where the results are saved if dumpToFile is True.
              If this parameter is not provided, the jobid is used instead
          output_format : str, optional, default 'votable'
              results format
          verbose : bool, optional, default 'False'
              flag to display information about the process
          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, optional, default None
              resource temporary table name associated to the uploaded resource.
              This argument is required if upload_resource is provided.
          autorun : boolean, optional, default True
              if 'True', sets 'phase' parameter to 'RUN',
              so the framework can start the job.
          maxrec : int, optional, default None
              maximum number of rows to return (TAP ``MAXREC`` parameter)
  
          Returns
          -------
          A Job object
          """
  
          output_file_updated = taputils.get_suitable_output_file_name_for_current_output_format(output_file,
                                                                                                 output_format)
  
          if verbose:
              print(f"Launched query: '{query}'")
          if upload_resource is not None:
              if upload_table_name is None:
                  raise ValueError(
                      "Table name is required when a resource is uploaded")
              response = self.__launchJobMultipart(query,
                                                   upload_resource,
                                                   upload_table_name,
                                                   output_format,
                                                   "async",
                                                   verbose,
                                                   name,
                                                   autorun,
                                                   maxrec)
          else:
              response = self.__launchJob(query,
                                          output_format,
                                          "async",
                                          verbose,
                                          name,
                                          autorun,
                                          maxrec)
          isError = self.__connHandler.check_launch_response_status(response,
                                                                    verbose,
                                                                    303,
                                                                    False)
          job = Job(async_job=True, query=query, connhandler=self.__connHandler)
          headers = response.getheaders()
          suitableOutputFile = taputils.get_suitable_output_file(self.__connHandler,
                                                                 True,
                                                                 output_file_updated,
                                                                 headers,
                                                                 isError,
                                                                 output_format)
          job.outputFile = suitableOutputFile
          job.outputFileUser = output_file
          job.set_response_status(response.status, response.reason)
          job.parameters['format'] = output_format
          job.set_phase('PENDING')
          if isError:
              job.failed = True
              job.set_phase('ERROR')
              if dump_to_file:
                  if verbose:
                      print(f"Saving error to: {suitableOutputFile}")
                  self.__connHandler.dump_to_file(suitableOutputFile,
                                                  response)
              raise requests.exceptions.HTTPError(response.reason)
          else:
              location = self.__connHandler.find_header(
                  response.getheaders(),
                  "location")
              jobid = taputils.get_jobid_from_location(location)
              if verbose:
                  print(f"job {jobid}, at: {location}")
              job.jobid = jobid
              job.remoteLocation = location
              if autorun is True:
                  job.set_phase('EXECUTING')
                  if not background:
                      if verbose:
                          print("Retrieving async. results...")
                      # saveResults or getResults will block (not background)
                      if dump_to_file:
                          job.save_results(verbose)
                      else:
                          job.get_results()
                          log.info("Query finished.")
          return job
  
      def load_async_job(self, jobid=None, name=None, verbose=False,
                         load_results=True):
          """Loads an asynchronous job
  
          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
          load_results : bool, optional, default 'True'
              load results associated to this job
  
          Returns
          -------
          A Job object
          """
          if name is not None:
              jobfilter = Filter()
              jobfilter.add_filter('name', name)
              jobs = self.search_async_jobs(jobfilter)
              if jobs is None or len(jobs) < 1:
                  log.info(f"No job found for name '{name}'")
                  return None
              jobid = jobs[0].jobid
          if jobid is None:
              log.info("No job identifier found")
              return None
          subContext = f"async/{jobid}"
          response = self.__connHandler.execute_tapget(subContext,
                                                       verbose=verbose)
          if verbose:
              print(response.status, response.reason)
              print(response.getheaders())
          isError = self.__connHandler.check_launch_response_status(response,
                                                                    verbose,
                                                                    200)
          if isError:
              log.info(response.reason)
              raise requests.exceptions.HTTPError(response.reason)
              return None
          # parse job
          jsp = JobSaxParser(async_job=True)
          job = jsp.parseData(response)[0]
          job.connHandler = self.__connHandler
          # load resulst
          if load_results:
              job.get_results()
          return job
  
      def list_async_jobs(self, verbose=False):
          """Returns all the asynchronous jobs
  
          Parameters
          ----------
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          Returns
          -------
          A list of Job objects
          """
          subContext = "async"
          response = self.__connHandler.execute_tapget(subContext,
                                                       verbose=verbose)
          if verbose:
              print(response.status, response.reason)
              print(response.getheaders())
          isError = self.__connHandler.check_launch_response_status(response,
                                                                    verbose,
                                                                    200)
          if isError:
              log.info(response.reason)
              raise requests.exceptions.HTTPError(response.reason)
              return None
          # parse jobs
          jsp = JobListSaxParser(async_job=True)
          jobs = jsp.parseData(response)
          if jobs is not None:
              for j in jobs:
                  j.connHandler = self.__connHandler
          return jobs
  
      def __appendData(self, args):
          data = urlencode(args)
          result = ""
          firtsTime = True
          for k in data:
              if firtsTime:
                  firtsTime = False
                  result = f"{k}={data[k]}"
              else:
                  result = f"{result}&{k}={data[k]}"
          return result
  
      def save_results(self, job, verbose=False):
          """Saves job results
  
          Parameters
          ----------
          job : Job, mandatory
              job
          verbose : bool, optional, default 'False'
              flag to display information about the process
          """
          job.save_results(verbose=verbose)
  
      def __launchJobMultipart(self, query, uploadResource, uploadTableName,
                               outputFormat, context, verbose, name=None,
                               autorun=True, maxrec=None):
          uploadValue = f"{uploadTableName},param:{uploadTableName}"
          args = {
              "REQUEST": "doQuery",
              "LANG": "ADQL",
              "FORMAT": str(outputFormat),
              "tapclient": str(self.tap_client_id),
              "QUERY": str(query),
              "UPLOAD": "" + str(uploadValue)}
          if maxrec is not None:
              args['MAXREC'] = maxrec
          if autorun is True:
              args['PHASE'] = 'RUN'
          if name is not None:
              args['jobname'] = name
          if isinstance(uploadResource, Table):
              fh = tempfile.NamedTemporaryFile(delete=False)
              uploadResource.write(fh, format='votable')
              fh.close()
              f = open(fh.name, "r")
              chunk = f.read()
              f.close()
              os.unlink(fh.name)
              name = 'pytable'
              args['format'] = 'votable'
          else:
              with open(uploadResource, "r") as fh:
                  chunk = fh.read()
              name = os.path.basename(uploadResource)
          files = [[uploadTableName, name, chunk]]
          contentType, body = self.__connHandler.encode_multipart(args, files)
          response = self.__connHandler.execute_tappost(context,
                                                        body,
                                                        contentType,
                                                        verbose)
          if verbose:
              print(response.status, response.reason)
              print(response.getheaders())
          return response
  
      def __launchJob(self, query, outputFormat, context, verbose, name=None,
                      autorun=True, maxrec=None):
          args = {
              "REQUEST": "doQuery",
              "LANG": "ADQL",
              "FORMAT": str(outputFormat),
              "tapclient": str(self.tap_client_id),
              "QUERY": str(query)}
          if maxrec is not None:
              args['MAXREC'] = maxrec
          if autorun is True:
              args['PHASE'] = 'RUN'
          if name is not None:
              args['jobname'] = name
          response = self.__connHandler.execute_tappost(subcontext=context,
                                                        data=urlencode(args),
                                                        verbose=verbose)
          if verbose:
              print(response.status, response.reason)
              print(response.getheaders())
          return response
  
      def __extract_sync_subcontext(self, location):
          pos = location.find('sync')
          if pos < 0:
              return location
          return location[pos:]
  
      def __findCookieInHeader(self, headers, verbose=False):
          cookies = self.__connHandler.find_header(headers, 'Set-Cookie')
          if verbose:
              print(cookies)
          if cookies is None:
              return None
          else:
              items = cookies.split(';')
              for i in items:
                  if i.startswith("JSESSIONID="):
                      return i
          return None
  
      def __parseUrl(self, url, verbose=False):
          isHttps = False
          if url.startswith("https://"):
              isHttps = True
              protocol = "https"
          else:
              protocol = "http"
  
          if verbose:
              print(f"is https: {isHttps}")
  
          urlInfoPos = url.find("://")
  
          if urlInfoPos < 0:
              raise ValueError("Invalid URL format")
  
          urlInfo = url[(urlInfoPos + 3):]
  
          items = urlInfo.split("/")
  
          if verbose:
              print(f"'{urlInfo}'")
              for i in items:
                  print(f"'{i}'")
  
          itemsSize = len(items)
          hostPort = items[0]
          portPos = hostPort.find(":")
          if portPos > 0:
              # port found
              host = hostPort[0:portPos]
              port = int(hostPort[portPos + 1:])
          else:
              # no port found
              host = hostPort
              # no port specified: use defaults
              if isHttps:
                  port = 443
              else:
                  port = 80
  
          if itemsSize == 1:
              serverContext = ""
              tapContext = ""
          elif itemsSize == 2:
              serverContext = f"/{items[1]}"
              tapContext = ""
          elif itemsSize == 3:
              serverContext = f"/{items[1]}"
              tapContext = f"/{items[2]}"
          else:
              data = []
              for i in range(1, itemsSize - 1):
                  data.append(f"/{items[i]}")
              serverContext = utils.util_create_string_from_buffer(data)
              tapContext = f"/{items[itemsSize - 1]}"
          if verbose:
              print(f"protocol: '{protocol}'")
              print(f"host: '{host}'")
              print(f"port: '{port}'")
              print(f"server context: '{serverContext}'")
              print(f"tap context: '{tapContext}'")
          return protocol, host, port, serverContext, tapContext
  
      def __str__(self):
          return (f"Created TAP+ (v{VERSION}) - Connection:\n{self.__connHandler}")
  
  
  class TapPlus(Tap):
      """TAP plus class
      Provides TAP and TAP+ capabilities
      """
  
      def __init__(self, url=None,
                   host=None,
                   server_context=None,
                   tap_context=None,
                   port=80, sslport=443,
                   default_protocol_is_https=False,
                   connhandler=None,
                   upload_context=None,
                   table_edit_context=None,
                   data_context=None,
                   datalink_context=None,
                   verbose=False,
                   client_id=None):
          """Constructor
  
          Parameters
          ----------
          url : str, mandatory if no host is specified, default None
              TAP URL
          host : str, optional, default None
              host name
          server_context : str, optional, default None
              server context
          tap_context : str, optional, default None
              tap context
          upload_context : str, optional, default None
              upload context
          table_edit_context : str, optional, default None
              context for all actions to be performed over a existing table
          data_context : str, optional, default None
              data context
          datalink_context : str, optional, default None
              datalink context
          port : int, optional, default '80'
              HTTP port
          sslport : int, optional, default '443'
              HTTPS port
          default_protocol_is_https : bool, optional, default False
              Specifies whether the default protocol to be used is HTTPS
          connhandler : connection handler object, optional, default None
              HTTP(s) connection hander (creator). If no handler is provided, a
              new one is created.
          verbose : bool, optional, default 'True'
              flag to display information about the process
          """
  
          super(TapPlus, self).__init__(url, host,
                                        server_context=server_context,
                                        tap_context=tap_context,
                                        upload_context=upload_context,
                                        table_edit_context=table_edit_context,
                                        data_context=data_context,
                                        datalink_context=datalink_context,
                                        port=port, sslport=sslport,
                                        default_protocol_is_https=default_protocol_is_https,  # noqa
                                        connhandler=connhandler,
                                        verbose=verbose)
          self.__internalInit()
          self.__set_client_id(client_id=client_id)
  
      def __internalInit(self):
          self.__user = None
          self.__pwd = None
          self.__isLoggedIn = False
  
      def __set_client_id(self, client_id):
          if client_id:
              self.tap_client_id = client_id
  
      def load_tables(self, only_names=False, include_shared_tables=False,
                      verbose=False):
          """Loads all public tables
  
          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._Tap__load_tables(only_names=only_names,
                                        include_shared_tables=include_shared_tables,  # noqa
                                        verbose=verbose)
  
      def load_data(self, params_dict=None, output_file=None, verbose=False):
          """Loads the specified data
  
          Parameters
          ----------
          params_dict : dictionary, mandatory
              list of request parameters
          output_file : string, optional, default None
              file where the results are saved.
              If it is not provided, the http response contents are returned.
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          Returns
          -------
          A table object if output_file is None.
          None if output_file is not None.
          """
          if verbose:
              print("Retrieving data.")
          connHandler = self.__getconnhandler()
          if not isinstance(params_dict, dict):
              raise ValueError("Parameters dictionary expected")
          data = urlencode(params_dict)
          if verbose:
              print(f"Data request: {data}")
          response = connHandler.execute_datapost(data=data, verbose=verbose)
          if verbose:
              print(response.status, response.reason)
          connHandler.check_launch_response_status(response,
                                                   verbose,
                                                   200)
          if verbose:
              print("Reading...")
          if output_file is not None:
              file = open(output_file, "wb")
              file.write(response.read())
              file.close()
              if verbose:
                  print("Done.")
              return None
          else:
              if 'format' in params_dict:
                  output_format = params_dict['format'].lower()
              else:
                  if 'FORMAT' in params_dict:
                      output_format = params_dict['FORMAT'].lower()
                  else:
                      output_format = "votable"
              results = utils.read_http_response(response, output_format)
              if verbose:
                  print("Done.")
              return results
  
      def load_groups(self, verbose=False):
          """Loads groups
  
          Parameters
          ----------
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          Returns
          -------
          A set of groups of a user
          """
          context = "share?action=GetGroups"
          connHandler = self.__getconnhandler()
          response = connHandler.execute_tapget(context, verbose=verbose)
          if verbose:
              print(response.status, response.reason)
              print(response.getheaders())
          connHandler.check_launch_response_status(response,
                                                   verbose,
                                                   200)
          if verbose:
              print("Parsing groups...")
          gsp = GroupSaxParser()
          gsp.parseData(response)
          log.info(f"Done. {len(gsp.get_groups())} groups found")
          if verbose:
              for g in gsp.get_groups():
                  print(g.title)
          return gsp.get_groups()
  
      def load_group(self, group_name=None, verbose=False):
          """Load group with title being group_name
  
          Parameters
          ----------
          group_name : str, required
              group to be loaded
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          Returns
          -------
          A group with title being group_name
          """
          if group_name is None:
              raise ValueError("'group_name' must be specified")
          groups = self.load_groups(verbose)
          group = None
          for g in groups:
              if str(g.title) == str(group_name):
                  group = g
                  break
          return group
  
      def load_shared_items(self, verbose=False):
          """Loads shared items
  
          Parameters
          ----------
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          Returns
          -------
          A set of shared items
          """
          context = "share?action=GetSharedItems"
          connHandler = self.__getconnhandler()
          response = connHandler.execute_tapget(context, verbose=verbose)
          if verbose:
              print(response.status, response.reason)
              print(response.getheaders())
          connHandler.check_launch_response_status(response,
                                                   verbose,
                                                   200)
          if verbose:
              print("Parsing shared items...")
          ssp = SharedItemsSaxParser()
          ssp.parseData(response)
          log.info(f"Done. {len(ssp.get_shared_items())} shared items found")
          if verbose:
              for g in ssp.get_shared_items():
                  print(g.title)
          return ssp.get_shared_items()
  
      def share_table(self, group_name=None,
                      table_name=None,
                      description=None,
                      verbose=False):
          """Shares a table with a group
  
          Parameters
          ----------
          group_name : str, required
              group in which table will be shared
          table_name : str, required
              table to be shared
          description : str, required
              description of the sharing
          verbose : bool, optional, default 'False'
              flag to display information about the process
          """
          if group_name is None or table_name is None:
              raise ValueError("Both 'group_name' and 'table_name' must be specified")
          if description is None:
              description = ""
          group = self.load_group(group_name, verbose)
          if group is None:
              raise ValueError(f"Group '{group_name}' not found.")
          table = self.load_table(table=table_name, verbose=verbose)
          if table is None:
              raise ValueError(f"Table '{table_name}' not found.")
          data = (f"action=CreateOrUpdateItem&resource_type=0&title="
                  f"{table_name}"
                  f"&description="
                  f"{description}"
                  f"&items_list="
                  f"{group.id}|Group|Read")
          connHandler = self.__getconnhandler()
          response = connHandler.execute_share(data, verbose=verbose)
          if verbose:
              print(response.status, response.reason)
              print(response.getheaders())
          connHandler.check_launch_response_status(response,
                                                   verbose,
                                                   200)
          msg = f"Shared table '{table_name}' to group '{group_name}'."
          log.info(msg)
  
      def share_table_stop(self, group_name=None, table_name=None,
                           verbose=False):
          """Stop sharing a table
  
          Parameters
          ----------
          group_name : str, required
              group where the table is shared to
          table_name : str, required
              table to be stopped from being shared
          verbose : bool, optional, default 'False'
              flag to display information about the process
          """
          if group_name is None or table_name is None:
              raise ValueError("Both 'group_name' and 'table_name' must be specified")
          group = self.load_group(group_name, verbose)
          if group is None:
              raise ValueError(f"Group '{group_name}' not found.")
          shared_items = self.load_shared_items(verbose)
          shared_item = None
          for s in shared_items:
              if str(s.title) == str(table_name):
                  # check group
                  groups = s.shared_to_items
                  for g in groups:
                      if group.id == g.id:
                          shared_item = s
                          break
                  if shared_item is not None:
                      break
          if shared_item is None:
              raise ValueError(f"Table '{table_name}', shared to group '{group_name}', not found.")
          data = (f"action=RemoveItem&resource_type=0&resource_id="
                  f"{shared_item.id}"
                  f"&resource_type=0")
          connHandler = self.__getconnhandler()
          response = connHandler.execute_share(data, verbose=verbose)
          if verbose:
              print(response.status, response.reason)
              print(response.getheaders())
          connHandler.check_launch_response_status(response,
                                                   verbose,
                                                   200)
  
          msg = f"Stop sharing table '{table_name}' to group '{group_name}'."
          log.info(msg)
  
      def share_group_create(self, group_name=None, description=None,
                             verbose=False):
          """Creates a group
  
          Parameters
          ----------
          group_name : str, required
              group to be created
          description : str, required
              description of the group
          verbose : bool, optional, default 'False'
              flag to display information about the process
          """
          if group_name is None:
              raise ValueError("'group_name' must be specified")
          if description is None:
              description = ""
          group = self.load_group(group_name, verbose)
          if group is not None:
              raise ValueError(f"Group {group_name} already exists")
          data = (f"action=CreateOrUpdateGroup&resource_type=0&title="
                  f"{group_name}"
                  f"&description="
                  f"{description}")
          connHandler = self.__getconnhandler()
          response = connHandler.execute_share(data, verbose=verbose)
          if verbose:
              print(response.status, response.reason)
              print(response.getheaders())
          connHandler.check_launch_response_status(response,
                                                   verbose,
                                                   200)
          msg = f"Created group '{group_name}'."
          log.info(msg)
  
      def share_group_delete(self,
                             group_name=None,
                             verbose=False):
          """Deletes a group
  
          Parameters
          ----------
          group_name : str, required
              group to be created
          verbose : bool, optional, default 'False'
              flag to display information about the process
          """
          if group_name is None:
              raise ValueError("'group_name' must be specified")
          group = self.load_group(group_name, verbose)
          if group is None:
              raise ValueError(f"Group '{group_name}' doesn't exist")
          data = (f"action=RemoveGroup&resource_type=0&group_id={group.id}")
          connHandler = self.__getconnhandler()
          response = connHandler.execute_share(data, verbose=verbose)
          if verbose:
              print(response.status, response.reason)
              print(response.getheaders())
          connHandler.check_launch_response_status(response,
                                                   verbose,
                                                   200)
          msg = f"Deleted group '{group_name}'."
          log.info(msg)
  
      def share_group_add_user(self,
                               group_name=None,
                               user_id=None,
                               verbose=False):
          """Adds user to a group
  
          Parameters
          ----------
          group_name : str, required
              group which user_id will be added in
          user_id : str, required
              user id to be added
          verbose : bool, optional, default 'False'
              flag to display information about the process
          """
          if group_name is None or user_id is None:
              raise ValueError("Both 'group_name' and 'user_id' must be specified")
          group = self.load_group(group_name, verbose)
          if group is None:
              raise ValueError(f"Group '{group_name}' doesn't exist")
          user_found_in_group = False
          for u in group.users:
              if str(u.id) == user_id:
                  user_found_in_group = True
                  break
          if user_found_in_group is True:
              raise ValueError(f"User id '{user_id}' found in group '{group_name}'")
          if self.is_valid_user(user_id, verbose) is False:
              raise ValueError(f"User id '{user_id}' not found.")
          users = ""
          for u in group.users:
              users = f"{users}{u.id},"
          users = users + user_id
          data = ("action=CreateOrUpdateGroup&group_id="
                  f"{group.id}&title="
                  f"{group.title}&description="
                  f"{group.description}&users_list="
                  f"{users}")
          connHandler = self.__getconnhandler()
          response = connHandler.execute_share(data, verbose=verbose)
          if verbose:
              print(response.status, response.reason)
              print(response.getheaders())
          connHandler.check_launch_response_status(response,
                                                   verbose,
                                                   200)
          msg = f"Added user '{user_id}' from group '{group_name}'."
          log.info(msg)
  
      def share_group_delete_user(self,
                                  group_name=None,
                                  user_id=None,
                                  verbose=False):
          """Deletes user from a group
  
          Parameters
          ----------
          group_name : str, required
              group which user_id will be removed from
          user_id : str, required
              user id to be deleted
          verbose : bool, optional, default 'False'
              flag to display information about the process
          """
          if group_name is None or user_id is None:
              raise ValueError("Both 'group_name' and 'user_id' must be specified")
          group = self.load_group(group_name, verbose)
          if group is None:
              raise ValueError(f"Group '{group_name}' doesn't exist")
          user_found_in_group = False
          for u in group.users:
              if str(u.id) == user_id:
                  user_found_in_group = True
                  break
          if user_found_in_group is False:
              raise ValueError(f"User id '{user_id}' not found in group '{group_name}'")
          users = ""
          for u in group.users:
              if str(u.id) == str(user_id):
                  continue
              users = f"{users}{u.id},"
          if str(users) != "":
              users = users[:-1]
          data = (f"action=CreateOrUpdateGroup&group_id="
                  f"{group.id}&title="
                  f"{group.title}&description="
                  f"{group.description}&users_list="
                  f"{users}")
          connHandler = self.__getconnhandler()
          response = connHandler.execute_share(data, verbose=verbose)
          if verbose:
              print(response.status, response.reason)
              print(response.getheaders())
          connHandler.check_launch_response_status(response,
                                                   verbose,
                                                   200)
          msg = f"Deleted user '{user_id}' from group '{group_name}'."
          log.info(msg)
  
      def is_valid_user(self, user_id=None, verbose=False):
          """Determines if the specified user exists in the system
          TAP+ only
  
          Parameters
          ----------
          user_id : str, mandatory
              user id to be checked
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          Returns
          -------
          Boolean indicating if the specified user exists
          """
          if user_id is None:
              raise ValueError("'user_id' must be specified")
          context = f"users?USER={user_id}"
          connHandler = self.__getconnhandler()
          response = connHandler.execute_tapget(context, verbose=verbose)
          if verbose:
              print(response.status, response.reason)
              print(response.getheaders())
          connHandler.check_launch_response_status(response,
                                                   verbose,
                                                   200)
          responseBytes = response.read()
          user = responseBytes.decode('utf-8')
          if verbose:
              print(f"USER response = {user}")
          return user.startswith(f"{user_id}:") and user.count("\\n") == 0
  
      def get_datalinks(self, ids, verbose=False):
          """Gets datalinks associated to the provided identifiers
  
          Parameters
          ----------
          ids : str list, mandatory
              list of identifiers
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          Returns
          -------
          A table object
          """
          if verbose:
              print("Retrieving datalink.")
          if ids is None:
              raise ValueError("Missing mandatory argument 'ids'")
          if isinstance(ids, str):
              ids_arg = f"ID={ids}"
          else:
              if isinstance(ids, int):
                  ids_arg = f"ID={ids}"
              else:
                  ids_arg = f"ID={','.join(str(item) for item in ids)}"
          if verbose:
              print(f"Datalink request: {ids_arg}")
          connHandler = self.__getconnhandler()
          response = connHandler.execute_datalinkpost(subcontext="links",
                                                      data=ids_arg,
                                                      verbose=verbose)
          if verbose:
              print(response.status, response.reason)
          connHandler.check_launch_response_status(response,
                                                   verbose,
                                                   200)
          if verbose:
              print("Done.")
          results = utils.read_http_response(response, "votable")
  
          return results
  
      def search_async_jobs(self, jobfilter=None, verbose=False):
          """Searches for jobs applying the specified filter
  
          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
          """
          # jobs/list?[&session=][&limit=][&offset=][&order=][&metadata_only=true|false]
          subContext = "jobs/async"
          if jobfilter is not None:
              data = jobfilter.createUrlRequest()
              if data is not None:
                  subContext = f"{subContext}?{self.__appendData(data)}"
          connHandler = self.__getconnhandler()
          response = connHandler.execute_tapget(subContext, verbose=verbose)
          if verbose:
              print(response.status, response.reason)
              print(response.getheaders())
          connHandler.check_launch_response_status(response,
                                                   verbose,
                                                   200)
          # parse jobs
          jsp = JobSaxParser(async_job=True)
          jobs = jsp.parseData(response)
          if jobs is not None:
              for j in jobs:
                  j.connHandler = connHandler
          return jobs
  
      def remove_jobs(self, jobs_list, verbose=False):
          """Removes the specified jobs
  
          Parameters
          ----------
          jobs_list : str, mandatory
              jobs identifiers to be removed
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          """
          if jobs_list is None:
              return
          jobsIds = None
          if isinstance(jobs_list, str):
              jobsIds = jobs_list
          elif isinstance(jobs_list, list):
              jobsIds = ','.join(jobs_list)
          else:
              raise Exception("Invalid object type")
          if verbose:
              print(f"Jobs to be removed: {jobsIds}")
          data = f"JOB_IDS={jobsIds}"
          subContext = "deletejobs"
          connHandler = self.__getconnhandler()
          response = connHandler.execute_tappost(subContext,
                                                 data,
                                                 verbose=verbose)
          if verbose:
              print(response.status, response.reason)
              print(response.getheaders())
          connHandler.check_launch_response_status(response,
                                                   verbose,
                                                   200)
          msg = f"Removed jobs: '{jobs_list}'."
          log.info(msg)
  
      def __execLogin(self, usr, pwd, verbose=False):
          subContext = "login"
          args = {
              "username": usr,
              "password": pwd}
          connHandler = self.__getconnhandler()
          response = connHandler.execute_secure(subContext, urlencode(args), verbose)
          if verbose:
              print(response.status, response.reason)
              print(response.getheaders())
          return response
  
      def upload_table(self, upload_resource=None, table_name=None,
                       table_description=None,
                       format=None, verbose=False):
          """Uploads a table to the user private space
  
          Parameters
          ----------
          upload_resource : object, mandatory
              table to be uploaded: pyTable, file or URL.
          table_name : str, required if uploadResource is provided, default None
              resource temporary table name associated to the uploaded resource
          table_description : str, optional, default None
              table description
          format : str, optional, default 'VOTable'
              resource format
          verbose : bool, optional, default 'False'
              flag to display information about the process
          """
  
          if upload_resource is None:
              raise ValueError("Missing mandatory argument 'upload_resource'")
          if table_name is None:
              raise ValueError("Missing mandatory argument 'table_name'")
          if table_description is None:
              description = ""
          else:
              description = table_description
          if format is None:
              format = "votable"
  
          response = self.__uploadTableMultipart(resource=upload_resource,
                                                 table_name=table_name,
                                                 table_description=description,
                                                 resource_format=format,
                                                 verbose=verbose)
          if response.status == 303:
              location = self.__getconnhandler().find_header(
                  response.getheaders(),
                  "location")
              jobid = taputils.get_jobid_from_location(location)
              job = Job(async_job=True,
                        query=None,
                        connhandler=self.__getconnhandler())
              job.jobid = jobid
              job.name = 'Table upload'
              job.set_phase('EXECUTING')
              log.info(f"Job '{jobid}' created to upload table '{table_name}'.")
              return job
          else:
              log.info(f"Uploaded table '{table_name}'.")
              return None
  
      def __uploadTableMultipart(self, resource, table_name=None,
                                 table_description=None,
                                 resource_format="VOTable",
                                 verbose=False):
          connHandler = self.__getconnhandler()
          if isinstance(resource, Table):
              args = {
                  "TASKID": str(-1),
                  "TABLE_NAME": str(table_name),
                  "TABLE_DESC": str(table_description),
                  "FORMAT": 'votable'}
              log.info("Sending pytable.")
              fh = tempfile.NamedTemporaryFile(delete=False)
              resource.write(fh, format='votable')
              fh.close()
              f = open(fh.name, "r")
              chunk = f.read()
              f.close()
              os.unlink(fh.name)
              files = [['FILE', 'pytable', chunk]]
              contentType, body = connHandler.encode_multipart(args, files)
          else:
              if not (str(resource).startswith("http")):  # upload from file
                  args = {
                      "TASKID": str(-1),
                      "TABLE_NAME": str(table_name),
                      "TABLE_DESC": str(table_description),
                      "FORMAT": str(resource_format)}
                  log.info(f"Sending file: {resource}")
                  with open(resource, "r") as f:
                      chunk = f.read()
                  files = [['FILE', os.path.basename(resource), chunk]]
                  contentType, body = connHandler.encode_multipart(args, files)
              else:  # upload from URL
                  args = {
                      "TASKID": str(-1),
                      "TABLE_NAME": str(table_name),
                      "TABLE_DESC": str(table_description),
                      "FORMAT": str(resource_format),
                      "URL": str(resource)}
                  files = [['FILE', "", ""]]
                  contentType, body = connHandler.encode_multipart(args, files)
          response = connHandler.execute_upload(body, contentType)
          if verbose:
              print(response.status, response.reason)
              print(response.getheaders())
          if response.status != 303 and response.status != 302:
              connHandler.check_launch_response_status(response,
                                                       verbose,
                                                       200)
          return response
  
      def upload_table_from_job(self, job=None, table_name=None,
                                table_description=None, verbose=False):
          """Creates a table to the user private space from a job
  
          Parameters
          ----------
          job: job, mandatory
              job used to create a table. Could be a string with the jobid or
              a job itself
          table_name : str, default 't'+jobid
              resource temporary table name associated to the uploaded resource
          table_description : str, optional, default None
              table description
          verbose : bool, optional, default 'False'
              flag to display information about the process
          """
          if job is None:
              raise ValueError("Missing mandatory argument 'job'")
          if isinstance(job, Job):
              j = job
              description = j.parameters['query']
          else:
              j = self.load_async_job(jobid=job, load_results=False)
              if j is None:
                  raise ValueError(f"Job {job} not found")
                  return
              description = j.parameters['query']
          if table_name is None:
              table_name = f"t{j.jobid}"
          if table_description is None:
              table_description = description
          if verbose:
              print(f"JOB = {j.jobid}")
          self.__uploadTableMultipartFromJob(resource=j.jobid,
                                             table_name=table_name,
                                             table_description=table_description,
                                             verbose=verbose)
          msg = f"Created table '{table_name}' from job: '{j.jobid}'."
          log.info(msg)
  
      def __uploadTableMultipartFromJob(self, resource, table_name=None,
                                        table_description=None, verbose=False):
          args = {
              "TASKID": str(-1),
              "JOBID": str(resource),
              "TABLE_NAME": str(table_name),
              "TABLE_DESC": str(table_description),
              "FORMAT": str(format)}
          files = [['FILE', "", ""]]
          connHandler = self.__getconnhandler()
          contentType, body = connHandler.encode_multipart(args, files)
          response = connHandler.execute_upload(body, contentType)
          if verbose:
              print(response.status, response.reason)
              print(response.getheaders())
          connHandler.check_launch_response_status(response,
                                                   verbose,
                                                   200)
          return response
  
      def delete_user_table(self, table_name=None, force_removal=False,
                            verbose=False):
          """Removes a user table
  
          Parameters
          ----------
          table_name : str, required
              table to be removed
          force_removal : bool, optional, default 'False'
              flag to indicate if removal should be forced
          verbose : bool, optional, default 'False'
              flag to display information about the process
          """
          if table_name is None:
              raise ValueError("Table name cannot be null")
          if force_removal is True:
              args = {
                  "TABLE_NAME": str(table_name),
                  "DELETE": "TRUE",
                  "FORCE_REMOVAL": "TRUE"}
          else:
              args = {
                  "TABLE_NAME": str(table_name),
                  "DELETE": "TRUE",
                  "FORCE_REMOVAL": "FALSE"}
          connHandler = self.__getconnhandler()
          response = connHandler.execute_upload(urlencode(args), verbose=verbose)
          if verbose:
              print(response.status, response.reason)
              print(response.getheaders())
          connHandler.check_launch_response_status(response,
                                                   verbose,
                                                   200)
          msg = f"Table '{table_name}' deleted."
          log.info(msg)
  
      def rename_table(self, table_name=None, new_table_name=None, new_column_names_dict=None,
                       verbose=False):
          """ This method allows you to update the column names of a user table.
  
          Parameters
          ----------
          table_name: str, required
              old name of the user's table
          new_table_name: str, required
              new name of the user's table
          new_column_names_dict: dict str:str
              dict with pairs "old_column1_name:new_column1_name"
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          """
          if new_column_names_dict is None:
              new_column_names_dict = {}
          args = {}
  
          if table_name is None:
              raise ValueError(
                  "Argument 'table_name' is mandatory. "
                  "Please introduce the name of the table that is going to be updated")
          if (new_table_name is None) and (new_column_names_dict is None):
              raise ValueError("Please introduce as minimum a new name for the table or a new name for a column with "
                               "format old_column1_name:new_column1_name, ... ,old_columnN_name:new_columnN_name")
          if new_table_name is not None:
              if new_column_names_dict is None:
                  # case 1: We only need to rename the table
                  args = self.get_args_4_rename_table(table_name, new_table_name)
              else:
                  # case 2: We need to rename both, table and column names
                  args = self.get_args_4_rename_table(table_name, new_table_name, new_column_names_dict)
  
          if new_table_name is None:
              if new_column_names_dict:
                  # case 3: We only need to rename the columns but same table name
                  args = self.get_args_4_rename_table(table_name, table_name, new_column_names_dict)
  
          connHandler = self.__getconnhandler()
          response = connHandler.execute_table_tool(urlencode(args), verbose=verbose)
  
          if verbose:
              print(response.status, response.reason)
              print(response.getheaders())
          connHandler.check_launch_response_status(response,
                                                   verbose,
                                                   200)
          if verbose:
              msg = f"Table '{table_name}' updated."
              print(msg)
  
      def get_args_4_rename_table(self, table_name, new_table_name, new_column_names_dict):
  
          args = {}
  
          if not new_column_names_dict:
              args = {
                  "action": "rename",
                  "new_table_name": new_table_name,
                  "table_name": table_name
              }
          else:
              new_column_names = ','.join(f'{key}:{value}' for key, value in new_column_names_dict.items())
  
              args = {
                  "action": "rename",
                  "new_column_names": new_column_names,
                  "new_table_name": new_table_name,
                  "table_name": table_name
              }
          return args
  
      def update_user_table(self, table_name=None, list_of_changes=[],
                            verbose=False):
          """Updates a user table
  
          Parameters
          ----------
          table_name : str
              table to be updated
          list_of_changes : list
              list of lists, each one of them containing sets of
              [column_name, field_name, value].
              column_name is the name of the column to be updated
              field_name is the name of the tap field to be modified
              field name can be 'utype', 'ucd', 'flags' or 'indexed'
              value is the new value this field of this column will take
          verbose : bool, optional, default 'False'
              flag to display information about the process
          """
          if table_name is None:
              raise ValueError("Table name cannot be null")
          if len(list_of_changes) == 0:
              raise ValueError("List of changes cannot be empty")
          for change in list_of_changes:
              if change is None:
                  raise ValueError("None of the changes can be null")
              if len(change) != 3:  # [column_name, field_name, value]
                  raise ValueError("All of the changes must have three elements: [column_name, field_name, value]")
              index = 0
              for value in change:
                  if value is None:
                      raise ValueError("None of the values for the changes can be null")
                  if (index == 1 and value != 'utype' and value != 'ucd' and value != 'flags' and value != 'indexed'):
                      raise ValueError("Position 2 of all changes must be 'utype', 'ucd', 'flags' or 'indexed'")
                  index = index + 1
  
          table = self.load_table(table=table_name, verbose=verbose)
          if table is None:
              raise ValueError("Table name not found")
          columns = table.columns
          if len(columns) == 0:
              raise ValueError("Table has no columns")
  
          for change in list_of_changes:
              index = 0
              for value in change:
                  if index == 0:
                      found = False
                      for c in columns:
                          if c.name == value:
                              found = True
                              break
                      if found is False:
                          raise ValueError(f"Column name introduced "
                                           f"{value}"
                                           f" was not found in the table")
                  index = index + 1
  
          new_ra_column = TapPlus.__changesContainFlag(list_of_changes, "Ra")
          new_dec_column = TapPlus.__changesContainFlag(list_of_changes, "Dec")
  
          # check whether both (Ra/Dec) are present
          # or both are None
          if ((new_ra_column is not None and new_dec_column is None)
                  or (new_ra_column is None and new_dec_column is not None)):
              raise ValueError("Both Ra and Dec must be specified when updating one of them.")
  
          args = TapPlus.get_table_update_arguments(table_name, columns,
                                                    list_of_changes)
  
          connHandler = self.__getconnhandler()
          response = connHandler.execute_table_edit(urlencode(args), verbose=verbose)
          if verbose:
              print(response.status, response.reason)
              print(response.getheaders())
          connHandler.check_launch_response_status(response,
                                                   verbose,
                                                   200)
          msg = f"Table '{table_name}' updated."
          log.info(msg)
  
      @staticmethod
      def get_table_update_arguments(table_name, columns, list_of_changes):
          num_cols = len(columns)
          args = {
              "ACTION": "edit",
              "NUMTABLES": str(1),
              "TABLE0_NUMCOLS": str(num_cols),
              "TABLE0": str(table_name),
          }
          index = 0
          for column in columns:
              found_in_changes = False
              for change in list_of_changes:
                  if (str(change[0]) == str(column.name)):
                      found_in_changes = True
                      break
  
              # set current values
              column_name, flags, indexed, ucd, utype = \
                  TapPlus.get_current_column_values_for_update(column)
  
              # Update values if required
              if found_in_changes:
                  flags, indexed, ucd, utype = \
                      TapPlus.get_new_column_values_for_update(list_of_changes,
                                                               column_name,
                                                               flags,
                                                               indexed,
                                                               ucd, utype)
  
              # Prepare http request parameters for a column
              args[f"TABLE0_COL{index}"] = str(column_name)
              args[f"TABLE0_COL{index}_UCD"] = str(ucd)
              args[f"TABLE0_COL{index}_UTYPE"] = str(utype)
              args[f"TABLE0_COL{index}_INDEXED"] = str(indexed)
              args[f"TABLE0_COL{index}_FLAGS"] = str(flags)
              index = index + 1
          return args
  
      @staticmethod
      def get_current_column_values_for_update(column):
          column_name = column.name
          flags = column.flags
          if str(flags) == '1':
              flags = 'Ra'
          elif str(flags) == '2':
              flags = 'Dec'
          elif str(flags) == '4':
              flags = 'Flux'
          elif str(flags) == '8':
              flags = 'Mag'
          elif str(flags) == '16':
              flags = 'PK'
          elif str(flags) == '33':
              flags = 'Ra'
          elif str(flags) == '34':
              flags = 'Dec'
          elif str(flags) == '38':
              flags = 'Flux'
          elif str(flags) == '40':
              flags = 'Mag'
          elif str(flags) == '48':
              flags = 'PK'
          else:
              flags = None
          indexed = (str(column.flag) == 'indexed'
                     or str(flags) == 'Ra'
                     or str(flags) == 'Dec'
                     or str(flags) == 'PK')
          ucd = str(column.ucd)
          utype = str(column.utype)
          return column_name, flags, indexed, ucd, utype
  
      @staticmethod
      def get_new_column_values_for_update(list_of_changes, column_name,
                                           c_flags, c_indexed, c_ucd, c_utype):
          found_new_flags = False
          found_new_indexed = False
          found_new_ucd = False
          found_new_utype = False
          n_flags = None
          n_indexed = None
          n_utype = None
          n_ucd = None
          for change in list_of_changes:
              if str(change[0]) == column_name:
                  if str(change[1]) == 'flags':
                      n_flags = str(change[2])
                      found_new_flags = True
                  if str(change[1]) == 'indexed':
                      n_indexed = str(change[2])
                      found_new_indexed = True
                  if str(change[1]) == 'ucd':
                      n_ucd = str(change[2])
                      found_new_ucd = True
                  if str(change[1]) == 'utype':
                      n_utype = str(change[2])
                      found_new_utype = True
  
          if found_new_ucd:
              ucd = n_ucd
          else:
              ucd = c_ucd
  
          if found_new_utype:
              utype = n_utype
          else:
              utype = c_utype
  
          if found_new_indexed:
              indexed = n_indexed
          else:
              indexed = c_indexed
  
          # index could be updated
          if found_new_flags:
              if n_flags is None or n_flags == '':
                  if found_new_indexed:
                      indexed = str(n_indexed)
                  else:
                      indexed = str(False)
              else:
                  # Index required for PK, Ra, Dec
                  if c_flags == 'Ra' or c_flags == 'Dec' or c_flags == 'PK':
                      indexed = str(True)
              flags = n_flags
          else:
              flags = c_flags
  
          return flags, indexed, ucd, utype
  
      def set_ra_dec_columns(self, table_name=None,
                             ra_column_name=None, dec_column_name=None,
                             verbose=False):
          """Set columns of a table as ra and dec respectively a user table
  
          Parameters
          ----------
          table_name : str
              table to be set
          ra_column_name : str
              ra column to be set
          dec_column_name : str
              dec column to be set
          verbose : bool, optional, default 'False'
              flag to display information about the process
          """
  
          if table_name is None:
              raise ValueError("Table name cannot be null")
          if ra_column_name is None:
              raise ValueError("Ra column name cannot be null")
          if dec_column_name is None:
              raise ValueError("Dec column name cannot be null")
  
          args = {
              "ACTION": "radec",
              "TABLE_NAME": str(table_name),
              "RA": str(ra_column_name),
              "DEC": str(dec_column_name),
          }
          connHandler = self.__getconnhandler()
          response = connHandler.execute_table_edit(urlencode(args), verbose=verbose)
          isError = connHandler.check_launch_response_status(response,
                                                             verbose,
                                                             200)
          if isError:
              log.info(response.reason)
              raise requests.exceptions.HTTPError(response.reason)
          msg = f"Table '{table_name}' updated (ra/dec)."
          return msg
  
      def login(self, user=None, password=None, credentials_file=None,
                verbose=False):
          """Performs a login.
          User and password arguments 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).
          If no arguments are provided, a prompt asking for user name and
          password will appear.
  
          Parameters
          ----------
          user : str, default None
              login name
          password : str, default None
              user password
          credentials_file : str, default None
              file containing user and password in two lines
          verbose : bool, optional, default 'False'
              flag to display information about the process
          """
          if credentials_file is not None:
              # read file: get user & password
              with open(credentials_file, "r") as ins:
                  user = ins.readline().strip()
                  password = ins.readline().strip()
          if user is None:
              user = input("User: ")
              if user is None:
                  log.info("Invalid user name")
                  return
          if password is None:
              password = getpass.getpass("Password: ")
              if password is None:
                  log.info("Invalid password")
                  return
          self.__user = str(user)
          self.__pwd = str(password)
          self.__dologin(verbose)
  
      def login_gui(self, verbose=False):
          """Performs a login using a GUI dialog
  
          Parameters
          ----------
          verbose : bool, optional, default 'False'
              flag to display information about the process
          """
          connHandler = self.__getconnhandler()
          url = connHandler.get_host_url()
          loginDialog = LoginDialog(url)
          loginDialog.show_login()
          if loginDialog.is_accepted():
              self.__user = loginDialog.get_user()
              self.__pwd = loginDialog.get_password()
              # execute login
              self.__dologin(verbose)
          else:
              self.__isLoggedIn = False
  
      def __dologin(self, verbose=False):
          self.__isLoggedIn = False
          response = self.__execLogin(self.__user, self.__pwd, verbose)
          # check response
          connHandler = self.__getconnhandler()
          isError = connHandler.check_launch_response_status(response,
                                                             verbose,
                                                             200)
          if isError:
              log.info(f"Login error: {response.reason}")
              raise requests.exceptions.HTTPError(f"Login error: {response.reason}")
          else:
              # extract cookie
              cookie = self._Tap__findCookieInHeader(response.getheaders())
              if cookie is not None:
                  self.__isLoggedIn = True
                  connHandler.set_cookie(cookie)
          log.info("OK")
  
      def logout(self, verbose=False):
          """Performs a logout
  
          Parameters
          ----------
          verbose : bool, optional, default 'False'
              flag to display information about the process
          """
          subContext = "logout"
          connHandler = self.__getconnhandler()
          response = connHandler.execute_secure(subContext, "")
          if verbose:
              print(response.status, response.reason)
              print(response.getheaders())
          self.__isLoggedIn = False
  
      @staticmethod
      def __columnsContainFlag(columns=None, flag=None, verbose=False):
          c = None
          if columns is not None and len(columns) > 0:
              for column in columns:
                  f = column.flags
                  if str(f) == '1' or str(f) == '33':
                      f = 'Ra'
                  elif str(f) == '2' or str(f) == '34':
                      f = 'Dec'
                  elif str(f) == '4' or str(f) == '38':
                      f = 'Flux'
                  elif str(f) == '8' or str(f) == '40':
                      f = 'Mag'
                  elif str(f) == '16' or str(f) == '48':
                      f = 'PK'
                  else:
                      f = None
                  if str(flag) == str(f):
                      c = column.name
                      break
          return c
  
      @staticmethod
      def __changesContainFlag(changes=None, flag=None, verbose=False):
          c = None
          if changes is not None and len(changes) > 0:
              for change in changes:
                  if str(change[1]) == "flags":
                      value = str(change[2])
                      if str(flag) == str(value):
                          c = str(change[0])
                          break
          return c
  
      def __getconnhandler(self):
          return self._Tap__connHandler
  
files: astroquery/utils/tap/core.py
files: # Licensed under a 3-clause BSD style license - see LICENSE.rst
  """
  =============
  TAP plus
  =============
  
  @author: Juan Carlos Segovia
  @contact: juan.carlos.segovia@sciops.esa.int
  
  European Space Astronomy Centre (ESAC)
  European Space Agency (ESA)
  
  Created on 30 jun. 2016
  Modified on 1 jun. 2021 by mhsarmiento
  """
  from astroquery.utils.tap import taputils
  from astroquery.utils.tap.conn.tapconn import TapConn
  from astroquery.utils.tap.xmlparser.tableSaxParser import TableSaxParser
  from astroquery.utils.tap.model.job import Job
  from astroquery.utils.tap.gui.login import LoginDialog
  from astroquery.utils.tap.xmlparser.jobSaxParser import JobSaxParser
  from astroquery.utils.tap.xmlparser.jobListSaxParser import JobListSaxParser
  from astroquery.utils.tap.xmlparser.groupSaxParser import GroupSaxParser
  from astroquery.utils.tap.xmlparser.sharedItemsSaxParser import SharedItemsSaxParser  # noqa
  from astroquery.utils.tap.xmlparser import utils
  from astroquery.utils.tap.model.filter import Filter
  import requests
  from astroquery import log
  import getpass
  import os
  from astropy.table.table import Table
  import tempfile
  
  __all__ = ['Tap', 'TapPlus']
  
  VERSION = "20200428.1"
  
  
  class Tap:
      """TAP class
      Provides TAP capabilities
      """
  
      def __init__(self, url=None,
                   host=None,
                   server_context=None,
                   tap_context=None,
                   port=80, sslport=443,
                   default_protocol_is_https=False,
                   connhandler=None,
                   upload_context=None,
                   table_edit_context=None,
                   data_context=None,
                   datalink_context=None,
                   verbose=False):
          """Constructor
  
          Parameters
          ----------
          url : str, mandatory if no host is specified, default None
              TAP URL
          host : str, optional, default None
              host name
          server_context : str, mandatory, default None
              server context
          tap_context : str, mandatory, default None
              tap context
          upload_context : str, optional, default None
              upload context
          table_edit_context : str, mandatory, default None
              context for all actions to be performed over a existing table
          data_context : str, optional, default None
              data context
          datalink_context : str, optional, default None
              datalink context
          port : int, optional, default '80'
              HTTP port
          sslport : int, optional, default '443'
              HTTPS port
          default_protocol_is_https : bool, optional, default False
              Specifies whether the default protocol to be used is HTTPS
          connhandler : connection handler object, optional, default None
              HTTP(s) connection hander (creator). If no handler is provided, a
              new one is created.
          verbose : bool, optional, default 'False'
              flag to display information about the process
          """
          self.__internalInit()
          if url is not None:
              protocol, host, port, server, tap = self.__parseUrl(url)
              if server_context is None:
                  server_context = server
              if tap_context is None:
                  tap_context = tap
              if protocol == "http":
                  tap = TapConn(ishttps=False,
                                host=host,
                                server_context=server_context,
                                tap_context=tap_context,
                                upload_context=upload_context,
                                table_edit_context=table_edit_context,
                                data_context=data_context,
                                datalink_context=datalink_context,
                                port=port,
                                sslport=sslport)
                  self.__connHandler = tap
              else:
                  # https port -> sslPort
                  tap = TapConn(ishttps=True,
                                host=host,
                                server_context=server_context,
                                tap_context=tap_context,
                                upload_context=upload_context,
                                table_edit_context=table_edit_context,
                                data_context=data_context,
                                datalink_context=datalink_context,
                                port=port,
                                sslport=port)
                  self.__connHandler = tap
          else:
              tap = TapConn(ishttps=default_protocol_is_https,
                            host=host,
                            server_context=server_context,
                            tap_context=tap_context,
                            upload_context=upload_context,
                            table_edit_context=table_edit_context,
                            data_context=data_context,
                            datalink_context=datalink_context,
                            port=port,
                            sslport=sslport)
              self.__connHandler = tap
          # if connectionHandler is set, use it (useful for testing)
          if connhandler is not None:
              self.__connHandler = connhandler
          if verbose:
              print(f"Created TAP+ (v{VERSION}) - Connection:\n{self.__connHandler}")
  
      def __internalInit(self):
          self.__connHandler = None
          self.tap_client_id = f"aqtappy1-{VERSION}"
  
      def load_tables(self, verbose=False):
          """Loads all public tables
  
          Parameters
          ----------
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          Returns
          -------
          A list of table objects
          """
          return self.__load_tables(verbose=verbose)
  
      def load_table(self, table, verbose=False):
          """Loads the specified table
  
          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
          """
          if table is None:
              raise ValueError("Table name is required")
          if verbose:
              print(f"Retrieving table '{table}'")
          response = self.__connHandler.execute_tapget(f"tables?tables={table}",
                                                       verbose=verbose)
          if verbose:
              print(response.status, response.reason)
          self.__connHandler.check_launch_response_status(response,
                                                          verbose,
                                                          200)
          if verbose:
              print(f"Parsing table '{table}'...")
          tsp = TableSaxParser()
          tsp.parseData(response)
          if verbose:
              print("Done.")
          return tsp.get_table()
  
      def __load_tables(self, only_names=False, include_shared_tables=False,
                        verbose=False):
          """Loads all public tables
  
          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
          """
          # share_info=true&share_accessible=true&only_tables=true
          flags = ""
          addedItem = False
          if only_names:
              flags = "only_tables=true"
              addedItem = True
          if include_shared_tables:
              if addedItem:
                  flags += "&"
              flags += "share_accessible=true"
              addedItem = True
          log.info("Retrieving tables...")
          if flags != "":
              response = self.__connHandler.execute_tapget(f"tables?{flags}", verbose=verbose)
          else:
              response = self.__connHandler.execute_tapget("tables",
                                                           verbose=verbose)
          if verbose:
              print(response.status, response.reason)
          isError = self.__connHandler.check_launch_response_status(response,
                                                                    verbose,
                                                                    200)
          if isError:
              log.info(f"{response.status} {response.reason}")
              raise requests.exceptions.HTTPError(response.reason)
              return None
          log.info("Parsing tables...")
          tsp = TableSaxParser()
          tsp.parseData(response)
          log.info("Done.")
          return tsp.get_tables()
  
      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, maxrec=None):
          """Launches a synchronous job
  
          Parameters
          ----------
          query : str, mandatory
              query to be executed
          name : str, optional, default None
              custom name defined by the user for the job that is going to be created
          output_file : str, optional, default None
              file name where the results are saved if dumpToFile is True.
              If this parameter is not provided, the jobid is used instead
          output_format : str, optional, default 'votable'
              results format
          verbose : bool, optional, default 'False'
              flag to display information about the process
          dump_to_file : bool, optional, default 'False'
              if True, the results are saved in a file instead of using memory
          upload_resource : str, optional, default None
              resource to be uploaded to UPLOAD_SCHEMA
          upload_table_name : str, optional, default None
              resource temporary table name associated to the uploaded resource.
              This argument is required if upload_resource is provided.
          maxrec : int, optional, default None
              maximum number of rows to return (TAP ``MAXREC`` parameter)
  
          Returns
          -------
          A Job object
          """
          output_file_updated = taputils.get_suitable_output_file_name_for_current_output_format(output_file,
                                                                                                 output_format)
          query = taputils.set_top_in_query(query, 2000)
          if verbose:
              print(f"Launched query: '{query}'")
          if upload_resource is not None:
              if upload_table_name is None:
                  raise ValueError("Table name is required when a resource is uploaded")
              response = self.__launchJobMultipart(query=query,
                                                   uploadResource=upload_resource,
                                                   uploadTableName=upload_table_name,
                                                   outputFormat=output_format,
                                                   context="sync",
                                                   verbose=verbose,
                                                   name=name,
                                                   maxrec=maxrec)
          else:
              response = self.__launchJob(query=query,
                                          outputFormat=output_format,
                                          context="sync",
                                          verbose=verbose,
                                          name=name,
                                          maxrec=maxrec)
          # handle redirection
          if response.status == 303:
              # redirection
              if verbose:
                  print("Redirection found")
              location = self.__connHandler.find_header(
                  response.getheaders(),
                  "location")
              if location is None:
                  raise requests.exceptions.HTTPError("No location found "
                                                      "after redirection was "
                                                      "received (303)")
              if verbose:
                  print(f"Redirect to {location}")
              subcontext = self.__extract_sync_subcontext(location)
              response = self.__connHandler.execute_tapget(subcontext,
                                                           verbose=verbose)
          job = Job(async_job=False, query=query, connhandler=self.__connHandler)
          isError = self.__connHandler.check_launch_response_status(response,
                                                                    verbose,
                                                                    200,
                                                                    False)
          headers = response.getheaders()
          suitableOutputFile = taputils.get_suitable_output_file(self.__connHandler,
                                                                 False,
                                                                 output_file_updated,
                                                                 headers,
                                                                 isError,
                                                                 output_format)
          job.outputFile = suitableOutputFile
          job.outputFileUser = output_file
          job.parameters['format'] = output_format
          job.set_response_status(response.status, response.reason)
          job.set_phase('PENDING')
          if isError:
              job.failed = True
              job.set_phase('ERROR')
              responseBytes = response.read()
              responseStr = responseBytes.decode('utf-8')
              if dump_to_file:
                  if verbose:
                      print(f"Saving error to: {suitableOutputFile}")
                  self.__connHandler.dump_to_file(suitableOutputFile,
                                                  responseStr)
              raise requests.exceptions.HTTPError(
                  taputils.parse_http_response_error(responseStr,
                                                     response.status))
          else:
              if verbose:
                  print("Retrieving sync. results...")
              if dump_to_file:
                  if verbose:
                      print(f"Saving results to: {suitableOutputFile}")
                  self.__connHandler.dump_to_file(suitableOutputFile, response)
              else:
                  results = utils.read_http_response(response, output_format)
                  job.set_results(results)
              if verbose:
                  print("Query finished.")
              job._phase = 'COMPLETED'
          return 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,
                           autorun=True, maxrec=None):
          """Launches an asynchronous job
  
          Parameters
          ----------
          query : str, mandatory
              query to be executed
          name : str, optional, default None
              custom name defined by the user for the job that is going to be created
          output_file : str, optional, default None
              file name where the results are saved if dumpToFile is True.
              If this parameter is not provided, the jobid is used instead
          output_format : str, optional, default 'votable'
              results format
          verbose : bool, optional, default 'False'
              flag to display information about the process
          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, optional, default None
              resource temporary table name associated to the uploaded resource.
              This argument is required if upload_resource is provided.
          autorun : boolean, optional, default True
              if 'True', sets 'phase' parameter to 'RUN',
              so the framework can start the job.
          maxrec : int, optional, default None
              maximum number of rows to return (TAP ``MAXREC`` parameter)
  
          Returns
          -------
          A Job object
          """
  
          output_file_updated = taputils.get_suitable_output_file_name_for_current_output_format(output_file,
                                                                                                 output_format)
  
          if verbose:
              print(f"Launched query: '{query}'")
          if upload_resource is not None:
              if upload_table_name is None:
                  raise ValueError(
                      "Table name is required when a resource is uploaded")
              response = self.__launchJobMultipart(query,
                                                   upload_resource,
                                                   upload_table_name,
                                                   output_format,
                                                   "async",
                                                   verbose,
                                                   name,
                                                   autorun,
                                                   maxrec)
          else:
              response = self.__launchJob(query,
                                          output_format,
                                          "async",
                                          verbose,
                                          name,
                                          autorun,
                                          maxrec)
          isError = self.__connHandler.check_launch_response_status(response,
                                                                    verbose,
                                                                    303,
                                                                    False)
          job = Job(async_job=True, query=query, connhandler=self.__connHandler)
          headers = response.getheaders()
          suitableOutputFile = taputils.get_suitable_output_file(self.__connHandler,
                                                                 True,
                                                                 output_file_updated,
                                                                 headers,
                                                                 isError,
                                                                 output_format)
          job.outputFile = suitableOutputFile
          job.outputFileUser = output_file
          job.set_response_status(response.status, response.reason)
          job.parameters['format'] = output_format
          job.set_phase('PENDING')
          if isError:
              job.failed = True
              job.set_phase('ERROR')
              if dump_to_file:
                  if verbose:
                      print(f"Saving error to: {suitableOutputFile}")
                  self.__connHandler.dump_to_file(suitableOutputFile,
                                                  response)
              raise requests.exceptions.HTTPError(response.reason)
          else:
              location = self.__connHandler.find_header(
                  response.getheaders(),
                  "location")
              jobid = taputils.get_jobid_from_location(location)
              if verbose:
                  print(f"job {jobid}, at: {location}")
              job.jobid = jobid
              job.remoteLocation = location
              if autorun is True:
                  job.set_phase('EXECUTING')
                  if not background:
                      if verbose:
                          print("Retrieving async. results...")
                      # saveResults or getResults will block (not background)
                      if dump_to_file:
                          job.save_results(verbose)
                      else:
                          job.get_results()
                          log.info("Query finished.")
          return job
  
      def load_async_job(self, jobid=None, name=None, verbose=False,
                         load_results=True):
          """Loads an asynchronous job
  
          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
          load_results : bool, optional, default 'True'
              load results associated to this job
  
          Returns
          -------
          A Job object
          """
          if name is not None:
              jobfilter = Filter()
              jobfilter.add_filter('name', name)
              jobs = self.search_async_jobs(jobfilter)
              if jobs is None or len(jobs) < 1:
                  log.info(f"No job found for name '{name}'")
                  return None
              jobid = jobs[0].jobid
          if jobid is None:
              log.info("No job identifier found")
              return None
          subContext = f"async/{jobid}"
          response = self.__connHandler.execute_tapget(subContext,
                                                       verbose=verbose)
          if verbose:
              print(response.status, response.reason)
              print(response.getheaders())
          isError = self.__connHandler.check_launch_response_status(response,
                                                                    verbose,
                                                                    200)
          if isError:
              log.info(response.reason)
              raise requests.exceptions.HTTPError(response.reason)
              return None
          # parse job
          jsp = JobSaxParser(async_job=True)
          job = jsp.parseData(response)[0]
          job.connHandler = self.__connHandler
          # load resulst
          if load_results:
              job.get_results()
          return job
  
      def list_async_jobs(self, verbose=False):
          """Returns all the asynchronous jobs
  
          Parameters
          ----------
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          Returns
          -------
          A list of Job objects
          """
          subContext = "async"
          response = self.__connHandler.execute_tapget(subContext,
                                                       verbose=verbose)
          if verbose:
              print(response.status, response.reason)
              print(response.getheaders())
          isError = self.__connHandler.check_launch_response_status(response,
                                                                    verbose,
                                                                    200)
          if isError:
              log.info(response.reason)
              raise requests.exceptions.HTTPError(response.reason)
              return None
          # parse jobs
          jsp = JobListSaxParser(async_job=True)
          jobs = jsp.parseData(response)
          if jobs is not None:
              for j in jobs:
                  j.connHandler = self.__connHandler
          return jobs
  
      def __appendData(self, args):
          data = self.__connHandler.url_encode(args)
          result = ""
          firtsTime = True
          for k in data:
              if firtsTime:
                  firtsTime = False
                  result = f"{k}={data[k]}"
              else:
                  result = f"{result}&{k}={data[k]}"
          return result
  
      def save_results(self, job, verbose=False):
          """Saves job results
  
          Parameters
          ----------
          job : Job, mandatory
              job
          verbose : bool, optional, default 'False'
              flag to display information about the process
          """
          job.save_results(verbose=verbose)
  
      def __launchJobMultipart(self, query, uploadResource, uploadTableName,
                               outputFormat, context, verbose, name=None,
                               autorun=True, maxrec=None):
          uploadValue = f"{uploadTableName},param:{uploadTableName}"
          args = {
              "REQUEST": "doQuery",
              "LANG": "ADQL",
              "FORMAT": str(outputFormat),
              "tapclient": str(self.tap_client_id),
              "QUERY": str(query),
              "UPLOAD": "" + str(uploadValue)}
          if maxrec is not None:
              args['MAXREC'] = maxrec
          if autorun is True:
              args['PHASE'] = 'RUN'
          if name is not None:
              args['jobname'] = name
          if isinstance(uploadResource, Table):
              fh = tempfile.NamedTemporaryFile(delete=False)
              uploadResource.write(fh, format='votable')
              fh.close()
              f = open(fh.name, "r")
              chunk = f.read()
              f.close()
              os.unlink(fh.name)
              name = 'pytable'
              args['format'] = 'votable'
          else:
              with open(uploadResource, "r") as fh:
                  chunk = fh.read()
              name = os.path.basename(uploadResource)
          files = [[uploadTableName, name, chunk]]
          contentType, body = self.__connHandler.encode_multipart(args, files)
          response = self.__connHandler.execute_tappost(context,
                                                        body,
                                                        contentType,
                                                        verbose)
          if verbose:
              print(response.status, response.reason)
              print(response.getheaders())
          return response
  
      def __launchJob(self, query, outputFormat, context, verbose, name=None,
                      autorun=True, maxrec=None):
          args = {
              "REQUEST": "doQuery",
              "LANG": "ADQL",
              "FORMAT": str(outputFormat),
              "tapclient": str(self.tap_client_id),
              "QUERY": str(query)}
          if maxrec is not None:
              args['MAXREC'] = maxrec
          if autorun is True:
              args['PHASE'] = 'RUN'
          if name is not None:
              args['jobname'] = name
          data = self.__connHandler.url_encode(args)
          response = self.__connHandler.execute_tappost(subcontext=context,
                                                        data=data,
                                                        verbose=verbose)
          if verbose:
              print(response.status, response.reason)
              print(response.getheaders())
          return response
  
      def __extract_sync_subcontext(self, location):
          pos = location.find('sync')
          if pos < 0:
              return location
          return location[pos:]
  
      def __findCookieInHeader(self, headers, verbose=False):
          cookies = self.__connHandler.find_header(headers, 'Set-Cookie')
          if verbose:
              print(cookies)
          if cookies is None:
              return None
          else:
              items = cookies.split(';')
              for i in items:
                  if i.startswith("JSESSIONID="):
                      return i
          return None
  
      def __parseUrl(self, url, verbose=False):
          isHttps = False
          if url.startswith("https://"):
              isHttps = True
              protocol = "https"
          else:
              protocol = "http"
  
          if verbose:
              print(f"is https: {isHttps}")
  
          urlInfoPos = url.find("://")
  
          if urlInfoPos < 0:
              raise ValueError("Invalid URL format")
  
          urlInfo = url[(urlInfoPos + 3):]
  
          items = urlInfo.split("/")
  
          if verbose:
              print(f"'{urlInfo}'")
              for i in items:
                  print(f"'{i}'")
  
          itemsSize = len(items)
          hostPort = items[0]
          portPos = hostPort.find(":")
          if portPos > 0:
              # port found
              host = hostPort[0:portPos]
              port = int(hostPort[portPos + 1:])
          else:
              # no port found
              host = hostPort
              # no port specified: use defaults
              if isHttps:
                  port = 443
              else:
                  port = 80
  
          if itemsSize == 1:
              serverContext = ""
              tapContext = ""
          elif itemsSize == 2:
              serverContext = f"/{items[1]}"
              tapContext = ""
          elif itemsSize == 3:
              serverContext = f"/{items[1]}"
              tapContext = f"/{items[2]}"
          else:
              data = []
              for i in range(1, itemsSize - 1):
                  data.append(f"/{items[i]}")
              serverContext = utils.util_create_string_from_buffer(data)
              tapContext = f"/{items[itemsSize - 1]}"
          if verbose:
              print(f"protocol: '{protocol}'")
              print(f"host: '{host}'")
              print(f"port: '{port}'")
              print(f"server context: '{serverContext}'")
              print(f"tap context: '{tapContext}'")
          return protocol, host, port, serverContext, tapContext
  
      def __str__(self):
          return (f"Created TAP+ (v{VERSION}) - Connection:\n{self.__connHandler}")
  
  
  class TapPlus(Tap):
      """TAP plus class
      Provides TAP and TAP+ capabilities
      """
  
      def __init__(self, url=None,
                   host=None,
                   server_context=None,
                   tap_context=None,
                   port=80, sslport=443,
                   default_protocol_is_https=False,
                   connhandler=None,
                   upload_context=None,
                   table_edit_context=None,
                   data_context=None,
                   datalink_context=None,
                   verbose=False,
                   client_id=None):
          """Constructor
  
          Parameters
          ----------
          url : str, mandatory if no host is specified, default None
              TAP URL
          host : str, optional, default None
              host name
          server_context : str, optional, default None
              server context
          tap_context : str, optional, default None
              tap context
          upload_context : str, optional, default None
              upload context
          table_edit_context : str, optional, default None
              context for all actions to be performed over a existing table
          data_context : str, optional, default None
              data context
          datalink_context : str, optional, default None
              datalink context
          port : int, optional, default '80'
              HTTP port
          sslport : int, optional, default '443'
              HTTPS port
          default_protocol_is_https : bool, optional, default False
              Specifies whether the default protocol to be used is HTTPS
          connhandler : connection handler object, optional, default None
              HTTP(s) connection hander (creator). If no handler is provided, a
              new one is created.
          verbose : bool, optional, default 'True'
              flag to display information about the process
          """
  
          super(TapPlus, self).__init__(url, host,
                                        server_context=server_context,
                                        tap_context=tap_context,
                                        upload_context=upload_context,
                                        table_edit_context=table_edit_context,
                                        data_context=data_context,
                                        datalink_context=datalink_context,
                                        port=port, sslport=sslport,
                                        default_protocol_is_https=default_protocol_is_https,  # noqa
                                        connhandler=connhandler,
                                        verbose=verbose)
          self.__internalInit()
          self.__set_client_id(client_id=client_id)
  
      def __internalInit(self):
          self.__user = None
          self.__pwd = None
          self.__isLoggedIn = False
  
      def __set_client_id(self, client_id):
          if client_id:
              self.tap_client_id = client_id
  
      def load_tables(self, only_names=False, include_shared_tables=False,
                      verbose=False):
          """Loads all public tables
  
          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._Tap__load_tables(only_names=only_names,
                                        include_shared_tables=include_shared_tables,  # noqa
                                        verbose=verbose)
  
      def load_data(self, params_dict=None, output_file=None, verbose=False):
          """Loads the specified data
  
          Parameters
          ----------
          params_dict : dictionary, mandatory
              list of request parameters
          output_file : string, optional, default None
              file where the results are saved.
              If it is not provided, the http response contents are returned.
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          Returns
          -------
          A table object if output_file is None.
          None if output_file is not None.
          """
          if verbose:
              print("Retrieving data.")
          connHandler = self.__getconnhandler()
          if not isinstance(params_dict, dict):
              raise ValueError("Parameters dictionary expected")
          data = connHandler.url_encode(params_dict)
          if verbose:
              print(f"Data request: {data}")
          response = connHandler.execute_datapost(data=data, verbose=verbose)
          if verbose:
              print(response.status, response.reason)
          connHandler.check_launch_response_status(response,
                                                   verbose,
                                                   200)
          if verbose:
              print("Reading...")
          if output_file is not None:
              file = open(output_file, "wb")
              file.write(response.read())
              file.close()
              if verbose:
                  print("Done.")
              return None
          else:
              if 'format' in params_dict:
                  output_format = params_dict['format'].lower()
              else:
                  if 'FORMAT' in params_dict:
                      output_format = params_dict['FORMAT'].lower()
                  else:
                      output_format = "votable"
              results = utils.read_http_response(response, output_format)
              if verbose:
                  print("Done.")
              return results
  
      def load_groups(self, verbose=False):
          """Loads groups
  
          Parameters
          ----------
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          Returns
          -------
          A set of groups of a user
          """
          context = "share?action=GetGroups"
          connHandler = self.__getconnhandler()
          response = connHandler.execute_tapget(context, verbose=verbose)
          if verbose:
              print(response.status, response.reason)
              print(response.getheaders())
          connHandler.check_launch_response_status(response,
                                                   verbose,
                                                   200)
          if verbose:
              print("Parsing groups...")
          gsp = GroupSaxParser()
          gsp.parseData(response)
          log.info(f"Done. {len(gsp.get_groups())} groups found")
          if verbose:
              for g in gsp.get_groups():
                  print(g.title)
          return gsp.get_groups()
  
      def load_group(self, group_name=None, verbose=False):
          """Load group with title being group_name
  
          Parameters
          ----------
          group_name : str, required
              group to be loaded
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          Returns
          -------
          A group with title being group_name
          """
          if group_name is None:
              raise ValueError("'group_name' must be specified")
          groups = self.load_groups(verbose)
          group = None
          for g in groups:
              if str(g.title) == str(group_name):
                  group = g
                  break
          return group
  
      def load_shared_items(self, verbose=False):
          """Loads shared items
  
          Parameters
          ----------
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          Returns
          -------
          A set of shared items
          """
          context = "share?action=GetSharedItems"
          connHandler = self.__getconnhandler()
          response = connHandler.execute_tapget(context, verbose=verbose)
          if verbose:
              print(response.status, response.reason)
              print(response.getheaders())
          connHandler.check_launch_response_status(response,
                                                   verbose,
                                                   200)
          if verbose:
              print("Parsing shared items...")
          ssp = SharedItemsSaxParser()
          ssp.parseData(response)
          log.info(f"Done. {len(ssp.get_shared_items())} shared items found")
          if verbose:
              for g in ssp.get_shared_items():
                  print(g.title)
          return ssp.get_shared_items()
  
      def share_table(self, group_name=None,
                      table_name=None,
                      description=None,
                      verbose=False):
          """Shares a table with a group
  
          Parameters
          ----------
          group_name : str, required
              group in which table will be shared
          table_name : str, required
              table to be shared
          description : str, required
              description of the sharing
          verbose : bool, optional, default 'False'
              flag to display information about the process
          """
          if group_name is None or table_name is None:
              raise ValueError("Both 'group_name' and 'table_name' must be specified")
          if description is None:
              description = ""
          group = self.load_group(group_name, verbose)
          if group is None:
              raise ValueError(f"Group '{group_name}' not found.")
          table = self.load_table(table=table_name, verbose=verbose)
          if table is None:
              raise ValueError(f"Table '{table_name}' not found.")
          data = (f"action=CreateOrUpdateItem&resource_type=0&title="
                  f"{table_name}"
                  f"&description="
                  f"{description}"
                  f"&items_list="
                  f"{group.id}|Group|Read")
          connHandler = self.__getconnhandler()
          response = connHandler.execute_share(data, verbose=verbose)
          if verbose:
              print(response.status, response.reason)
              print(response.getheaders())
          connHandler.check_launch_response_status(response,
                                                   verbose,
                                                   200)
          msg = f"Shared table '{table_name}' to group '{group_name}'."
          log.info(msg)
  
      def share_table_stop(self, group_name=None, table_name=None,
                           verbose=False):
          """Stop sharing a table
  
          Parameters
          ----------
          group_name : str, required
              group where the table is shared to
          table_name : str, required
              table to be stopped from being shared
          verbose : bool, optional, default 'False'
              flag to display information about the process
          """
          if group_name is None or table_name is None:
              raise ValueError("Both 'group_name' and 'table_name' must be specified")
          group = self.load_group(group_name, verbose)
          if group is None:
              raise ValueError(f"Group '{group_name}' not found.")
          shared_items = self.load_shared_items(verbose)
          shared_item = None
          for s in shared_items:
              if str(s.title) == str(table_name):
                  # check group
                  groups = s.shared_to_items
                  for g in groups:
                      if group.id == g.id:
                          shared_item = s
                          break
                  if shared_item is not None:
                      break
          if shared_item is None:
              raise ValueError(f"Table '{table_name}', shared to group '{group_name}', not found.")
          data = (f"action=RemoveItem&resource_type=0&resource_id="
                  f"{shared_item.id}"
                  f"&resource_type=0")
          connHandler = self.__getconnhandler()
          response = connHandler.execute_share(data, verbose=verbose)
          if verbose:
              print(response.status, response.reason)
              print(response.getheaders())
          connHandler.check_launch_response_status(response,
                                                   verbose,
                                                   200)
  
          msg = f"Stop sharing table '{table_name}' to group '{group_name}'."
          log.info(msg)
  
      def share_group_create(self, group_name=None, description=None,
                             verbose=False):
          """Creates a group
  
          Parameters
          ----------
          group_name : str, required
              group to be created
          description : str, required
              description of the group
          verbose : bool, optional, default 'False'
              flag to display information about the process
          """
          if group_name is None:
              raise ValueError("'group_name' must be specified")
          if description is None:
              description = ""
          group = self.load_group(group_name, verbose)
          if group is not None:
              raise ValueError(f"Group {group_name} already exists")
          data = (f"action=CreateOrUpdateGroup&resource_type=0&title="
                  f"{group_name}"
                  f"&description="
                  f"{description}")
          connHandler = self.__getconnhandler()
          response = connHandler.execute_share(data, verbose=verbose)
          if verbose:
              print(response.status, response.reason)
              print(response.getheaders())
          connHandler.check_launch_response_status(response,
                                                   verbose,
                                                   200)
          msg = f"Created group '{group_name}'."
          log.info(msg)
  
      def share_group_delete(self,
                             group_name=None,
                             verbose=False):
          """Deletes a group
  
          Parameters
          ----------
          group_name : str, required
              group to be created
          verbose : bool, optional, default 'False'
              flag to display information about the process
          """
          if group_name is None:
              raise ValueError("'group_name' must be specified")
          group = self.load_group(group_name, verbose)
          if group is None:
              raise ValueError(f"Group '{group_name}' doesn't exist")
          data = (f"action=RemoveGroup&resource_type=0&group_id={group.id}")
          connHandler = self.__getconnhandler()
          response = connHandler.execute_share(data, verbose=verbose)
          if verbose:
              print(response.status, response.reason)
              print(response.getheaders())
          connHandler.check_launch_response_status(response,
                                                   verbose,
                                                   200)
          msg = f"Deleted group '{group_name}'."
          log.info(msg)
  
      def share_group_add_user(self,
                               group_name=None,
                               user_id=None,
                               verbose=False):
          """Adds user to a group
  
          Parameters
          ----------
          group_name : str, required
              group which user_id will be added in
          user_id : str, required
              user id to be added
          verbose : bool, optional, default 'False'
              flag to display information about the process
          """
          if group_name is None or user_id is None:
              raise ValueError("Both 'group_name' and 'user_id' must be specified")
          group = self.load_group(group_name, verbose)
          if group is None:
              raise ValueError(f"Group '{group_name}' doesn't exist")
          user_found_in_group = False
          for u in group.users:
              if str(u.id) == user_id:
                  user_found_in_group = True
                  break
          if user_found_in_group is True:
              raise ValueError(f"User id '{user_id}' found in group '{group_name}'")
          if self.is_valid_user(user_id, verbose) is False:
              raise ValueError(f"User id '{user_id}' not found.")
          users = ""
          for u in group.users:
              users = f"{users}{u.id},"
          users = users + user_id
          data = ("action=CreateOrUpdateGroup&group_id="
                  f"{group.id}&title="
                  f"{group.title}&description="
                  f"{group.description}&users_list="
                  f"{users}")
          connHandler = self.__getconnhandler()
          response = connHandler.execute_share(data, verbose=verbose)
          if verbose:
              print(response.status, response.reason)
              print(response.getheaders())
          connHandler.check_launch_response_status(response,
                                                   verbose,
                                                   200)
          msg = f"Added user '{user_id}' from group '{group_name}'."
          log.info(msg)
  
      def share_group_delete_user(self,
                                  group_name=None,
                                  user_id=None,
                                  verbose=False):
          """Deletes user from a group
  
          Parameters
          ----------
          group_name : str, required
              group which user_id will be removed from
          user_id : str, required
              user id to be deleted
          verbose : bool, optional, default 'False'
              flag to display information about the process
          """
          if group_name is None or user_id is None:
              raise ValueError("Both 'group_name' and 'user_id' must be specified")
          group = self.load_group(group_name, verbose)
          if group is None:
              raise ValueError(f"Group '{group_name}' doesn't exist")
          user_found_in_group = False
          for u in group.users:
              if str(u.id) == user_id:
                  user_found_in_group = True
                  break
          if user_found_in_group is False:
              raise ValueError(f"User id '{user_id}' not found in group '{group_name}'")
          users = ""
          for u in group.users:
              if str(u.id) == str(user_id):
                  continue
              users = f"{users}{u.id},"
          if str(users) != "":
              users = users[:-1]
          data = (f"action=CreateOrUpdateGroup&group_id="
                  f"{group.id}&title="
                  f"{group.title}&description="
                  f"{group.description}&users_list="
                  f"{users}")
          connHandler = self.__getconnhandler()
          response = connHandler.execute_share(data, verbose=verbose)
          if verbose:
              print(response.status, response.reason)
              print(response.getheaders())
          connHandler.check_launch_response_status(response,
                                                   verbose,
                                                   200)
          msg = f"Deleted user '{user_id}' from group '{group_name}'."
          log.info(msg)
  
      def is_valid_user(self, user_id=None, verbose=False):
          """Determines if the specified user exists in the system
          TAP+ only
  
          Parameters
          ----------
          user_id : str, mandatory
              user id to be checked
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          Returns
          -------
          Boolean indicating if the specified user exists
          """
          if user_id is None:
              raise ValueError("'user_id' must be specified")
          context = f"users?USER={user_id}"
          connHandler = self.__getconnhandler()
          response = connHandler.execute_tapget(context, verbose=verbose)
          if verbose:
              print(response.status, response.reason)
              print(response.getheaders())
          connHandler.check_launch_response_status(response,
                                                   verbose,
                                                   200)
          responseBytes = response.read()
          user = responseBytes.decode('utf-8')
          if verbose:
              print(f"USER response = {user}")
          return user.startswith(f"{user_id}:") and user.count("\\n") == 0
  
      def get_datalinks(self, ids, verbose=False):
          """Gets datalinks associated to the provided identifiers
  
          Parameters
          ----------
          ids : str list, mandatory
              list of identifiers
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          Returns
          -------
          A table object
          """
          if verbose:
              print("Retrieving datalink.")
          if ids is None:
              raise ValueError("Missing mandatory argument 'ids'")
          if isinstance(ids, str):
              ids_arg = f"ID={ids}"
          else:
              if isinstance(ids, int):
                  ids_arg = f"ID={ids}"
              else:
                  ids_arg = f"ID={','.join(str(item) for item in ids)}"
          if verbose:
              print(f"Datalink request: {ids_arg}")
          connHandler = self.__getconnhandler()
          response = connHandler.execute_datalinkpost(subcontext="links",
                                                      data=ids_arg,
                                                      verbose=verbose)
          if verbose:
              print(response.status, response.reason)
          connHandler.check_launch_response_status(response,
                                                   verbose,
                                                   200)
          if verbose:
              print("Done.")
          results = utils.read_http_response(response, "votable")
  
          return results
  
      def search_async_jobs(self, jobfilter=None, verbose=False):
          """Searches for jobs applying the specified filter
  
          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
          """
          # jobs/list?[&session=][&limit=][&offset=][&order=][&metadata_only=true|false]
          subContext = "jobs/async"
          if jobfilter is not None:
              data = jobfilter.createUrlRequest()
              if data is not None:
                  subContext = f"{subContext}?{self.__appendData(data)}"
          connHandler = self.__getconnhandler()
          response = connHandler.execute_tapget(subContext, verbose=verbose)
          if verbose:
              print(response.status, response.reason)
              print(response.getheaders())
          connHandler.check_launch_response_status(response,
                                                   verbose,
                                                   200)
          # parse jobs
          jsp = JobSaxParser(async_job=True)
          jobs = jsp.parseData(response)
          if jobs is not None:
              for j in jobs:
                  j.connHandler = connHandler
          return jobs
  
      def remove_jobs(self, jobs_list, verbose=False):
          """Removes the specified jobs
  
          Parameters
          ----------
          jobs_list : str, mandatory
              jobs identifiers to be removed
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          """
          if jobs_list is None:
              return
          jobsIds = None
          if isinstance(jobs_list, str):
              jobsIds = jobs_list
          elif isinstance(jobs_list, list):
              jobsIds = ','.join(jobs_list)
          else:
              raise Exception("Invalid object type")
          if verbose:
              print(f"Jobs to be removed: {jobsIds}")
          data = f"JOB_IDS={jobsIds}"
          subContext = "deletejobs"
          connHandler = self.__getconnhandler()
          response = connHandler.execute_tappost(subContext,
                                                 data,
                                                 verbose=verbose)
          if verbose:
              print(response.status, response.reason)
              print(response.getheaders())
          connHandler.check_launch_response_status(response,
                                                   verbose,
                                                   200)
          msg = f"Removed jobs: '{jobs_list}'."
          log.info(msg)
  
      def __execLogin(self, usr, pwd, verbose=False):
          subContext = "login"
          args = {
              "username": usr,
              "password": pwd}
          connHandler = self.__getconnhandler()
          data = connHandler.url_encode(args)
          response = connHandler.execute_secure(subContext, data, verbose)
          if verbose:
              print(response.status, response.reason)
              print(response.getheaders())
          return response
  
      def upload_table(self, upload_resource=None, table_name=None,
                       table_description=None,
                       format=None, verbose=False):
          """Uploads a table to the user private space
  
          Parameters
          ----------
          upload_resource : object, mandatory
              table to be uploaded: pyTable, file or URL.
          table_name : str, required if uploadResource is provided, default None
              resource temporary table name associated to the uploaded resource
          table_description : str, optional, default None
              table description
          format : str, optional, default 'VOTable'
              resource format
          verbose : bool, optional, default 'False'
              flag to display information about the process
          """
  
          if upload_resource is None:
              raise ValueError("Missing mandatory argument 'upload_resource'")
          if table_name is None:
              raise ValueError("Missing mandatory argument 'table_name'")
          if table_description is None:
              description = ""
          else:
              description = table_description
          if format is None:
              format = "votable"
  
          response = self.__uploadTableMultipart(resource=upload_resource,
                                                 table_name=table_name,
                                                 table_description=description,
                                                 resource_format=format,
                                                 verbose=verbose)
          if response.status == 303:
              location = self.__getconnhandler().find_header(
                  response.getheaders(),
                  "location")
              jobid = taputils.get_jobid_from_location(location)
              job = Job(async_job=True,
                        query=None,
                        connhandler=self.__getconnhandler())
              job.jobid = jobid
              job.name = 'Table upload'
              job.set_phase('EXECUTING')
              log.info(f"Job '{jobid}' created to upload table '{table_name}'.")
              return job
          else:
              log.info(f"Uploaded table '{table_name}'.")
              return None
  
      def __uploadTableMultipart(self, resource, table_name=None,
                                 table_description=None,
                                 resource_format="VOTable",
                                 verbose=False):
          connHandler = self.__getconnhandler()
          if isinstance(resource, Table):
              args = {
                  "TASKID": str(-1),
                  "TABLE_NAME": str(table_name),
                  "TABLE_DESC": str(table_description),
                  "FORMAT": 'votable'}
              log.info("Sending pytable.")
              fh = tempfile.NamedTemporaryFile(delete=False)
              resource.write(fh, format='votable')
              fh.close()
              f = open(fh.name, "r")
              chunk = f.read()
              f.close()
              os.unlink(fh.name)
              files = [['FILE', 'pytable', chunk]]
              contentType, body = connHandler.encode_multipart(args, files)
          else:
              if not (str(resource).startswith("http")):  # upload from file
                  args = {
                      "TASKID": str(-1),
                      "TABLE_NAME": str(table_name),
                      "TABLE_DESC": str(table_description),
                      "FORMAT": str(resource_format)}
                  log.info(f"Sending file: {resource}")
                  with open(resource, "r") as f:
                      chunk = f.read()
                  files = [['FILE', os.path.basename(resource), chunk]]
                  contentType, body = connHandler.encode_multipart(args, files)
              else:  # upload from URL
                  args = {
                      "TASKID": str(-1),
                      "TABLE_NAME": str(table_name),
                      "TABLE_DESC": str(table_description),
                      "FORMAT": str(resource_format),
                      "URL": str(resource)}
                  files = [['FILE', "", ""]]
                  contentType, body = connHandler.encode_multipart(args, files)
          response = connHandler.execute_upload(body, contentType)
          if verbose:
              print(response.status, response.reason)
              print(response.getheaders())
          if response.status != 303 and response.status != 302:
              connHandler.check_launch_response_status(response,
                                                       verbose,
                                                       200)
          return response
  
      def upload_table_from_job(self, job=None, table_name=None,
                                table_description=None, verbose=False):
          """Creates a table to the user private space from a job
  
          Parameters
          ----------
          job: job, mandatory
              job used to create a table. Could be a string with the jobid or
              a job itself
          table_name : str, default 't'+jobid
              resource temporary table name associated to the uploaded resource
          table_description : str, optional, default None
              table description
          verbose : bool, optional, default 'False'
              flag to display information about the process
          """
          if job is None:
              raise ValueError("Missing mandatory argument 'job'")
          if isinstance(job, Job):
              j = job
              description = j.parameters['query']
          else:
              j = self.load_async_job(jobid=job, load_results=False)
              if j is None:
                  raise ValueError(f"Job {job} not found")
                  return
              description = j.parameters['query']
          if table_name is None:
              table_name = f"t{j.jobid}"
          if table_description is None:
              table_description = description
          if verbose:
              print(f"JOB = {j.jobid}")
          self.__uploadTableMultipartFromJob(resource=j.jobid,
                                             table_name=table_name,
                                             table_description=table_description,
                                             verbose=verbose)
          msg = f"Created table '{table_name}' from job: '{j.jobid}'."
          log.info(msg)
  
      def __uploadTableMultipartFromJob(self, resource, table_name=None,
                                        table_description=None, verbose=False):
          args = {
              "TASKID": str(-1),
              "JOBID": str(resource),
              "TABLE_NAME": str(table_name),
              "TABLE_DESC": str(table_description),
              "FORMAT": str(format)}
          files = [['FILE', "", ""]]
          connHandler = self.__getconnhandler()
          contentType, body = connHandler.encode_multipart(args, files)
          response = connHandler.execute_upload(body, contentType)
          if verbose:
              print(response.status, response.reason)
              print(response.getheaders())
          connHandler.check_launch_response_status(response,
                                                   verbose,
                                                   200)
          return response
  
      def delete_user_table(self, table_name=None, force_removal=False,
                            verbose=False):
          """Removes a user table
  
          Parameters
          ----------
          table_name : str, required
              table to be removed
          force_removal : bool, optional, default 'False'
              flag to indicate if removal should be forced
          verbose : bool, optional, default 'False'
              flag to display information about the process
          """
          if table_name is None:
              raise ValueError("Table name cannot be null")
          if force_removal is True:
              args = {
                  "TABLE_NAME": str(table_name),
                  "DELETE": "TRUE",
                  "FORCE_REMOVAL": "TRUE"}
          else:
              args = {
                  "TABLE_NAME": str(table_name),
                  "DELETE": "TRUE",
                  "FORCE_REMOVAL": "FALSE"}
          connHandler = self.__getconnhandler()
          data = connHandler.url_encode(args)
          response = connHandler.execute_upload(data, verbose=verbose)
          if verbose:
              print(response.status, response.reason)
              print(response.getheaders())
          connHandler.check_launch_response_status(response,
                                                   verbose,
                                                   200)
          msg = f"Table '{table_name}' deleted."
          log.info(msg)
  
      def rename_table(self, table_name=None, new_table_name=None, new_column_names_dict=None,
                       verbose=False):
          """ This method allows you to update the column names of a user table.
  
          Parameters
          ----------
          table_name: str, required
              old name of the user's table
          new_table_name: str, required
              new name of the user's table
          new_column_names_dict: dict str:str
              dict with pairs "old_column1_name:new_column1_name"
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          """
          if new_column_names_dict is None:
              new_column_names_dict = {}
          args = {}
  
          if table_name is None:
              raise ValueError(
                  "Argument 'table_name' is mandatory. "
                  "Please introduce the name of the table that is going to be updated")
          if (new_table_name is None) and (new_column_names_dict is None):
              raise ValueError("Please introduce as minimum a new name for the table or a new name for a column with "
                               "format old_column1_name:new_column1_name, ... ,old_columnN_name:new_columnN_name")
          if new_table_name is not None:
              if new_column_names_dict is None:
                  # case 1: We only need to rename the table
                  args = self.get_args_4_rename_table(table_name, new_table_name)
              else:
                  # case 2: We need to rename both, table and column names
                  args = self.get_args_4_rename_table(table_name, new_table_name, new_column_names_dict)
  
          if new_table_name is None:
              if new_column_names_dict:
                  # case 3: We only need to rename the columns but same table name
                  args = self.get_args_4_rename_table(table_name, table_name, new_column_names_dict)
  
          connHandler = self.__getconnhandler()
          data = connHandler.url_encode(args)
          response = connHandler.execute_table_tool(data, verbose=verbose)
  
          if verbose:
              print(response.status, response.reason)
              print(response.getheaders())
          connHandler.check_launch_response_status(response,
                                                   verbose,
                                                   200)
          if verbose:
              msg = f"Table '{table_name}' updated."
              print(msg)
  
      def get_args_4_rename_table(self, table_name, new_table_name, new_column_names_dict):
  
          args = {}
  
          if not new_column_names_dict:
              args = {
                  "action": "rename",
                  "new_table_name": new_table_name,
                  "table_name": table_name
              }
          else:
              new_column_names = ','.join(f'{key}:{value}' for key, value in new_column_names_dict.items())
  
              args = {
                  "action": "rename",
                  "new_column_names": new_column_names,
                  "new_table_name": new_table_name,
                  "table_name": table_name
              }
          return args
  
      def update_user_table(self, table_name=None, list_of_changes=[],
                            verbose=False):
          """Updates a user table
  
          Parameters
          ----------
          table_name : str
              table to be updated
          list_of_changes : list
              list of lists, each one of them containing sets of
              [column_name, field_name, value].
              column_name is the name of the column to be updated
              field_name is the name of the tap field to be modified
              field name can be 'utype', 'ucd', 'flags' or 'indexed'
              value is the new value this field of this column will take
          verbose : bool, optional, default 'False'
              flag to display information about the process
          """
          if table_name is None:
              raise ValueError("Table name cannot be null")
          if len(list_of_changes) == 0:
              raise ValueError("List of changes cannot be empty")
          for change in list_of_changes:
              if change is None:
                  raise ValueError("None of the changes can be null")
              if len(change) != 3:  # [column_name, field_name, value]
                  raise ValueError("All of the changes must have three elements: [column_name, field_name, value]")
              index = 0
              for value in change:
                  if value is None:
                      raise ValueError("None of the values for the changes can be null")
                  if (index == 1 and value != 'utype' and value != 'ucd' and value != 'flags' and value != 'indexed'):
                      raise ValueError("Position 2 of all changes must be 'utype', 'ucd', 'flags' or 'indexed'")
                  index = index + 1
  
          table = self.load_table(table=table_name, verbose=verbose)
          if table is None:
              raise ValueError("Table name not found")
          columns = table.columns
          if len(columns) == 0:
              raise ValueError("Table has no columns")
  
          for change in list_of_changes:
              index = 0
              for value in change:
                  if index == 0:
                      found = False
                      for c in columns:
                          if c.name == value:
                              found = True
                              break
                      if found is False:
                          raise ValueError(f"Column name introduced "
                                           f"{value}"
                                           f" was not found in the table")
                  index = index + 1
  
          new_ra_column = TapPlus.__changesContainFlag(list_of_changes, "Ra")
          new_dec_column = TapPlus.__changesContainFlag(list_of_changes, "Dec")
  
          # check whether both (Ra/Dec) are present
          # or both are None
          if ((new_ra_column is not None and new_dec_column is None)
                  or (new_ra_column is None and new_dec_column is not None)):
              raise ValueError("Both Ra and Dec must be specified when updating one of them.")
  
          args = TapPlus.get_table_update_arguments(table_name, columns,
                                                    list_of_changes)
  
          connHandler = self.__getconnhandler()
          data = connHandler.url_encode(args)
          response = connHandler.execute_table_edit(data, verbose=verbose)
          if verbose:
              print(response.status, response.reason)
              print(response.getheaders())
          connHandler.check_launch_response_status(response,
                                                   verbose,
                                                   200)
          msg = f"Table '{table_name}' updated."
          log.info(msg)
  
      @staticmethod
      def get_table_update_arguments(table_name, columns, list_of_changes):
          num_cols = len(columns)
          args = {
              "ACTION": "edit",
              "NUMTABLES": str(1),
              "TABLE0_NUMCOLS": str(num_cols),
              "TABLE0": str(table_name),
          }
          index = 0
          for column in columns:
              found_in_changes = False
              for change in list_of_changes:
                  if (str(change[0]) == str(column.name)):
                      found_in_changes = True
                      break
  
              # set current values
              column_name, flags, indexed, ucd, utype = \
                  TapPlus.get_current_column_values_for_update(column)
  
              # Update values if required
              if found_in_changes:
                  flags, indexed, ucd, utype = \
                      TapPlus.get_new_column_values_for_update(list_of_changes,
                                                               column_name,
                                                               flags,
                                                               indexed,
                                                               ucd, utype)
  
              # Prepare http request parameters for a column
              args[f"TABLE0_COL{index}"] = str(column_name)
              args[f"TABLE0_COL{index}_UCD"] = str(ucd)
              args[f"TABLE0_COL{index}_UTYPE"] = str(utype)
              args[f"TABLE0_COL{index}_INDEXED"] = str(indexed)
              args[f"TABLE0_COL{index}_FLAGS"] = str(flags)
              index = index + 1
          return args
  
      @staticmethod
      def get_current_column_values_for_update(column):
          column_name = column.name
          flags = column.flags
          if str(flags) == '1':
              flags = 'Ra'
          elif str(flags) == '2':
              flags = 'Dec'
          elif str(flags) == '4':
              flags = 'Flux'
          elif str(flags) == '8':
              flags = 'Mag'
          elif str(flags) == '16':
              flags = 'PK'
          elif str(flags) == '33':
              flags = 'Ra'
          elif str(flags) == '34':
              flags = 'Dec'
          elif str(flags) == '38':
              flags = 'Flux'
          elif str(flags) == '40':
              flags = 'Mag'
          elif str(flags) == '48':
              flags = 'PK'
          else:
              flags = None
          indexed = (str(column.flag) == 'indexed'
                     or str(flags) == 'Ra'
                     or str(flags) == 'Dec'
                     or str(flags) == 'PK')
          ucd = str(column.ucd)
          utype = str(column.utype)
          return column_name, flags, indexed, ucd, utype
  
      @staticmethod
      def get_new_column_values_for_update(list_of_changes, column_name,
                                           c_flags, c_indexed, c_ucd, c_utype):
          found_new_flags = False
          found_new_indexed = False
          found_new_ucd = False
          found_new_utype = False
          n_flags = None
          n_indexed = None
          n_utype = None
          n_ucd = None
          for change in list_of_changes:
              if str(change[0]) == column_name:
                  if str(change[1]) == 'flags':
                      n_flags = str(change[2])
                      found_new_flags = True
                  if str(change[1]) == 'indexed':
                      n_indexed = str(change[2])
                      found_new_indexed = True
                  if str(change[1]) == 'ucd':
                      n_ucd = str(change[2])
                      found_new_ucd = True
                  if str(change[1]) == 'utype':
                      n_utype = str(change[2])
                      found_new_utype = True
  
          if found_new_ucd:
              ucd = n_ucd
          else:
              ucd = c_ucd
  
          if found_new_utype:
              utype = n_utype
          else:
              utype = c_utype
  
          if found_new_indexed:
              indexed = n_indexed
          else:
              indexed = c_indexed
  
          # index could be updated
          if found_new_flags:
              if n_flags is None or n_flags == '':
                  if found_new_indexed:
                      indexed = str(n_indexed)
                  else:
                      indexed = str(False)
              else:
                  # Index required for PK, Ra, Dec
                  if c_flags == 'Ra' or c_flags == 'Dec' or c_flags == 'PK':
                      indexed = str(True)
              flags = n_flags
          else:
              flags = c_flags
  
          return flags, indexed, ucd, utype
  
      def set_ra_dec_columns(self, table_name=None,
                             ra_column_name=None, dec_column_name=None,
                             verbose=False):
          """Set columns of a table as ra and dec respectively a user table
  
          Parameters
          ----------
          table_name : str
              table to be set
          ra_column_name : str
              ra column to be set
          dec_column_name : str
              dec column to be set
          verbose : bool, optional, default 'False'
              flag to display information about the process
          """
  
          if table_name is None:
              raise ValueError("Table name cannot be null")
          if ra_column_name is None:
              raise ValueError("Ra column name cannot be null")
          if dec_column_name is None:
              raise ValueError("Dec column name cannot be null")
  
          args = {
              "ACTION": "radec",
              "TABLE_NAME": str(table_name),
              "RA": str(ra_column_name),
              "DEC": str(dec_column_name),
          }
          connHandler = self.__getconnhandler()
          data = connHandler.url_encode(args)
          response = connHandler.execute_table_edit(data, verbose=verbose)
          isError = connHandler.check_launch_response_status(response,
                                                             verbose,
                                                             200)
          if isError:
              log.info(response.reason)
              raise requests.exceptions.HTTPError(response.reason)
          msg = f"Table '{table_name}' updated (ra/dec)."
          return msg
  
      def login(self, user=None, password=None, credentials_file=None,
                verbose=False):
          """Performs a login.
          User and password arguments 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).
          If no arguments are provided, a prompt asking for user name and
          password will appear.
  
          Parameters
          ----------
          user : str, default None
              login name
          password : str, default None
              user password
          credentials_file : str, default None
              file containing user and password in two lines
          verbose : bool, optional, default 'False'
              flag to display information about the process
          """
          if credentials_file is not None:
              # read file: get user & password
              with open(credentials_file, "r") as ins:
                  user = ins.readline().strip()
                  password = ins.readline().strip()
          if user is None:
              user = input("User: ")
              if user is None:
                  log.info("Invalid user name")
                  return
          if password is None:
              password = getpass.getpass("Password: ")
              if password is None:
                  log.info("Invalid password")
                  return
          self.__user = str(user)
          self.__pwd = str(password)
          self.__dologin(verbose)
  
      def login_gui(self, verbose=False):
          """Performs a login using a GUI dialog
  
          Parameters
          ----------
          verbose : bool, optional, default 'False'
              flag to display information about the process
          """
          connHandler = self.__getconnhandler()
          url = connHandler.get_host_url()
          loginDialog = LoginDialog(url)
          loginDialog.show_login()
          if loginDialog.is_accepted():
              self.__user = loginDialog.get_user()
              self.__pwd = loginDialog.get_password()
              # execute login
              self.__dologin(verbose)
          else:
              self.__isLoggedIn = False
  
      def __dologin(self, verbose=False):
          self.__isLoggedIn = False
          response = self.__execLogin(self.__user, self.__pwd, verbose)
          # check response
          connHandler = self.__getconnhandler()
          isError = connHandler.check_launch_response_status(response,
                                                             verbose,
                                                             200)
          if isError:
              log.info(f"Login error: {response.reason}")
              raise requests.exceptions.HTTPError(f"Login error: {response.reason}")
          else:
              # extract cookie
              cookie = self._Tap__findCookieInHeader(response.getheaders())
              if cookie is not None:
                  self.__isLoggedIn = True
                  connHandler.set_cookie(cookie)
          log.info("OK")
  
      def logout(self, verbose=False):
          """Performs a logout
  
          Parameters
          ----------
          verbose : bool, optional, default 'False'
              flag to display information about the process
          """
          subContext = "logout"
          args = {}
          connHandler = self.__getconnhandler()
          data = connHandler.url_encode(args)
          response = connHandler.execute_secure(subContext, data)
          if verbose:
              print(response.status, response.reason)
              print(response.getheaders())
          self.__isLoggedIn = False
  
      @staticmethod
      def __columnsContainFlag(columns=None, flag=None, verbose=False):
          c = None
          if columns is not None and len(columns) > 0:
              for column in columns:
                  f = column.flags
                  if str(f) == '1' or str(f) == '33':
                      f = 'Ra'
                  elif str(f) == '2' or str(f) == '34':
                      f = 'Dec'
                  elif str(f) == '4' or str(f) == '38':
                      f = 'Flux'
                  elif str(f) == '8' or str(f) == '40':
                      f = 'Mag'
                  elif str(f) == '16' or str(f) == '48':
                      f = 'PK'
                  else:
                      f = None
                  if str(flag) == str(f):
                      c = column.name
                      break
          return c
  
      @staticmethod
      def __changesContainFlag(changes=None, flag=None, verbose=False):
          c = None
          if changes is not None and len(changes) > 0:
              for change in changes:
                  if str(change[1]) == "flags":
                      value = str(change[2])
                      if str(flag) == str(value):
                          c = str(change[0])
                          break
          return c
  
      def __getconnhandler(self):
          return self._Tap__connHandler
  
files: astroquery/utils/tap/core.py
files: # Licensed under a 3-clause BSD style license - see LICENSE.rst
  """
  =============
  TAP plus
  =============
  
  @author: Juan Carlos Segovia
  @contact: juan.carlos.segovia@sciops.esa.int
  
  European Space Astronomy Centre (ESAC)
  European Space Agency (ESA)
  
  Created on 30 jun. 2016
  
  
  """
  
  import time
  from urllib.parse import urlencode
  
  from astroquery.utils.tap.model import modelutils
  from astroquery.utils.tap.xmlparser import utils
  from astroquery.utils.tap import taputils
  from astropy.logger import log
  import requests
  
  
  __all__ = ['Job']
  
  
  class Job:
      """Job class
      """
  
      def __init__(self, async_job, query=None, connhandler=None):
          """Constructor
  
          Parameters
          ----------
          async_job : bool, mandatory
              'True' if the job is asynchronous
          query : str, optional, default None
              Query
          connhandler : TapConn, optional, default None
              Connection handler
          """
          # async is a reserved keyword starting python 3.7
          self.async_ = async_job
          self.connHandler = None
          self.isFinished = None
          self.jobid = None
          self.remoteLocation = None
          # phase is actually indended to be private as get_phase is non-trivial
          self._phase = None
          self.outputFile = None
          self.outputFileUser = None
          self.responseStatus = 0
          self.responseMsg = None
          self.results = None
          self.__resultInMemory = False    # only used within class
          self.failed = False
          self.runid = None
          self.ownerid = None
          self.startTime = None
          self.endTime = None
          self.creationTime = None
          self.executionDuration = None
          self.destruction = None
          self.locationId = None
          self.name = None
          self.quote = None
  
          self.connHandler = connhandler
          self.parameters = {}
          self.parameters['query'] = query
          # default output format
          self.parameters['format'] = 'votable'
  
      def set_phase(self, phase):
          """Sets the job phase
  
          Parameters
          ----------
          phase : str, mandatory
              job phase
          """
          if self.is_finished():
              raise ValueError("Cannot assign a phase when a job is finished")
          self._phase = phase
  
      def start(self, verbose=False):
          """Starts the job (allowed in PENDING phase only)
  
          Parameters
          ----------
          verbose : bool, optional, default 'False'
              flag to display information about the process
          """
          self.__change_phase(phase="RUN", verbose=verbose)
  
      def abort(self, verbose=False):
          """Aborts the job (allowed in PENDING phase only)
  
          Parameters
          ----------
          verbose : bool, optional, default 'False'
              flag to display information about the process
          """
          self.__change_phase(phase="ABORT", verbose=verbose)
  
      def __change_phase(self, phase, verbose=False):
          if self._phase == 'PENDING':
              context = f"async/{self.jobid}/phase"
              response = self.connHandler.execute_tappost(
                  subcontext=context, data=urlencode({"PHASE": phase}), verbose=verbose
              )
              if verbose:
                  print(response.status, response.reason)
                  print(response.getheaders())
              self.__last_phase_response_status = response.status
              if phase == 'RUN':
                  # a request for RUN does not mean the server executes the job
                  phase = 'QUEUED'
                  if response.status != 200 and response.status != 303:
                      errMsg = taputils.get_http_response_error(response)
                      print(response.status, errMsg)
                      raise requests.exceptions.HTTPError(errMsg)
              else:
                  if response.status != 200:
                      errMsg = taputils.get_http_response_error(response)
                      print(response.status, errMsg)
                      raise requests.exceptions.HTTPError(errMsg)
              self._phase = phase
              return response
          else:
              raise ValueError(f"Cannot start a job in phase: {self._phase}")
  
      def send_parameter(self, name=None, value=None, verbose=False):
          """Sends a job parameter (allowed in PENDING phase only).
  
          Parameters
          ----------
          name : string
              Parameter name.
          value : string
              Parameter value.
          """
          if self._phase == 'PENDING':
              # send post parameter/value
              context = f"async/{self.jobid}"
              response = self.connHandler.execute_tappost(subcontext=context,
                                                          data=urlencode({name: value}),
                                                          verbose=verbose)
              if verbose:
                  print(response.status, response.reason)
                  print(response.getheaders())
              self.__last_phase_response_status = response.status
              if response.status != 200:
                  errMsg = taputils.get_http_response_error(response)
                  print(response.status, errMsg)
                  raise requests.exceptions.HTTPError(errMsg)
              return response
          else:
              raise ValueError(f"Cannot start a job in phase: {self._phase}")
  
      def get_phase(self, update=False):
          """Returns the job phase. May optionally update the job's phase.
  
          Parameters
          ----------
          update : bool
              if True, the phase will by updated by querying the server before
              returning.
  
          Returns
          -------
          The job phase
          """
          if update:
              phase_request = f"async/{self.jobid}/phase"
              response = self.connHandler.execute_tapget(phase_request)
  
              self.__last_phase_response_status = response.status
              if response.status != 200:
                  errMsg = taputils.get_http_response_error(response)
                  print(response.status, errMsg)
                  raise requests.exceptions.HTTPError(errMsg)
  
              self._phase = str(response.read().decode('utf-8'))
          return self._phase
  
      def set_response_status(self, status, msg):
          """Sets the HTTP(s) connection status
  
          Parameters
          ----------
          status : int, mandatory
              HTTP(s) response status
          msg : str, mandatory
              HTTP(s) response message
          """
          self.__responseStatus = status
          self.__responseMsg = msg
  
      def get_data(self):
          """Returns the job results (Astroquery API specification)
          This method will block if the job is asynchronous and the job has not
          finished yet.
  
          Returns
          -------
          The job results (astropy.table).
          """
          return self.get_results()
  
      def get_results(self):
          """Returns the job results
          This method will block if the job is asynchronous and the job has not
          finished yet.
  
          Returns
          -------
          The job results (astropy.table).
          """
          if self.results is not None:
              return self.results
          # try load results from file
          # read_results_table_from_file checks whether
          # the file already exists or not
          outputFormat = self.parameters['format']
          results = modelutils.read_results_table_from_file(self.outputFile,
                                                            outputFormat)
          if results is not None:
              self.results = results
              return results
          # Try to load from server: only async
          if not self.async_:
              # sync: result is in a file
              return None
          else:
              # async: result is in the server once the job is finished
              self.__load_async_job_results()
              return self.results
  
      def set_results(self, results):
          """Sets the job results
  
          Parameters
          ----------
          results : Table object, mandatory
              job results
          """
          self.results = results
          self.__resultInMemory = True
  
      def save_results(self, verbose=False):
          """Saves job results
          If the job is asynchronous, this method will block until the results
          are available.
  
          Parameters
          ----------
          verbose : bool, optional, default 'False'
              flag to display information about the process
          """
          if self.__resultInMemory:
              if verbose:
                  print(f"Saving results to: {self.outputFile}")
              self.results.to_xml(self.outputFile)
          else:
              if not self.async_:
                  # sync: cannot access server again
                  log.info("No results to save")
              else:
                  # Async
                  self.wait_for_job_end(verbose)
                  response = self.connHandler.execute_tapget(
                      f"async/{self.jobid}/results/result")
                  if verbose:
                      print(response.status, response.reason)
                      print(response.getheaders())
                  isError = self.connHandler.\
                      check_launch_response_status(response,
                                                   verbose,
                                                   200)
                  if isError:
                      print(response.reason)
                      raise Exception(response.reason)
                  if self.outputFileUser is None:
                      # User did not provide an output
                      # The output is a temporary one, analyse header
                      self.outputFile = taputils.get_suitable_output_file(
                          self.connHandler, True, None, response.getheaders(),
                          False, self.parameters['format'])
                      output = self.outputFile
                  else:
                      output = self.outputFileUser
                  if verbose:
                      print(f"Saving results to: {output}")
                  self.connHandler.dump_to_file(output, response)
  
      def wait_for_job_end(self, verbose=False):
          """Waits until a job is finished
  
          Parameters
          ----------
          verbose : bool, optional, default 'False'
              flag to display information about the process
          """
          currentResponse = None
          responseData = None
          lphase = None
          # execute job if not running
          if self._phase == 'PENDING':
              log.info("Job in PENDING phase, sending phase=RUN request.")
              try:
                  self.start(verbose)
              except Exception as ex:
                  # ignore
                  if verbose:
                      print("Exception when trying to start job", ex)
          while True:
              responseData = self.get_phase(update=True)
              currentResponse = self.__last_phase_response_status
  
              lphase = responseData.upper().strip()
              if verbose:
                  print(f"Job {self.jobid} status: {lphase}")
              if ("PENDING" != lphase and "QUEUED" != lphase and "EXECUTING" != lphase):
                  break
              # PENDING, QUEUED, EXECUTING, COMPLETED, ERROR, ABORTED, UNKNOWN,
              # HELD, SUSPENDED, ARCHIVED:
              time.sleep(0.5)
          return currentResponse, lphase
  
      def __load_async_job_results(self, debug=False):
          wjResponse, phase = self.wait_for_job_end()
          subContext = f"async/{self.jobid}/results/result"
          resultsResponse = self.connHandler.execute_tapget(subContext)
          # resultsResponse = self.__readAsyncResults(self.__jobid, debug)
          if debug:
              print(resultsResponse.status, resultsResponse.reason)
              print(resultsResponse.getheaders())
  
          resultsResponse = self.__handle_redirect_if_required(resultsResponse,
                                                               debug)
          isError = self.connHandler.\
              check_launch_response_status(resultsResponse,
                                           debug,
                                           200)
          self._phase = phase
          if phase == 'ERROR':
              errMsg = self.get_error(debug)
              raise SystemError(errMsg)
          else:
              if isError:
                  errMsg = taputils.get_http_response_error(resultsResponse)
                  print(resultsResponse.status, errMsg)
                  raise requests.exceptions.HTTPError(errMsg)
              else:
                  outputFormat = self.parameters['format']
                  results = utils.read_http_response(resultsResponse,
                                                     outputFormat)
                  self.set_results(results)
  
      def __handle_redirect_if_required(self, resultsResponse, verbose=False):
          # Thanks @emeraldTree24
          numberOfRedirects = 0
          while ((resultsResponse.status == 303 or resultsResponse.status == 302) and numberOfRedirects < 20):
              joblocation = self.connHandler.\
                  find_header(resultsResponse.getheaders(), "location")
              if verbose:
                  print(f"Redirecting to: {joblocation}")
              resultsResponse = self.connHandler.execute_tapget(joblocation)
              numberOfRedirects += 1
              if verbose:
                  print(resultsResponse.status, resultsResponse.reason)
                  print(resultsResponse.getheaders())
          return resultsResponse
  
      def get_error(self, verbose=False):
          """Returns the error associated to a job
  
          Parameters
          ----------
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          Returns
          -------
          The job error.
          """
          subContext = f"async/{self.jobid}/error"
          resultsResponse = self.connHandler.execute_tapget(subContext)
          # resultsResponse = self.__readAsyncResults(self.__jobid, debug)
          if verbose:
              print(resultsResponse.status, resultsResponse.reason)
              print(resultsResponse.getheaders())
          if (resultsResponse.status != 200 and resultsResponse.status != 303 and resultsResponse.status != 302):
              errMsg = taputils.get_http_response_error(resultsResponse)
              print(resultsResponse.status, errMsg)
              raise requests.exceptions.HTTPError(errMsg)
          else:
              if resultsResponse.status == 303 or resultsResponse.status == 302:
                  # get location
                  location = self.connHandler.\
                      find_header(resultsResponse.getheaders(), "location")
                  if location is None:
                      raise requests.exceptions.HTTPError("No location found after redirection was received (303)")
                  if verbose:
                      print(f"Redirect to {location}")
                  # load
                  relativeLocation = self.__extract_relative_location(location, self.jobid)
                  relativeLocationSubContext = f"async/{self.jobid}/{relativeLocation}"
                  response = self.connHandler.\
                      execute_tapget(relativeLocationSubContext)
                  response = self.__handle_redirect_if_required(response,
                                                                verbose)
                  isError = self.connHandler.\
                      check_launch_response_status(response, verbose, 200)
                  if isError:
                      errMsg = taputils.get_http_response_error(resultsResponse)
                      print(resultsResponse.status, errMsg)
                      raise requests.exceptions.HTTPError(errMsg)
              else:
                  response = resultsResponse
              errMsg = taputils.get_http_response_error(response)
          return errMsg
  
      def is_finished(self):
          """Returns whether the job is finished (ERROR, ABORTED, COMPLETED) or not
  
          """
          if (self._phase == 'ERROR' or self._phase == 'ABORTED' or self._phase == 'COMPLETED'):
              return True
          else:
              return False
  
      def __extract_relative_location(self, location, jobid):
          """Extracts uws subpath from location.
  
          Parameters
          ----------
          location : str, mandatory
              A 303 redirection header
  
          Returns
          -------
          The relative location.
          """
          pos = location.find(jobid)
          if pos < 0:
              return location
          pos += len(str(jobid))
          # skip '/'
          pos += 1
          return location[pos:]
  
      def __str__(self):
          if self.results is None:
              result = "None"
          else:
              result = self.results.info()
          return f"Jobid: {self.jobid}" \
              f"\nPhase: {self._phase}" \
              f"\nOwner: {self.ownerid}" \
              f"\nOutput file: {self.outputFile}" \
              f"\nResults: {result}"
  
files: astroquery/utils/tap/model/job.py
files: # Licensed under a 3-clause BSD style license - see LICENSE.rst
  """
  =============
  TAP plus
  =============
  
  @author: Juan Carlos Segovia
  @contact: juan.carlos.segovia@sciops.esa.int
  
  European Space Astronomy Centre (ESAC)
  European Space Agency (ESA)
  
  Created on 30 jun. 2016
  
  
  """
  
  import time
  
  from astroquery.utils.tap.model import modelutils
  from astroquery.utils.tap.xmlparser import utils
  from astroquery.utils.tap import taputils
  from astropy.logger import log
  import requests
  
  
  __all__ = ['Job']
  
  
  class Job:
      """Job class
      """
  
      def __init__(self, async_job, query=None, connhandler=None):
          """Constructor
  
          Parameters
          ----------
          async_job : bool, mandatory
              'True' if the job is asynchronous
          query : str, optional, default None
              Query
          connhandler : TapConn, optional, default None
              Connection handler
          """
          # async is a reserved keyword starting python 3.7
          self.async_ = async_job
          self.connHandler = None
          self.isFinished = None
          self.jobid = None
          self.remoteLocation = None
          # phase is actually indended to be private as get_phase is non-trivial
          self._phase = None
          self.outputFile = None
          self.outputFileUser = None
          self.responseStatus = 0
          self.responseMsg = None
          self.results = None
          self.__resultInMemory = False    # only used within class
          self.failed = False
          self.runid = None
          self.ownerid = None
          self.startTime = None
          self.endTime = None
          self.creationTime = None
          self.executionDuration = None
          self.destruction = None
          self.locationId = None
          self.name = None
          self.quote = None
  
          self.connHandler = connhandler
          self.parameters = {}
          self.parameters['query'] = query
          # default output format
          self.parameters['format'] = 'votable'
  
      def set_phase(self, phase):
          """Sets the job phase
  
          Parameters
          ----------
          phase : str, mandatory
              job phase
          """
          if self.is_finished():
              raise ValueError("Cannot assign a phase when a job is finished")
          self._phase = phase
  
      def start(self, verbose=False):
          """Starts the job (allowed in PENDING phase only)
  
          Parameters
          ----------
          verbose : bool, optional, default 'False'
              flag to display information about the process
          """
          self.__change_phase(phase="RUN", verbose=verbose)
  
      def abort(self, verbose=False):
          """Aborts the job (allowed in PENDING phase only)
  
          Parameters
          ----------
          verbose : bool, optional, default 'False'
              flag to display information about the process
          """
          self.__change_phase(phase="ABORT", verbose=verbose)
  
      def __change_phase(self, phase, verbose=False):
          if self._phase == 'PENDING':
              context = f"async/{self.jobid}/phase"
              args = {
                  "PHASE": str(phase)}
              data = self.connHandler.url_encode(args)
              response = self.connHandler.execute_tappost(subcontext=context,
                                                          data=data,
                                                          verbose=verbose)
              if verbose:
                  print(response.status, response.reason)
                  print(response.getheaders())
              self.__last_phase_response_status = response.status
              if phase == 'RUN':
                  # a request for RUN does not mean the server executes the job
                  phase = 'QUEUED'
                  if response.status != 200 and response.status != 303:
                      errMsg = taputils.get_http_response_error(response)
                      print(response.status, errMsg)
                      raise requests.exceptions.HTTPError(errMsg)
              else:
                  if response.status != 200:
                      errMsg = taputils.get_http_response_error(response)
                      print(response.status, errMsg)
                      raise requests.exceptions.HTTPError(errMsg)
              self._phase = phase
              return response
          else:
              raise ValueError(f"Cannot start a job in phase: {self._phase}")
  
      def send_parameter(self, name=None, value=None, verbose=False):
          """Sends a job parameter (allowed in PENDING phase only).
  
          Parameters
          ----------
          name : string
              Parameter name.
          value : string
              Parameter value.
          """
          if self._phase == 'PENDING':
              # send post parameter/value
              context = f"async/{self.jobid}"
              args = {
                  name: str(value)}
              data = self.connHandler.url_encode(args)
              response = self.connHandler.execute_tappost(subcontext=context,
                                                          data=data,
                                                          verbose=verbose)
              if verbose:
                  print(response.status, response.reason)
                  print(response.getheaders())
              self.__last_phase_response_status = response.status
              if response.status != 200:
                  errMsg = taputils.get_http_response_error(response)
                  print(response.status, errMsg)
                  raise requests.exceptions.HTTPError(errMsg)
              return response
          else:
              raise ValueError(f"Cannot start a job in phase: {self._phase}")
  
      def get_phase(self, update=False):
          """Returns the job phase. May optionally update the job's phase.
  
          Parameters
          ----------
          update : bool
              if True, the phase will by updated by querying the server before
              returning.
  
          Returns
          -------
          The job phase
          """
          if update:
              phase_request = f"async/{self.jobid}/phase"
              response = self.connHandler.execute_tapget(phase_request)
  
              self.__last_phase_response_status = response.status
              if response.status != 200:
                  errMsg = taputils.get_http_response_error(response)
                  print(response.status, errMsg)
                  raise requests.exceptions.HTTPError(errMsg)
  
              self._phase = str(response.read().decode('utf-8'))
          return self._phase
  
      def set_response_status(self, status, msg):
          """Sets the HTTP(s) connection status
  
          Parameters
          ----------
          status : int, mandatory
              HTTP(s) response status
          msg : str, mandatory
              HTTP(s) response message
          """
          self.__responseStatus = status
          self.__responseMsg = msg
  
      def get_data(self):
          """Returns the job results (Astroquery API specification)
          This method will block if the job is asynchronous and the job has not
          finished yet.
  
          Returns
          -------
          The job results (astropy.table).
          """
          return self.get_results()
  
      def get_results(self):
          """Returns the job results
          This method will block if the job is asynchronous and the job has not
          finished yet.
  
          Returns
          -------
          The job results (astropy.table).
          """
          if self.results is not None:
              return self.results
          # try load results from file
          # read_results_table_from_file checks whether
          # the file already exists or not
          outputFormat = self.parameters['format']
          results = modelutils.read_results_table_from_file(self.outputFile,
                                                            outputFormat)
          if results is not None:
              self.results = results
              return results
          # Try to load from server: only async
          if not self.async_:
              # sync: result is in a file
              return None
          else:
              # async: result is in the server once the job is finished
              self.__load_async_job_results()
              return self.results
  
      def set_results(self, results):
          """Sets the job results
  
          Parameters
          ----------
          results : Table object, mandatory
              job results
          """
          self.results = results
          self.__resultInMemory = True
  
      def save_results(self, verbose=False):
          """Saves job results
          If the job is asynchronous, this method will block until the results
          are available.
  
          Parameters
          ----------
          verbose : bool, optional, default 'False'
              flag to display information about the process
          """
          if self.__resultInMemory:
              if verbose:
                  print(f"Saving results to: {self.outputFile}")
              self.results.to_xml(self.outputFile)
          else:
              if not self.async_:
                  # sync: cannot access server again
                  log.info("No results to save")
              else:
                  # Async
                  self.wait_for_job_end(verbose)
                  response = self.connHandler.execute_tapget(
                      f"async/{self.jobid}/results/result")
                  if verbose:
                      print(response.status, response.reason)
                      print(response.getheaders())
                  isError = self.connHandler.\
                      check_launch_response_status(response,
                                                   verbose,
                                                   200)
                  if isError:
                      print(response.reason)
                      raise Exception(response.reason)
                  if self.outputFileUser is None:
                      # User did not provide an output
                      # The output is a temporary one, analyse header
                      self.outputFile = taputils.get_suitable_output_file(
                          self.connHandler, True, None, response.getheaders(),
                          False, self.parameters['format'])
                      output = self.outputFile
                  else:
                      output = self.outputFileUser
                  if verbose:
                      print(f"Saving results to: {output}")
                  self.connHandler.dump_to_file(output, response)
  
      def wait_for_job_end(self, verbose=False):
          """Waits until a job is finished
  
          Parameters
          ----------
          verbose : bool, optional, default 'False'
              flag to display information about the process
          """
          currentResponse = None
          responseData = None
          lphase = None
          # execute job if not running
          if self._phase == 'PENDING':
              log.info("Job in PENDING phase, sending phase=RUN request.")
              try:
                  self.start(verbose)
              except Exception as ex:
                  # ignore
                  if verbose:
                      print("Exception when trying to start job", ex)
          while True:
              responseData = self.get_phase(update=True)
              currentResponse = self.__last_phase_response_status
  
              lphase = responseData.upper().strip()
              if verbose:
                  print(f"Job {self.jobid} status: {lphase}")
              if ("PENDING" != lphase and "QUEUED" != lphase and "EXECUTING" != lphase):
                  break
              # PENDING, QUEUED, EXECUTING, COMPLETED, ERROR, ABORTED, UNKNOWN,
              # HELD, SUSPENDED, ARCHIVED:
              time.sleep(0.5)
          return currentResponse, lphase
  
      def __load_async_job_results(self, debug=False):
          wjResponse, phase = self.wait_for_job_end()
          subContext = f"async/{self.jobid}/results/result"
          resultsResponse = self.connHandler.execute_tapget(subContext)
          # resultsResponse = self.__readAsyncResults(self.__jobid, debug)
          if debug:
              print(resultsResponse.status, resultsResponse.reason)
              print(resultsResponse.getheaders())
  
          resultsResponse = self.__handle_redirect_if_required(resultsResponse,
                                                               debug)
          isError = self.connHandler.\
              check_launch_response_status(resultsResponse,
                                           debug,
                                           200)
          self._phase = phase
          if phase == 'ERROR':
              errMsg = self.get_error(debug)
              raise SystemError(errMsg)
          else:
              if isError:
                  errMsg = taputils.get_http_response_error(resultsResponse)
                  print(resultsResponse.status, errMsg)
                  raise requests.exceptions.HTTPError(errMsg)
              else:
                  outputFormat = self.parameters['format']
                  results = utils.read_http_response(resultsResponse,
                                                     outputFormat)
                  self.set_results(results)
  
      def __handle_redirect_if_required(self, resultsResponse, verbose=False):
          # Thanks @emeraldTree24
          numberOfRedirects = 0
          while ((resultsResponse.status == 303 or resultsResponse.status == 302) and numberOfRedirects < 20):
              joblocation = self.connHandler.\
                  find_header(resultsResponse.getheaders(), "location")
              if verbose:
                  print(f"Redirecting to: {joblocation}")
              resultsResponse = self.connHandler.execute_tapget(joblocation)
              numberOfRedirects += 1
              if verbose:
                  print(resultsResponse.status, resultsResponse.reason)
                  print(resultsResponse.getheaders())
          return resultsResponse
  
      def get_error(self, verbose=False):
          """Returns the error associated to a job
  
          Parameters
          ----------
          verbose : bool, optional, default 'False'
              flag to display information about the process
  
          Returns
          -------
          The job error.
          """
          subContext = f"async/{self.jobid}/error"
          resultsResponse = self.connHandler.execute_tapget(subContext)
          # resultsResponse = self.__readAsyncResults(self.__jobid, debug)
          if verbose:
              print(resultsResponse.status, resultsResponse.reason)
              print(resultsResponse.getheaders())
          if (resultsResponse.status != 200 and resultsResponse.status != 303 and resultsResponse.status != 302):
              errMsg = taputils.get_http_response_error(resultsResponse)
              print(resultsResponse.status, errMsg)
              raise requests.exceptions.HTTPError(errMsg)
          else:
              if resultsResponse.status == 303 or resultsResponse.status == 302:
                  # get location
                  location = self.connHandler.\
                      find_header(resultsResponse.getheaders(), "location")
                  if location is None:
                      raise requests.exceptions.HTTPError("No location found after redirection was received (303)")
                  if verbose:
                      print(f"Redirect to {location}")
                  # load
                  relativeLocation = self.__extract_relative_location(location, self.jobid)
                  relativeLocationSubContext = f"async/{self.jobid}/{relativeLocation}"
                  response = self.connHandler.\
                      execute_tapget(relativeLocationSubContext)
                  response = self.__handle_redirect_if_required(response,
                                                                verbose)
                  isError = self.connHandler.\
                      check_launch_response_status(response, verbose, 200)
                  if isError:
                      errMsg = taputils.get_http_response_error(resultsResponse)
                      print(resultsResponse.status, errMsg)
                      raise requests.exceptions.HTTPError(errMsg)
              else:
                  response = resultsResponse
              errMsg = taputils.get_http_response_error(response)
          return errMsg
  
      def is_finished(self):
          """Returns whether the job is finished (ERROR, ABORTED, COMPLETED) or not
  
          """
          if (self._phase == 'ERROR' or self._phase == 'ABORTED' or self._phase == 'COMPLETED'):
              return True
          else:
              return False
  
      def __extract_relative_location(self, location, jobid):
          """Extracts uws subpath from location.
  
          Parameters
          ----------
          location : str, mandatory
              A 303 redirection header
  
          Returns
          -------
          The relative location.
          """
          pos = location.find(jobid)
          if pos < 0:
              return location
          pos += len(str(jobid))
          # skip '/'
          pos += 1
          return location[pos:]
  
      def __str__(self):
          if self.results is None:
              result = "None"
          else:
              result = self.results.info()
          return f"Jobid: {self.jobid}" \
              f"\nPhase: {self._phase}" \
              f"\nOwner: {self.ownerid}" \
              f"\nOutput file: {self.outputFile}" \
              f"\nResults: {result}"
  
files: astroquery/utils/tap/model/job.py
files: # Licensed under a 3-clause BSD style license - see LICENSE.rst
  """
  =============
  TAP plus
  =============
  
  @author: Juan Carlos Segovia
  @contact: juan.carlos.segovia@sciops.esa.int
  
  European Space Astronomy Centre (ESAC)
  European Space Agency (ESA)
  
  Created on 30 jun. 2016
  """
  import os
  from unittest.mock import patch
  from urllib.parse import quote_plus, urlencode
  
  import numpy as np
  import pytest
  from requests import HTTPError
  
  from astroquery.utils.tap.model.tapcolumn import TapColumn
  
  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.utils.tap import taputils
  
  
  def data_path(filename):
      data_dir = os.path.join(os.path.dirname(__file__), 'data')
      return os.path.join(data_dir, filename)
  
  
  def test_load_tables():
      connHandler = DummyConnHandler()
      tap = TapPlus("http://test:1111/tap", connhandler=connHandler)
      responseLoadTable = DummyResponse(500)
      tableDataFile = data_path('test_tables.xml')
      tableData = utils.read_file_content(tableDataFile)
      responseLoadTable.set_data(method='GET', body=tableData)
      tableRequest = "tables"
      connHandler.set_response(tableRequest, responseLoadTable)
      with pytest.raises(Exception):
          tap.load_tables()
  
      responseLoadTable.set_status_code(200)
      res = tap.load_tables()
      assert len(res) == 2
  
      # Table 1
      table = __find_table('public', 'table1', res)
      assert table.description == 'Table1 desc'
      columns = table.columns
      assert len(columns) == 2
      col = __find_column('table1_col1', columns)
      __check_column(col, 'Table1 Column1 desc', '', 'VARCHAR', 'indexed')
      col = __find_column('table1_col2', columns)
      __check_column(col, 'Table1 Column2 desc', '', 'INTEGER', None)
  
      # Table 2
      table = __find_table('public', 'table2', res)
      assert table.description == 'Table2 desc'
      columns = table.columns
      assert len(columns) == 3
      col = __find_column('table2_col1', columns)
      __check_column(col, 'Table2 Column1 desc', '', 'VARCHAR', 'indexed')
      col = __find_column('table2_col2', columns)
      __check_column(col, 'Table2 Column2 desc', '', 'INTEGER', None)
      col = __find_column('table2_col3', columns)
      __check_column(col, 'Table2 Column3 desc', '', 'INTEGER', None)
  
  
  def test_load_tables_parameters():
      connHandler = DummyConnHandler()
      tap = TapPlus("http://test:1111/tap", connhandler=connHandler)
      responseLoadTable = DummyResponse(200)
      tableDataFile = data_path('test_tables.xml')
      tableData = utils.read_file_content(tableDataFile)
      responseLoadTable.set_data(method='GET', body=tableData)
      tableRequest = "tables"
      connHandler.set_response(tableRequest, responseLoadTable)
  
      # empty request
      tap.load_tables()
      request = connHandler.get_last_request()
      assert request == tableRequest
  
      # flag only_names=false & share_accessible=false: equals to
      # empty request
      tap.load_tables(only_names=False, include_shared_tables=False)
      request = connHandler.get_last_request()
      assert request == tableRequest
  
      # flag only_names
      tableRequest = "tables?only_tables=true"
      connHandler.set_response(tableRequest, responseLoadTable)
      tap.load_tables(only_names=True)
      request = connHandler.get_last_request()
      assert request == tableRequest
  
      # flag share_accessible=true
      tableRequest = "tables?share_accessible=true"
      connHandler.set_response(tableRequest, responseLoadTable)
      tap.load_tables(include_shared_tables=True)
      request = connHandler.get_last_request()
      assert request == tableRequest
  
      # flag only_names=true & share_accessible=true
      tableRequest = "tables?only_tables=true&share_accessible=true"
      connHandler.set_response(tableRequest, responseLoadTable)
      tap.load_tables(only_names=True, include_shared_tables=True)
      request = connHandler.get_last_request()
      assert request == tableRequest
  
  
  def test_load_table():
      connHandler = DummyConnHandler()
      tap = TapPlus("http://test:1111/tap", connhandler=connHandler)
  
      # No arguments
      with pytest.raises(Exception):
          tap.load_table()
  
      responseLoadTable = DummyResponse(500)
      tableDataFile = data_path('test_table1.xml')
      tableData = utils.read_file_content(tableDataFile)
      responseLoadTable.set_data(method='GET', body=tableData)
      tableSchema = "public"
      tableName = "table1"
      fullQualifiedTableName = f"{tableSchema}.{tableName}"
      tableRequest = f"tables?tables={fullQualifiedTableName}"
      connHandler.set_response(tableRequest, responseLoadTable)
  
      with pytest.raises(Exception):
          tap.load_table(fullQualifiedTableName)
  
      responseLoadTable.set_status_code(200)
      table = tap.load_table(fullQualifiedTableName)
      assert table is not None
      assert table.description == 'Table1 desc'
      columns = table.columns
      assert len(columns) == 2
      col = __find_column('table1_col1', columns)
      __check_column(col, 'Table1 Column1 desc', '', 'VARCHAR', 'indexed')
      col = __find_column('table1_col2', columns)
      __check_column(col, 'Table1 Column2 desc', '', 'INTEGER', None)
  
  
  def test_launch_sync_job():
      connHandler = DummyConnHandler()
      tap = TapPlus("http://test:1111/tap", connhandler=connHandler)
      responseLaunchJob = DummyResponse(500)
      jobDataFile = data_path('job_1.vot')
      jobData = utils.read_file_content(jobDataFile)
      responseLaunchJob.set_data(method='POST', body=jobData)
      query = 'select top 5 * from table'
      dictTmp = {
          "REQUEST": "doQuery",
          "LANG": "ADQL",
          "FORMAT": "votable",
          "tapclient": str(tap.tap_client_id),
          "PHASE": "RUN",
          "QUERY": quote_plus(query)}
      sortedKey = taputils.taputil_create_sorted_dict_key(dictTmp)
      jobRequest = f"sync?{sortedKey}"
      connHandler.set_response(jobRequest, responseLaunchJob)
  
      with pytest.raises(Exception):
          tap.launch_job(query, maxrec=10)
  
      responseLaunchJob.set_status_code(200)
  
      job = tap.launch_job(query)
  
      assert job is not None
      assert job.async_ is False
      assert job.get_phase() == 'COMPLETED'
      assert job.failed is False
  
      # results
      results = job.get_results()
      assert len(results) == 3
      __check_results_column(results,
                             'ra',
                             'ra',
                             None,
                             np.float64)
      __check_results_column(results,
                             'dec',
                             'dec',
                             None,
                             np.float64)
      __check_results_column(results,
                             'source_id',
                             'source_id',
                             None,
                             object)
      __check_results_column(results,
                             'table1_oid',
                             'table1_oid',
                             None,
                             np.int32)
  
  
  def test_launch_sync_job_redirect():
      connHandler = DummyConnHandler()
      tap = TapPlus("http://test:1111/tap", connhandler=connHandler)
      responseLaunchJob = DummyResponse(500)
      jobid = '12345'
      resultsReq = f'sync/{jobid}'
      resultsLocation = f'http://test:1111/tap/{resultsReq}'
      launchResponseHeaders = [
          ['location', resultsLocation]
      ]
      responseLaunchJob.set_data(method='POST')
      query = 'select top 5 * from table'
      dictTmp = {
          "REQUEST": "doQuery",
          "LANG": "ADQL",
          "FORMAT": "votable",
          "tapclient": str(tap.tap_client_id),
          "PHASE": "RUN",
          "QUERY": quote_plus(query)}
      sortedKey = taputils.taputil_create_sorted_dict_key(dictTmp)
      jobRequest = f"sync?{sortedKey}"
      connHandler.set_response(jobRequest, responseLaunchJob)
      # Results response
      responseResultsJob = DummyResponse(500)
      jobDataFile = data_path('job_1.vot')
      jobData = utils.read_file_content(jobDataFile)
      responseResultsJob.set_data(method='GET', body=jobData)
      connHandler.set_response(resultsReq, responseResultsJob)
  
      with pytest.raises(Exception):
          tap.launch_job(query)
  
      # Response is redirect (303)
      # No location available
      responseLaunchJob.set_status_code(303)
      with pytest.raises(Exception):
          tap.launch_job(query)
  
      # Response is redirect (303)
      # Location available
      # Results raises error (500)
      responseResultsJob.set_status_code(200)
      responseLaunchJob.set_data(method='POST', headers=launchResponseHeaders)
      responseResultsJob.set_status_code(500)
      with pytest.raises(Exception):
          tap.launch_job(query)
  
      # Response is redirect (303)
      # Results is 200
      # Location available
      responseResultsJob.set_status_code(200)
      job = tap.launch_job(query)
      assert job is not None
      assert job.async_ is False
      assert job.get_phase() == 'COMPLETED'
      assert job.failed is False
  
      # Results
      results = job.get_results()
      assert len(results) == 3
      __check_results_column(results,
                             'ra',
                             'ra',
                             None,
                             np.float64)
      __check_results_column(results,
                             'dec',
                             'dec',
                             None,
                             np.float64)
      __check_results_column(results,
                             'source_id',
                             'source_id',
                             None,
                             object)
      __check_results_column(results,
                             'table1_oid',
                             'table1_oid',
                             None,
                             np.int32)
  
  
  def test_launch_async_job():
      connHandler = DummyConnHandler()
      tap = TapPlus("http://test:1111/tap", connhandler=connHandler)
      jobid = '12345'
      # Launch response
      responseLaunchJob = DummyResponse(500)
      # list of list (httplib implementation for headers in response)
      launchResponseHeaders = [
          ['location', f'http://test:1111/tap/async/{jobid}']
      ]
      responseLaunchJob.set_data(method='POST', headers=launchResponseHeaders)
      query = 'query'
      dictTmp = {
          "REQUEST": "doQuery",
          "LANG": "ADQL",
          "FORMAT": "votable",
          "tapclient": str(tap.tap_client_id),
          "PHASE": "RUN",
          "QUERY": str(query)}
      sortedKey = taputils.taputil_create_sorted_dict_key(dictTmp)
      req = f"async?{sortedKey}"
      connHandler.set_response(req, responseLaunchJob)
      # Phase response
      responsePhase = DummyResponse(500)
      responsePhase.set_data(method='GET', body="COMPLETED")
      req = f"async/{jobid}/phase"
      connHandler.set_response(req, responsePhase)
      # Results response
      responseResultsJob = DummyResponse(500)
      jobDataFile = data_path('job_1.vot')
      jobData = utils.read_file_content(jobDataFile)
      responseResultsJob.set_data(method='GET', body=jobData)
      req = f"async/{jobid}/results/result"
      connHandler.set_response(req, responseResultsJob)
  
      with pytest.raises(Exception):
          tap.launch_job_async(query)
  
      responseLaunchJob.set_status_code(303)
      with pytest.raises(Exception):
          tap.launch_job_async(query)
  
      responsePhase.set_status_code(200)
      with pytest.raises(Exception):
          tap.launch_job_async(query)
  
      responseResultsJob.set_status_code(200)
      job = tap.launch_job_async(query)
      assert job is not None
      assert job.async_ is True
      assert job.get_phase() == 'COMPLETED'
      assert job.failed is False
  
      # results
      results = job.get_results()
      assert len(results) == 3
      __check_results_column(results,
                             'ra',
                             'ra',
                             None,
                             np.float64)
      __check_results_column(results,
                             'dec',
                             'dec',
                             None,
                             np.float64)
      __check_results_column(results,
                             'source_id',
                             'source_id',
                             None,
                             object)
      __check_results_column(results,
                             'table1_oid',
                             'table1_oid',
                             None,
                             np.int32)
  
  
  def test_start_job():
      connHandler = DummyConnHandler()
      tap = TapPlus("http://test:1111/tap", connhandler=connHandler)
      jobid = '12345'
      # Phase POST response
      responsePhase = DummyResponse(200)
      responsePhase.set_data(method='POST')
      req = f"async/{jobid}/phase?PHASE=RUN"
      connHandler.set_response(req, responsePhase)
      # Launch response
      responseLaunchJob = DummyResponse(303)
      # list of list (httplib implementation for headers in response)
      launchResponseHeaders = [
          ['location', f'http://test:1111/tap/async/{jobid}']
      ]
      responseLaunchJob.set_data(method='POST', headers=launchResponseHeaders)
      query = 'query'
      dictTmp = {
          "REQUEST": "doQuery",
          "LANG": "ADQL",
          "FORMAT": "votable",
          "tapclient": str(tap.tap_client_id),
          "QUERY": str(query)}
      sortedKey = taputils.taputil_create_sorted_dict_key(dictTmp)
      req = f"async?{sortedKey}"
      connHandler.set_response(req, responseLaunchJob)
      # Phase response
      responsePhase = DummyResponse(200)
      responsePhase.set_data(method='GET', body="COMPLETED")
      req = f"async/{jobid}/phase"
      connHandler.set_response(req, responsePhase)
      # Results response
      responseResultsJob = DummyResponse(200)
      jobDataFile = data_path('job_1.vot')
      jobData = utils.read_file_content(jobDataFile)
      responseResultsJob.set_data(method='GET', body=jobData)
      req = f"async/{jobid}/results/result"
      connHandler.set_response(req, responseResultsJob)
  
      responseResultsJob.set_status_code(200)
      job = tap.launch_job_async(query, autorun=False)
      assert job is not None
      assert job.get_phase() == 'PENDING'
  
      # start job
      job.start()
      assert job.get_phase() == 'QUEUED'
  
      # results
      results = job.get_results()
      assert len(results) == 3
      assert job.get_phase() == 'COMPLETED'
      # try to start again
      with pytest.raises(Exception):
          job.start()
  
  
  def test_abort_job():
      connHandler = DummyConnHandler()
      tap = TapPlus("http://test:1111/tap", connhandler=connHandler)
      jobid = '12345'
      # Phase POST response
      responsePhase = DummyResponse(200)
      responsePhase.set_data(method='POST')
      req = f"async/{jobid}/phase?PHASE=ABORT"
      connHandler.set_response(req, responsePhase)
      # Launch response
      responseLaunchJob = DummyResponse(303)
      # list of list (httplib implementation for headers in response)
      launchResponseHeaders = [
          ['location', f'http://test:1111/tap/async/{jobid}']
      ]
      responseLaunchJob.set_data(method='POST', headers=launchResponseHeaders)
      query = 'query'
      dictTmp = {
          "REQUEST": "doQuery",
          "LANG": "ADQL",
          "FORMAT": "votable",
          "MAXREC": 10,
          "tapclient": str(tap.tap_client_id),
          "QUERY": str(query)}
      sortedKey = taputils.taputil_create_sorted_dict_key(dictTmp)
      req = f"async?{sortedKey}"
      connHandler.set_response(req, responseLaunchJob)
  
      job = tap.launch_job_async(query, autorun=False, maxrec=10)
      assert job is not None
      assert job.get_phase() == 'PENDING'
      # abort job
      job.abort()
      assert job.get_phase() == 'ABORT'
      # try to abort again
      with pytest.raises(Exception):
          job.abort()
  
  
  def test_job_parameters():
      connHandler = DummyConnHandler()
      tap = TapPlus("http://test:1111/tap", connhandler=connHandler)
      jobid = '12345'
      # Launch response
      responseLaunchJob = DummyResponse(303)
      # list of list (httplib implementation for headers in response)
      launchResponseHeaders = [
          ['location', f'http://test:1111/tap/async/{jobid}']
      ]
      responseLaunchJob.set_data(method='POST', headers=launchResponseHeaders)
      query = 'query'
      dictTmp = {
          "REQUEST": "doQuery",
          "LANG": "ADQL",
          "FORMAT": "votable",
          "MAXREC": 10,
          "tapclient": str(tap.tap_client_id),
          "QUERY": str(query)}
      sortedKey = taputils.taputil_create_sorted_dict_key(dictTmp)
      req = f"async?{sortedKey}"
      connHandler.set_response(req, responseLaunchJob)
      # Phase response
      responsePhase = DummyResponse(200)
      responsePhase.set_data(method='GET', body="COMPLETED")
      req = f"async/{jobid}/phase"
      connHandler.set_response(req, responsePhase)
      # Results response
      responseResultsJob = DummyResponse(200)
      jobDataFile = data_path('job_1.vot')
      jobData = utils.read_file_content(jobDataFile)
      responseResultsJob.set_data(method='GET', body=jobData)
      req = f"async/{jobid}/results/result"
      connHandler.set_response(req, responseResultsJob)
  
      responseResultsJob.set_status_code(200)
      job = tap.launch_job_async(query, maxrec=10, autorun=False)
      assert job is not None
      assert job.get_phase() == 'PENDING'
  
      # parameter response
      responseParameters = DummyResponse(200)
      responseParameters.set_data(method='GET')
      req = f"async/{jobid}?param1=value1"
      connHandler.set_response(req, responseParameters)
      # Phase POST response
      responsePhase = DummyResponse(200)
      responsePhase.set_data(method='POST')
      req = f"async/{jobid}/phase?PHASE=RUN"
      connHandler.set_response(req, responsePhase)
  
      # send parameter OK
      job.send_parameter("param1", "value1")
      # start job
      job.start()
      assert job.get_phase() == 'QUEUED'
      # try to send a parameter after execution
      with pytest.raises(Exception):
          job.send_parameter("param2", "value2")
  
  
  def test_list_async_jobs():
      connHandler = DummyConnHandler()
      tap = TapPlus("http://test:1111/tap", connhandler=connHandler)
      response = DummyResponse(500)
      jobDataFile = data_path('jobs_list.xml')
      jobData = utils.read_file_content(jobDataFile)
      response.set_data(method='GET', body=jobData)
      req = "async"
      connHandler.set_response(req, response)
      with pytest.raises(Exception):
          tap.list_async_jobs()
  
      response.set_status_code(200)
      jobs = tap.list_async_jobs()
      assert len(jobs) == 2
      assert jobs[0].jobid == '12345'
      assert jobs[0].get_phase() == 'COMPLETED'
      assert jobs[1].jobid == '77777'
      assert jobs[1].get_phase() == 'ERROR'
  
  
  def test_data():
      connHandler = DummyConnHandler()
      tap = TapPlus("http://test:1111/tap",
                    data_context="data",
                    connhandler=connHandler)
      responseResultsJob = DummyResponse(200)
      jobDataFile = data_path('job_1.vot')
      jobData = utils.read_file_content(jobDataFile)
      responseResultsJob.set_data(method='GET', body=jobData)
      req = "?ID=1%2C2&format=votable"
      connHandler.set_response(req, responseResultsJob)
      req = "?ID=1%2C2"
      connHandler.set_response(req, responseResultsJob)
  
      # error
      responseResultsJob.set_status_code(500)
      params_dict = {}
      params_dict['ID'] = "1,2"
      with pytest.raises(Exception):
          tap.load_data(params_dict)
  
      # OK
      responseResultsJob.set_status_code(200)
  
      # results
      results = tap.load_data(params_dict)
      assert len(results) == 3
      # error: no params dictionary
      with pytest.raises(Exception):
          # no dictionary: exception
          tap.load_data("1,2")
      params_dict['format'] = "votable"
      results = tap.load_data(params_dict)
      assert len(results) == 3
  
  
  def test_datalink():
      connHandler = DummyConnHandler()
      tap = TapPlus("http://test:1111/tap",
                    datalink_context="datalink",
                    connhandler=connHandler)
      responseResultsJob = DummyResponse(200)
      jobDataFile = data_path('job_1.vot')
      jobData = utils.read_file_content(jobDataFile)
      responseResultsJob.set_data(method='GET', body=jobData)
      req = "links?ID=1,2"
      connHandler.set_response(req, responseResultsJob)
  
      # error
      responseResultsJob.set_status_code(500)
      with pytest.raises(Exception):
          # missing IDS parameter
          tap.get_datalinks(ids=None)
  
      # OK
      responseResultsJob.set_status_code(200)
      # results
      results = tap.get_datalinks("1,2")
      assert len(results) == 3
      results = tap.get_datalinks([1, 2])
      assert len(results) == 3
      results = tap.get_datalinks(['1', '2'])
      assert len(results) == 3
  
  
  def test_get_new_column_values_for_update():
      # Different column (no modifications)
      list_of_changes = [['column2', 'flags', 'Dec'],
                         ['column2', 'indexed', 'true'],
                         ['column2', 'ucd', 'new_ucd_column2'],
                         ['column2', 'utype', 'new_utype_column2']]
      column_name = 'column'
      c_flags = 'Ra'
      c_indexed = 'false'
      c_ucd = 'ucd'
      c_utype = 'utype_'
      flags, indexed, ucd, utype = TapPlus.get_new_column_values_for_update(
          list_of_changes, column_name, c_flags, c_indexed, c_ucd, c_utype)
      assert flags == c_flags
      assert indexed == c_indexed
      assert ucd == c_ucd
      assert utype == c_utype
  
      # Modifciations
      list_of_changes = [['column2', 'flags', 'Dec'],
                         ['column2', 'indexed', 'true'],
                         ['column2', 'ucd', 'new_ucd_column2'],
                         ['column2', 'utype', 'new_utype_column2'],
                         ['column', 'flags', 'Dec'],
                         ['column', 'indexed', 'true'],
                         ['column', 'ucd', 'new_ucd'],
                         ['column', 'utype', 'new_utype']]
      column_name = 'column'
      c_flags = 'Ra'
      c_indexed = 'false'
      c_ucd = 'ucd'
      c_utype = 'utype'
      n_flags = 'Dec'
      n_indexed = 'True'
      n_ucd = 'new_ucd'
      n_utype = 'new_utype'
      flags, indexed, ucd, utype = TapPlus.get_new_column_values_for_update(
          list_of_changes, column_name, c_flags, c_indexed, c_ucd, c_utype)
      assert flags == n_flags
      assert indexed == n_indexed
      assert ucd == n_ucd
      assert utype == n_utype
  
      # Previous value Ra/indexed, new value remove Ra/indexed
      list_of_changes = [['column', 'flags', ''],
                         ['column', 'indexed', 'false']]
      column_name = 'column'
      c_flags = 'Ra'
      c_indexed = 'true'
      n_flags = ''
      n_indexed = 'false'
      flags, indexed, ucd, utype = TapPlus.get_new_column_values_for_update(
          list_of_changes, column_name, c_flags, c_indexed, c_ucd, c_utype)
      assert flags == n_flags
      assert indexed == n_indexed
  
      # Test if flag is Ra, indexed is True
      list_of_changes = [['column', 'flags', 'Ra'],
                         ['column', 'indexed', 'false']]
      column_name = 'column'
      c_flags = 'Ra'
      c_indexed = 'true'
      n_flags = 'Ra'
      n_indexed = 'True'
      flags, indexed, ucd, utype = TapPlus.get_new_column_values_for_update(
          list_of_changes, column_name, c_flags, c_indexed, c_ucd, c_utype)
      assert flags == n_flags
      assert indexed == n_indexed
  
  
  def test_get_current_column_values_for_update():
      column = TapColumn(None)
      column.name = 'colname'
      column.flags = 0
      column.flag = None
      column.ucd = 'ucd'
      column.utype = 'utype'
      column_name, flags, indexed, ucd, utype = \
          TapPlus.get_current_column_values_for_update(column)
      assert column_name == 'colname'
      assert flags is None
      assert indexed is False
      assert ucd == 'ucd'
      assert utype == 'utype'
  
      column.flags = '1'
      column_name, flags, indexed, ucd, utype = \
          TapPlus.get_current_column_values_for_update(column)
      assert flags == 'Ra'
      assert indexed is True
      column.flags = '2'
      column_name, flags, indexed, ucd, utype = \
          TapPlus.get_current_column_values_for_update(column)
      assert flags == 'Dec'
      assert indexed is True
      column.flags = '4'
      column_name, flags, indexed, ucd, utype = \
          TapPlus.get_current_column_values_for_update(column)
      assert flags == 'Flux'
      assert indexed is False
      column.flags = '8'
      column_name, flags, indexed, ucd, utype = \
          TapPlus.get_current_column_values_for_update(column)
      assert flags == 'Mag'
      assert indexed is False
      column.flags = '16'
      column_name, flags, indexed, ucd, utype = \
          TapPlus.get_current_column_values_for_update(column)
      assert flags == 'PK'
      assert indexed is True
      column.flags = '33'
      column_name, flags, indexed, ucd, utype = \
          TapPlus.get_current_column_values_for_update(column)
      assert flags == 'Ra'
      assert indexed is True
      column.flags = '34'
      column_name, flags, indexed, ucd, utype = \
          TapPlus.get_current_column_values_for_update(column)
      assert flags == 'Dec'
      assert indexed is True
      column.flags = '38'
      column_name, flags, indexed, ucd, utype = \
          TapPlus.get_current_column_values_for_update(column)
      assert flags == 'Flux'
      assert indexed is False
      column.flags = '40'
      column_name, flags, indexed, ucd, utype = \
          TapPlus.get_current_column_values_for_update(column)
      assert flags == 'Mag'
      assert indexed is False
      column.flags = '48'
      column_name, flags, indexed, ucd, utype = \
          TapPlus.get_current_column_values_for_update(column)
      assert flags == 'PK'
      assert indexed is True
  
  
  def test_update_user_table():
      tableName = 'table'
      connHandler = DummyConnHandler()
      tap = TapPlus("http://test:1111/tap", connhandler=connHandler)
      dummyResponse = DummyResponse(200)
      tableDataFile = data_path('test_table_update.xml')
      tableData = utils.read_file_content(tableDataFile)
      dummyResponse.set_data(method='GET', body=tableData)
      tableRequest = f"tables?tables={tableName}"
      connHandler.set_response(tableRequest, dummyResponse)
  
      with pytest.raises(Exception):
          tap.update_user_table()
      with pytest.raises(Exception):
          tap.update_user_table(table_name=tableName)
      with pytest.raises(Exception):
          tap.update_user_table(table_name=tableName, list_of_changes=[])
      with pytest.raises(Exception):
          tap.update_user_table(table_name=tableName, list_of_changes=[[]])
      with pytest.raises(Exception):
          tap.update_user_table(table_name=tableName, list_of_changes=[['', '', '']])
  
      # Test Ra & Dec are provided
      list_of_changes = [['alpha', 'flags', 'Ra']]
      with pytest.raises(Exception):
          tap.update_user_table(table_name=tableName, list_of_changes=list_of_changes)
      list_of_changes = [['delta', 'flags', 'Dec']]
      with pytest.raises(Exception):
          tap.update_user_table(table_name=tableName, list_of_changes=list_of_changes)
  
      # OK
      responseEditTable = DummyResponse(200)
      dictTmp = {
          "ACTION": "edit",
          "NUMTABLES": "1",
          "TABLE0": tableName,
          "TABLE0_COL0": "table6_oid",
          "TABLE0_COL0_FLAGS": "PK",
          "TABLE0_COL0_INDEXED": "True",
          "TABLE0_COL0_UCD": "",
          "TABLE0_COL0_UTYPE": "",
          "TABLE0_COL1": "source_id",
          "TABLE0_COL1_FLAGS": "None",
          "TABLE0_COL1_INDEXED": "False",
          "TABLE0_COL1_UCD": "None",
          "TABLE0_COL1_UTYPE": "None",
          "TABLE0_COL2": "alpha",
          "TABLE0_COL2_FLAGS": "Ra",
          "TABLE0_COL2_INDEXED": "False",
          "TABLE0_COL2_UCD": "",
          "TABLE0_COL2_UTYPE": "",
          "TABLE0_COL3": "delta",
          "TABLE0_COL3_FLAGS": "Dec",
          "TABLE0_COL3_INDEXED": "False",
          "TABLE0_COL3_UCD": "",
          "TABLE0_COL3_UTYPE": "",
          "TABLE0_NUMCOLS": "4"
      }
      sortedKey = taputils.taputil_create_sorted_dict_key(dictTmp)
      req = f"tableEdit?{sortedKey}"
      connHandler.set_response(req, responseEditTable)
  
      list_of_changes = [['alpha', 'flags', 'Ra'], ['delta', 'flags', 'Dec']]
      tap.update_user_table(table_name=tableName, list_of_changes=list_of_changes)
  
  
  def test_rename_table():
      tableName = 'user_test.table_test_rename'
      newTableName = 'user_test.table_test_rename_new'
      newColumnNames = {'ra': 'alpha', 'dec': 'delta'}
      connHandler = DummyConnHandler()
      tap = TapPlus("http://test:1111/tap", connhandler=connHandler)
      dummyResponse = DummyResponse(200)
      tableDataFile = data_path('test_table_rename.xml')
      tableData = utils.read_file_content(tableDataFile)
      dummyResponse.set_data(method='GET', body=tableData)
  
      with pytest.raises(Exception):
          tap.rename_table()
      with pytest.raises(Exception):
          tap.rename_table(table_name=tableName)
      with pytest.raises(Exception):
          tap.rename_table(table_name=tableName, new_table_name=None, new_column_names_dict=None)
  
      # Test OK.
      responseRenameTable = DummyResponse(200)
      dictArgs = {
          "action": "rename",
          "new_column_names": "ra:alpha,dec:delta",
          "new_table_name": newTableName,
          "table_name": tableName,
      }
      connHandler.set_response(f"TableTool?{urlencode(dictArgs)}", responseRenameTable)
      tap.rename_table(table_name=tableName, new_table_name=newTableName, new_column_names_dict=newColumnNames)
  
  
  def __find_table(schemaName, tableName, tables):
      qualifiedName = f"{schemaName}.{tableName}"
      for table in (tables):
          if table.get_qualified_name() == qualifiedName:
              return table
      # not found: raise exception
      pytest.fail(f"Table '{qualifiedName}' not found")
  
  
  def __find_column(columnName, columns):
      for c in (columns):
          if c.name == columnName:
              return c
      # not found: raise exception
      pytest.fail(f"Column '{columnName}' not found")
  
  
  def __check_column(column, description, unit, dataType, flag):
      assert column.description == description
      assert column.unit == unit
      assert column.data_type == dataType
      assert column.flag == flag
  
  
  def __check_results_column(results, columnName, description, unit,
                             dataType):
      c = results[columnName]
      assert c.description == description
      assert c.unit == unit
      assert c.dtype == dataType
  
  
  @patch.object(TapPlus, 'login')
  def test_login(mock_login):
      conn_handler = DummyConnHandler()
      tap = TapPlus("http://test:1111/tap", connhandler=conn_handler)
      tap.login("user", "password")
      assert (mock_login.call_count == 1)
      mock_login.side_effect = HTTPError("Login error")
      with pytest.raises(HTTPError):
          tap.login("user", "password")
      assert (mock_login.call_count == 2)
  
  
  @patch.object(TapPlus, 'login_gui')
  @patch.object(TapPlus, 'login')
  def test_login_gui(mock_login_gui, mock_login):
      conn_handler = DummyConnHandler()
      tap = TapPlus("http://test:1111/tap", connhandler=conn_handler)
      tap.login_gui()
      assert (mock_login_gui.call_count == 0)
      mock_login_gui.side_effect = HTTPError("Login error")
      with pytest.raises(HTTPError):
          tap.login("user", "password")
      assert (mock_login.call_count == 1)
  
  
  @patch.object(TapPlus, 'logout')
  def test_logout(mock_logout):
      conn_handler = DummyConnHandler()
      tap = TapPlus("http://test:1111/tap", connhandler=conn_handler)
      tap.logout()
      assert (mock_logout.call_count == 1)
      mock_logout.side_effect = HTTPError("Login error")
      with pytest.raises(HTTPError):
          tap.logout()
      assert (mock_logout.call_count == 2)
  
files: astroquery/utils/tap/tests/test_tap.py
files: # Licensed under a 3-clause BSD style license - see LICENSE.rst
  """
  =============
  TAP plus
  =============
  
  @author: Juan Carlos Segovia
  @contact: juan.carlos.segovia@sciops.esa.int
  
  European Space Astronomy Centre (ESAC)
  European Space Agency (ESA)
  
  Created on 30 jun. 2016
  """
  import os
  from unittest.mock import patch
  
  import numpy as np
  import pytest
  from requests import HTTPError
  
  from astroquery.utils.tap.model.tapcolumn import TapColumn
  
  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.utils.tap import taputils
  
  
  def data_path(filename):
      data_dir = os.path.join(os.path.dirname(__file__), 'data')
      return os.path.join(data_dir, filename)
  
  
  def test_load_tables():
      connHandler = DummyConnHandler()
      tap = TapPlus("http://test:1111/tap", connhandler=connHandler)
      responseLoadTable = DummyResponse(500)
      tableDataFile = data_path('test_tables.xml')
      tableData = utils.read_file_content(tableDataFile)
      responseLoadTable.set_data(method='GET', body=tableData)
      tableRequest = "tables"
      connHandler.set_response(tableRequest, responseLoadTable)
      with pytest.raises(Exception):
          tap.load_tables()
  
      responseLoadTable.set_status_code(200)
      res = tap.load_tables()
      assert len(res) == 2
  
      # Table 1
      table = __find_table('public', 'table1', res)
      assert table.description == 'Table1 desc'
      columns = table.columns
      assert len(columns) == 2
      col = __find_column('table1_col1', columns)
      __check_column(col, 'Table1 Column1 desc', '', 'VARCHAR', 'indexed')
      col = __find_column('table1_col2', columns)
      __check_column(col, 'Table1 Column2 desc', '', 'INTEGER', None)
  
      # Table 2
      table = __find_table('public', 'table2', res)
      assert table.description == 'Table2 desc'
      columns = table.columns
      assert len(columns) == 3
      col = __find_column('table2_col1', columns)
      __check_column(col, 'Table2 Column1 desc', '', 'VARCHAR', 'indexed')
      col = __find_column('table2_col2', columns)
      __check_column(col, 'Table2 Column2 desc', '', 'INTEGER', None)
      col = __find_column('table2_col3', columns)
      __check_column(col, 'Table2 Column3 desc', '', 'INTEGER', None)
  
  
  def test_load_tables_parameters():
      connHandler = DummyConnHandler()
      tap = TapPlus("http://test:1111/tap", connhandler=connHandler)
      responseLoadTable = DummyResponse(200)
      tableDataFile = data_path('test_tables.xml')
      tableData = utils.read_file_content(tableDataFile)
      responseLoadTable.set_data(method='GET', body=tableData)
      tableRequest = "tables"
      connHandler.set_response(tableRequest, responseLoadTable)
  
      # empty request
      tap.load_tables()
      request = connHandler.get_last_request()
      assert request == tableRequest
  
      # flag only_names=false & share_accessible=false: equals to
      # empty request
      tap.load_tables(only_names=False, include_shared_tables=False)
      request = connHandler.get_last_request()
      assert request == tableRequest
  
      # flag only_names
      tableRequest = "tables?only_tables=true"
      connHandler.set_response(tableRequest, responseLoadTable)
      tap.load_tables(only_names=True)
      request = connHandler.get_last_request()
      assert request == tableRequest
  
      # flag share_accessible=true
      tableRequest = "tables?share_accessible=true"
      connHandler.set_response(tableRequest, responseLoadTable)
      tap.load_tables(include_shared_tables=True)
      request = connHandler.get_last_request()
      assert request == tableRequest
  
      # flag only_names=true & share_accessible=true
      tableRequest = "tables?only_tables=true&share_accessible=true"
      connHandler.set_response(tableRequest, responseLoadTable)
      tap.load_tables(only_names=True, include_shared_tables=True)
      request = connHandler.get_last_request()
      assert request == tableRequest
  
  
  def test_load_table():
      connHandler = DummyConnHandler()
      tap = TapPlus("http://test:1111/tap", connhandler=connHandler)
  
      # No arguments
      with pytest.raises(Exception):
          tap.load_table()
  
      responseLoadTable = DummyResponse(500)
      tableDataFile = data_path('test_table1.xml')
      tableData = utils.read_file_content(tableDataFile)
      responseLoadTable.set_data(method='GET', body=tableData)
      tableSchema = "public"
      tableName = "table1"
      fullQualifiedTableName = f"{tableSchema}.{tableName}"
      tableRequest = f"tables?tables={fullQualifiedTableName}"
      connHandler.set_response(tableRequest, responseLoadTable)
  
      with pytest.raises(Exception):
          tap.load_table(fullQualifiedTableName)
  
      responseLoadTable.set_status_code(200)
      table = tap.load_table(fullQualifiedTableName)
      assert table is not None
      assert table.description == 'Table1 desc'
      columns = table.columns
      assert len(columns) == 2
      col = __find_column('table1_col1', columns)
      __check_column(col, 'Table1 Column1 desc', '', 'VARCHAR', 'indexed')
      col = __find_column('table1_col2', columns)
      __check_column(col, 'Table1 Column2 desc', '', 'INTEGER', None)
  
  
  def test_launch_sync_job():
      connHandler = DummyConnHandler()
      tap = TapPlus("http://test:1111/tap", connhandler=connHandler)
      responseLaunchJob = DummyResponse(500)
      jobDataFile = data_path('job_1.vot')
      jobData = utils.read_file_content(jobDataFile)
      responseLaunchJob.set_data(method='POST', body=jobData)
      query = 'select top 5 * from table'
      dTmp = {"q": query}
      dTmpEncoded = connHandler.url_encode(dTmp)
      p = dTmpEncoded.find("=")
      q = dTmpEncoded[p + 1:]
      dictTmp = {
          "REQUEST": "doQuery",
          "LANG": "ADQL",
          "FORMAT": "votable",
          "tapclient": str(tap.tap_client_id),
          "PHASE": "RUN",
          "QUERY": str(q)}
      sortedKey = taputils.taputil_create_sorted_dict_key(dictTmp)
      jobRequest = f"sync?{sortedKey}"
      connHandler.set_response(jobRequest, responseLaunchJob)
  
      with pytest.raises(Exception):
          tap.launch_job(query, maxrec=10)
  
      responseLaunchJob.set_status_code(200)
  
      job = tap.launch_job(query)
  
      assert job is not None
      assert job.async_ is False
      assert job.get_phase() == 'COMPLETED'
      assert job.failed is False
  
      # results
      results = job.get_results()
      assert len(results) == 3
      __check_results_column(results,
                             'ra',
                             'ra',
                             None,
                             np.float64)
      __check_results_column(results,
                             'dec',
                             'dec',
                             None,
                             np.float64)
      __check_results_column(results,
                             'source_id',
                             'source_id',
                             None,
                             object)
      __check_results_column(results,
                             'table1_oid',
                             'table1_oid',
                             None,
                             np.int32)
  
  
  def test_launch_sync_job_redirect():
      connHandler = DummyConnHandler()
      tap = TapPlus("http://test:1111/tap", connhandler=connHandler)
      responseLaunchJob = DummyResponse(500)
      jobid = '12345'
      resultsReq = f'sync/{jobid}'
      resultsLocation = f'http://test:1111/tap/{resultsReq}'
      launchResponseHeaders = [
          ['location', resultsLocation]
      ]
      responseLaunchJob.set_data(method='POST')
      query = 'select top 5 * from table'
      dTmp = {"q": query}
      dTmpEncoded = connHandler.url_encode(dTmp)
      p = dTmpEncoded.find("=")
      q = dTmpEncoded[p + 1:]
      dictTmp = {
          "REQUEST": "doQuery",
          "LANG": "ADQL",
          "FORMAT": "votable",
          "tapclient": str(tap.tap_client_id),
          "PHASE": "RUN",
          "QUERY": str(q)}
      sortedKey = taputils.taputil_create_sorted_dict_key(dictTmp)
      jobRequest = f"sync?{sortedKey}"
      connHandler.set_response(jobRequest, responseLaunchJob)
      # Results response
      responseResultsJob = DummyResponse(500)
      jobDataFile = data_path('job_1.vot')
      jobData = utils.read_file_content(jobDataFile)
      responseResultsJob.set_data(method='GET', body=jobData)
      connHandler.set_response(resultsReq, responseResultsJob)
  
      with pytest.raises(Exception):
          tap.launch_job(query)
  
      # Response is redirect (303)
      # No location available
      responseLaunchJob.set_status_code(303)
      with pytest.raises(Exception):
          tap.launch_job(query)
  
      # Response is redirect (303)
      # Location available
      # Results raises error (500)
      responseResultsJob.set_status_code(200)
      responseLaunchJob.set_data(method='POST', headers=launchResponseHeaders)
      responseResultsJob.set_status_code(500)
      with pytest.raises(Exception):
          tap.launch_job(query)
  
      # Response is redirect (303)
      # Results is 200
      # Location available
      responseResultsJob.set_status_code(200)
      job = tap.launch_job(query)
      assert job is not None
      assert job.async_ is False
      assert job.get_phase() == 'COMPLETED'
      assert job.failed is False
  
      # Results
      results = job.get_results()
      assert len(results) == 3
      __check_results_column(results,
                             'ra',
                             'ra',
                             None,
                             np.float64)
      __check_results_column(results,
                             'dec',
                             'dec',
                             None,
                             np.float64)
      __check_results_column(results,
                             'source_id',
                             'source_id',
                             None,
                             object)
      __check_results_column(results,
                             'table1_oid',
                             'table1_oid',
                             None,
                             np.int32)
  
  
  def test_launch_async_job():
      connHandler = DummyConnHandler()
      tap = TapPlus("http://test:1111/tap", connhandler=connHandler)
      jobid = '12345'
      # Launch response
      responseLaunchJob = DummyResponse(500)
      # list of list (httplib implementation for headers in response)
      launchResponseHeaders = [
          ['location', f'http://test:1111/tap/async/{jobid}']
      ]
      responseLaunchJob.set_data(method='POST', headers=launchResponseHeaders)
      query = 'query'
      dictTmp = {
          "REQUEST": "doQuery",
          "LANG": "ADQL",
          "FORMAT": "votable",
          "tapclient": str(tap.tap_client_id),
          "PHASE": "RUN",
          "QUERY": str(query)}
      sortedKey = taputils.taputil_create_sorted_dict_key(dictTmp)
      req = f"async?{sortedKey}"
      connHandler.set_response(req, responseLaunchJob)
      # Phase response
      responsePhase = DummyResponse(500)
      responsePhase.set_data(method='GET', body="COMPLETED")
      req = f"async/{jobid}/phase"
      connHandler.set_response(req, responsePhase)
      # Results response
      responseResultsJob = DummyResponse(500)
      jobDataFile = data_path('job_1.vot')
      jobData = utils.read_file_content(jobDataFile)
      responseResultsJob.set_data(method='GET', body=jobData)
      req = f"async/{jobid}/results/result"
      connHandler.set_response(req, responseResultsJob)
  
      with pytest.raises(Exception):
          tap.launch_job_async(query)
  
      responseLaunchJob.set_status_code(303)
      with pytest.raises(Exception):
          tap.launch_job_async(query)
  
      responsePhase.set_status_code(200)
      with pytest.raises(Exception):
          tap.launch_job_async(query)
  
      responseResultsJob.set_status_code(200)
      job = tap.launch_job_async(query)
      assert job is not None
      assert job.async_ is True
      assert job.get_phase() == 'COMPLETED'
      assert job.failed is False
  
      # results
      results = job.get_results()
      assert len(results) == 3
      __check_results_column(results,
                             'ra',
                             'ra',
                             None,
                             np.float64)
      __check_results_column(results,
                             'dec',
                             'dec',
                             None,
                             np.float64)
      __check_results_column(results,
                             'source_id',
                             'source_id',
                             None,
                             object)
      __check_results_column(results,
                             'table1_oid',
                             'table1_oid',
                             None,
                             np.int32)
  
  
  def test_start_job():
      connHandler = DummyConnHandler()
      tap = TapPlus("http://test:1111/tap", connhandler=connHandler)
      jobid = '12345'
      # Phase POST response
      responsePhase = DummyResponse(200)
      responsePhase.set_data(method='POST')
      req = f"async/{jobid}/phase?PHASE=RUN"
      connHandler.set_response(req, responsePhase)
      # Launch response
      responseLaunchJob = DummyResponse(303)
      # list of list (httplib implementation for headers in response)
      launchResponseHeaders = [
          ['location', f'http://test:1111/tap/async/{jobid}']
      ]
      responseLaunchJob.set_data(method='POST', headers=launchResponseHeaders)
      query = 'query'
      dictTmp = {
          "REQUEST": "doQuery",
          "LANG": "ADQL",
          "FORMAT": "votable",
          "tapclient": str(tap.tap_client_id),
          "QUERY": str(query)}
      sortedKey = taputils.taputil_create_sorted_dict_key(dictTmp)
      req = f"async?{sortedKey}"
      connHandler.set_response(req, responseLaunchJob)
      # Phase response
      responsePhase = DummyResponse(200)
      responsePhase.set_data(method='GET', body="COMPLETED")
      req = f"async/{jobid}/phase"
      connHandler.set_response(req, responsePhase)
      # Results response
      responseResultsJob = DummyResponse(200)
      jobDataFile = data_path('job_1.vot')
      jobData = utils.read_file_content(jobDataFile)
      responseResultsJob.set_data(method='GET', body=jobData)
      req = f"async/{jobid}/results/result"
      connHandler.set_response(req, responseResultsJob)
  
      responseResultsJob.set_status_code(200)
      job = tap.launch_job_async(query, autorun=False)
      assert job is not None
      assert job.get_phase() == 'PENDING'
  
      # start job
      job.start()
      assert job.get_phase() == 'QUEUED'
  
      # results
      results = job.get_results()
      assert len(results) == 3
      assert job.get_phase() == 'COMPLETED'
      # try to start again
      with pytest.raises(Exception):
          job.start()
  
  
  def test_abort_job():
      connHandler = DummyConnHandler()
      tap = TapPlus("http://test:1111/tap", connhandler=connHandler)
      jobid = '12345'
      # Phase POST response
      responsePhase = DummyResponse(200)
      responsePhase.set_data(method='POST')
      req = f"async/{jobid}/phase?PHASE=ABORT"
      connHandler.set_response(req, responsePhase)
      # Launch response
      responseLaunchJob = DummyResponse(303)
      # list of list (httplib implementation for headers in response)
      launchResponseHeaders = [
          ['location', f'http://test:1111/tap/async/{jobid}']
      ]
      responseLaunchJob.set_data(method='POST', headers=launchResponseHeaders)
      query = 'query'
      dictTmp = {
          "REQUEST": "doQuery",
          "LANG": "ADQL",
          "FORMAT": "votable",
          "MAXREC": 10,
          "tapclient": str(tap.tap_client_id),
          "QUERY": str(query)}
      sortedKey = taputils.taputil_create_sorted_dict_key(dictTmp)
      req = f"async?{sortedKey}"
      connHandler.set_response(req, responseLaunchJob)
  
      job = tap.launch_job_async(query, autorun=False, maxrec=10)
      assert job is not None
      assert job.get_phase() == 'PENDING'
      # abort job
      job.abort()
      assert job.get_phase() == 'ABORT'
      # try to abort again
      with pytest.raises(Exception):
          job.abort()
  
  
  def test_job_parameters():
      connHandler = DummyConnHandler()
      tap = TapPlus("http://test:1111/tap", connhandler=connHandler)
      jobid = '12345'
      # Launch response
      responseLaunchJob = DummyResponse(303)
      # list of list (httplib implementation for headers in response)
      launchResponseHeaders = [
          ['location', f'http://test:1111/tap/async/{jobid}']
      ]
      responseLaunchJob.set_data(method='POST', headers=launchResponseHeaders)
      query = 'query'
      dictTmp = {
          "REQUEST": "doQuery",
          "LANG": "ADQL",
          "FORMAT": "votable",
          "MAXREC": 10,
          "tapclient": str(tap.tap_client_id),
          "QUERY": str(query)}
      sortedKey = taputils.taputil_create_sorted_dict_key(dictTmp)
      req = f"async?{sortedKey}"
      connHandler.set_response(req, responseLaunchJob)
      # Phase response
      responsePhase = DummyResponse(200)
      responsePhase.set_data(method='GET', body="COMPLETED")
      req = f"async/{jobid}/phase"
      connHandler.set_response(req, responsePhase)
      # Results response
      responseResultsJob = DummyResponse(200)
      jobDataFile = data_path('job_1.vot')
      jobData = utils.read_file_content(jobDataFile)
      responseResultsJob.set_data(method='GET', body=jobData)
      req = f"async/{jobid}/results/result"
      connHandler.set_response(req, responseResultsJob)
  
      responseResultsJob.set_status_code(200)
      job = tap.launch_job_async(query, maxrec=10, autorun=False)
      assert job is not None
      assert job.get_phase() == 'PENDING'
  
      # parameter response
      responseParameters = DummyResponse(200)
      responseParameters.set_data(method='GET')
      req = f"async/{jobid}?param1=value1"
      connHandler.set_response(req, responseParameters)
      # Phase POST response
      responsePhase = DummyResponse(200)
      responsePhase.set_data(method='POST')
      req = f"async/{jobid}/phase?PHASE=RUN"
      connHandler.set_response(req, responsePhase)
  
      # send parameter OK
      job.send_parameter("param1", "value1")
      # start job
      job.start()
      assert job.get_phase() == 'QUEUED'
      # try to send a parameter after execution
      with pytest.raises(Exception):
          job.send_parameter("param2", "value2")
  
  
  def test_list_async_jobs():
      connHandler = DummyConnHandler()
      tap = TapPlus("http://test:1111/tap", connhandler=connHandler)
      response = DummyResponse(500)
      jobDataFile = data_path('jobs_list.xml')
      jobData = utils.read_file_content(jobDataFile)
      response.set_data(method='GET', body=jobData)
      req = "async"
      connHandler.set_response(req, response)
      with pytest.raises(Exception):
          tap.list_async_jobs()
  
      response.set_status_code(200)
      jobs = tap.list_async_jobs()
      assert len(jobs) == 2
      assert jobs[0].jobid == '12345'
      assert jobs[0].get_phase() == 'COMPLETED'
      assert jobs[1].jobid == '77777'
      assert jobs[1].get_phase() == 'ERROR'
  
  
  def test_data():
      connHandler = DummyConnHandler()
      tap = TapPlus("http://test:1111/tap",
                    data_context="data",
                    connhandler=connHandler)
      responseResultsJob = DummyResponse(200)
      jobDataFile = data_path('job_1.vot')
      jobData = utils.read_file_content(jobDataFile)
      responseResultsJob.set_data(method='GET', body=jobData)
      req = "?ID=1%2C2&format=votable"
      connHandler.set_response(req, responseResultsJob)
      req = "?ID=1%2C2"
      connHandler.set_response(req, responseResultsJob)
  
      # error
      responseResultsJob.set_status_code(500)
      params_dict = {}
      params_dict['ID'] = "1,2"
      with pytest.raises(Exception):
          tap.load_data(params_dict)
  
      # OK
      responseResultsJob.set_status_code(200)
  
      # results
      results = tap.load_data(params_dict)
      assert len(results) == 3
      # error: no params dictionary
      with pytest.raises(Exception):
          # no dictionary: exception
          tap.load_data("1,2")
      params_dict['format'] = "votable"
      results = tap.load_data(params_dict)
      assert len(results) == 3
  
  
  def test_datalink():
      connHandler = DummyConnHandler()
      tap = TapPlus("http://test:1111/tap",
                    datalink_context="datalink",
                    connhandler=connHandler)
      responseResultsJob = DummyResponse(200)
      jobDataFile = data_path('job_1.vot')
      jobData = utils.read_file_content(jobDataFile)
      responseResultsJob.set_data(method='GET', body=jobData)
      req = "links?ID=1,2"
      connHandler.set_response(req, responseResultsJob)
  
      # error
      responseResultsJob.set_status_code(500)
      with pytest.raises(Exception):
          # missing IDS parameter
          tap.get_datalinks(ids=None)
  
      # OK
      responseResultsJob.set_status_code(200)
      # results
      results = tap.get_datalinks("1,2")
      assert len(results) == 3
      results = tap.get_datalinks([1, 2])
      assert len(results) == 3
      results = tap.get_datalinks(['1', '2'])
      assert len(results) == 3
  
  
  def test_get_new_column_values_for_update():
      # Different column (no modifications)
      list_of_changes = [['column2', 'flags', 'Dec'],
                         ['column2', 'indexed', 'true'],
                         ['column2', 'ucd', 'new_ucd_column2'],
                         ['column2', 'utype', 'new_utype_column2']]
      column_name = 'column'
      c_flags = 'Ra'
      c_indexed = 'false'
      c_ucd = 'ucd'
      c_utype = 'utype_'
      flags, indexed, ucd, utype = TapPlus.get_new_column_values_for_update(
          list_of_changes, column_name, c_flags, c_indexed, c_ucd, c_utype)
      assert flags == c_flags
      assert indexed == c_indexed
      assert ucd == c_ucd
      assert utype == c_utype
  
      # Modifciations
      list_of_changes = [['column2', 'flags', 'Dec'],
                         ['column2', 'indexed', 'true'],
                         ['column2', 'ucd', 'new_ucd_column2'],
                         ['column2', 'utype', 'new_utype_column2'],
                         ['column', 'flags', 'Dec'],
                         ['column', 'indexed', 'true'],
                         ['column', 'ucd', 'new_ucd'],
                         ['column', 'utype', 'new_utype']]
      column_name = 'column'
      c_flags = 'Ra'
      c_indexed = 'false'
      c_ucd = 'ucd'
      c_utype = 'utype'
      n_flags = 'Dec'
      n_indexed = 'True'
      n_ucd = 'new_ucd'
      n_utype = 'new_utype'
      flags, indexed, ucd, utype = TapPlus.get_new_column_values_for_update(
          list_of_changes, column_name, c_flags, c_indexed, c_ucd, c_utype)
      assert flags == n_flags
      assert indexed == n_indexed
      assert ucd == n_ucd
      assert utype == n_utype
  
      # Previous value Ra/indexed, new value remove Ra/indexed
      list_of_changes = [['column', 'flags', ''],
                         ['column', 'indexed', 'false']]
      column_name = 'column'
      c_flags = 'Ra'
      c_indexed = 'true'
      n_flags = ''
      n_indexed = 'false'
      flags, indexed, ucd, utype = TapPlus.get_new_column_values_for_update(
          list_of_changes, column_name, c_flags, c_indexed, c_ucd, c_utype)
      assert flags == n_flags
      assert indexed == n_indexed
  
      # Test if flag is Ra, indexed is True
      list_of_changes = [['column', 'flags', 'Ra'],
                         ['column', 'indexed', 'false']]
      column_name = 'column'
      c_flags = 'Ra'
      c_indexed = 'true'
      n_flags = 'Ra'
      n_indexed = 'True'
      flags, indexed, ucd, utype = TapPlus.get_new_column_values_for_update(
          list_of_changes, column_name, c_flags, c_indexed, c_ucd, c_utype)
      assert flags == n_flags
      assert indexed == n_indexed
  
  
  def test_get_current_column_values_for_update():
      column = TapColumn(None)
      column.name = 'colname'
      column.flags = 0
      column.flag = None
      column.ucd = 'ucd'
      column.utype = 'utype'
      column_name, flags, indexed, ucd, utype = \
          TapPlus.get_current_column_values_for_update(column)
      assert column_name == 'colname'
      assert flags is None
      assert indexed is False
      assert ucd == 'ucd'
      assert utype == 'utype'
  
      column.flags = '1'
      column_name, flags, indexed, ucd, utype = \
          TapPlus.get_current_column_values_for_update(column)
      assert flags == 'Ra'
      assert indexed is True
      column.flags = '2'
      column_name, flags, indexed, ucd, utype = \
          TapPlus.get_current_column_values_for_update(column)
      assert flags == 'Dec'
      assert indexed is True
      column.flags = '4'
      column_name, flags, indexed, ucd, utype = \
          TapPlus.get_current_column_values_for_update(column)
      assert flags == 'Flux'
      assert indexed is False
      column.flags = '8'
      column_name, flags, indexed, ucd, utype = \
          TapPlus.get_current_column_values_for_update(column)
      assert flags == 'Mag'
      assert indexed is False
      column.flags = '16'
      column_name, flags, indexed, ucd, utype = \
          TapPlus.get_current_column_values_for_update(column)
      assert flags == 'PK'
      assert indexed is True
      column.flags = '33'
      column_name, flags, indexed, ucd, utype = \
          TapPlus.get_current_column_values_for_update(column)
      assert flags == 'Ra'
      assert indexed is True
      column.flags = '34'
      column_name, flags, indexed, ucd, utype = \
          TapPlus.get_current_column_values_for_update(column)
      assert flags == 'Dec'
      assert indexed is True
      column.flags = '38'
      column_name, flags, indexed, ucd, utype = \
          TapPlus.get_current_column_values_for_update(column)
      assert flags == 'Flux'
      assert indexed is False
      column.flags = '40'
      column_name, flags, indexed, ucd, utype = \
          TapPlus.get_current_column_values_for_update(column)
      assert flags == 'Mag'
      assert indexed is False
      column.flags = '48'
      column_name, flags, indexed, ucd, utype = \
          TapPlus.get_current_column_values_for_update(column)
      assert flags == 'PK'
      assert indexed is True
  
  
  def test_update_user_table():
      tableName = 'table'
      connHandler = DummyConnHandler()
      tap = TapPlus("http://test:1111/tap", connhandler=connHandler)
      dummyResponse = DummyResponse(200)
      tableDataFile = data_path('test_table_update.xml')
      tableData = utils.read_file_content(tableDataFile)
      dummyResponse.set_data(method='GET', body=tableData)
      tableRequest = f"tables?tables={tableName}"
      connHandler.set_response(tableRequest, dummyResponse)
  
      with pytest.raises(Exception):
          tap.update_user_table()
      with pytest.raises(Exception):
          tap.update_user_table(table_name=tableName)
      with pytest.raises(Exception):
          tap.update_user_table(table_name=tableName, list_of_changes=[])
      with pytest.raises(Exception):
          tap.update_user_table(table_name=tableName, list_of_changes=[[]])
      with pytest.raises(Exception):
          tap.update_user_table(table_name=tableName, list_of_changes=[['', '', '']])
  
      # Test Ra & Dec are provided
      list_of_changes = [['alpha', 'flags', 'Ra']]
      with pytest.raises(Exception):
          tap.update_user_table(table_name=tableName, list_of_changes=list_of_changes)
      list_of_changes = [['delta', 'flags', 'Dec']]
      with pytest.raises(Exception):
          tap.update_user_table(table_name=tableName, list_of_changes=list_of_changes)
  
      # OK
      responseEditTable = DummyResponse(200)
      dictTmp = {
          "ACTION": "edit",
          "NUMTABLES": "1",
          "TABLE0": tableName,
          "TABLE0_COL0": "table6_oid",
          "TABLE0_COL0_FLAGS": "PK",
          "TABLE0_COL0_INDEXED": "True",
          "TABLE0_COL0_UCD": "",
          "TABLE0_COL0_UTYPE": "",
          "TABLE0_COL1": "source_id",
          "TABLE0_COL1_FLAGS": "None",
          "TABLE0_COL1_INDEXED": "False",
          "TABLE0_COL1_UCD": "None",
          "TABLE0_COL1_UTYPE": "None",
          "TABLE0_COL2": "alpha",
          "TABLE0_COL2_FLAGS": "Ra",
          "TABLE0_COL2_INDEXED": "False",
          "TABLE0_COL2_UCD": "",
          "TABLE0_COL2_UTYPE": "",
          "TABLE0_COL3": "delta",
          "TABLE0_COL3_FLAGS": "Dec",
          "TABLE0_COL3_INDEXED": "False",
          "TABLE0_COL3_UCD": "",
          "TABLE0_COL3_UTYPE": "",
          "TABLE0_NUMCOLS": "4"
      }
      sortedKey = taputils.taputil_create_sorted_dict_key(dictTmp)
      req = f"tableEdit?{sortedKey}"
      connHandler.set_response(req, responseEditTable)
  
      list_of_changes = [['alpha', 'flags', 'Ra'], ['delta', 'flags', 'Dec']]
      tap.update_user_table(table_name=tableName, list_of_changes=list_of_changes)
  
  
  def test_rename_table():
      tableName = 'user_test.table_test_rename'
      newTableName = 'user_test.table_test_rename_new'
      newColumnNames = {'ra': 'alpha', 'dec': 'delta'}
      connHandler = DummyConnHandler()
      tap = TapPlus("http://test:1111/tap", connhandler=connHandler)
      dummyResponse = DummyResponse(200)
      tableDataFile = data_path('test_table_rename.xml')
      tableData = utils.read_file_content(tableDataFile)
      dummyResponse.set_data(method='GET', body=tableData)
  
      with pytest.raises(Exception):
          tap.rename_table()
      with pytest.raises(Exception):
          tap.rename_table(table_name=tableName)
      with pytest.raises(Exception):
          tap.rename_table(table_name=tableName, new_table_name=None, new_column_names_dict=None)
  
      # Test OK.
      responseRenameTable = DummyResponse(200)
      dictArgs = {
          "action": "rename",
          "new_column_names": "ra:alpha,dec:delta",
          "new_table_name": newTableName,
          "table_name": tableName,
      }
      data = connHandler.url_encode(dictArgs)
      req = f"TableTool?{data}"
      connHandler.set_response(req, responseRenameTable)
      tap.rename_table(table_name=tableName, new_table_name=newTableName, new_column_names_dict=newColumnNames)
  
  
  def __find_table(schemaName, tableName, tables):
      qualifiedName = f"{schemaName}.{tableName}"
      for table in (tables):
          if table.get_qualified_name() == qualifiedName:
              return table
      # not found: raise exception
      pytest.fail(f"Table '{qualifiedName}' not found")
  
  
  def __find_column(columnName, columns):
      for c in (columns):
          if c.name == columnName:
              return c
      # not found: raise exception
      pytest.fail(f"Column '{columnName}' not found")
  
  
  def __check_column(column, description, unit, dataType, flag):
      assert column.description == description
      assert column.unit == unit
      assert column.data_type == dataType
      assert column.flag == flag
  
  
  def __check_results_column(results, columnName, description, unit,
                             dataType):
      c = results[columnName]
      assert c.description == description
      assert c.unit == unit
      assert c.dtype == dataType
  
  
  @patch.object(TapPlus, 'login')
  def test_login(mock_login):
      conn_handler = DummyConnHandler()
      tap = TapPlus("http://test:1111/tap", connhandler=conn_handler)
      tap.login("user", "password")
      assert (mock_login.call_count == 1)
      mock_login.side_effect = HTTPError("Login error")
      with pytest.raises(HTTPError):
          tap.login("user", "password")
      assert (mock_login.call_count == 2)
  
  
  @patch.object(TapPlus, 'login_gui')
  @patch.object(TapPlus, 'login')
  def test_login_gui(mock_login_gui, mock_login):
      conn_handler = DummyConnHandler()
      tap = TapPlus("http://test:1111/tap", connhandler=conn_handler)
      tap.login_gui()
      assert (mock_login_gui.call_count == 0)
      mock_login_gui.side_effect = HTTPError("Login error")
      with pytest.raises(HTTPError):
          tap.login("user", "password")
      assert (mock_login.call_count == 1)
  
  
  @patch.object(TapPlus, 'logout')
  def test_logout(mock_logout):
      conn_handler = DummyConnHandler()
      tap = TapPlus("http://test:1111/tap", connhandler=conn_handler)
      tap.logout()
      assert (mock_logout.call_count == 1)
      mock_logout.side_effect = HTTPError("Login error")
      with pytest.raises(HTTPError):
          tap.logout()
      assert (mock_logout.call_count == 2)
  
files: astroquery/utils/tap/tests/test_tap.py