def _handle_error(self, text): """ Parse the response from a request to see if it failed Parameters ---------- text : str The decoded body of the response. Raises ------ InvalidColumnError : If ``select`` included an invalid column. InvalidTableError : If the queried ``table`` does not exist. RemoteServiceError : If anything else went wrong. """ # Error messages will always be formatted starting with the word "ERROR" if not text.startswith("ERROR"): return # Some errors have the form: # Error type: ... # Message: ... # so we'll parse those to try to provide some reasonable feedback to the user error_type = None error_message = None for line in text.replace("<br>", "").splitlines(): match = re.search(r"Error Type:\s(.+)$", line) if match: error_type = match.group(1).strip() continue match = re.search(r"Message:\s(.+)$", line) if match: error_message = match.group(1).strip() continue # If we hit this condition, that means that we weren't able to parse the error so we'll # just throw the full response if error_type is None or error_message is None: raise RemoteServiceError(text) # A useful special is if a column name is unrecognized. This has the format # Error type: SystemError # Message: ... "NAME_OF_COLUMN": invalid identifier ... if error_type.startswith("SystemError"): match = re.search(r'"(.*)": invalid identifier', error_message) if match: raise InvalidQueryError( ( "'{0}' is an invalid identifier. This error can be caused by invalid " "column names, missing quotes, or other syntax errors" ).format(match.group(1).lower()) ) elif error_type.startswith("UserError"): # Another important one is when the table is not recognized. This has the format: # Error type: UserError - "table" parameter # Message: ... "NAME_OF_TABLE" is not a valid table. match = re.search(r'"(.*)" is not a valid table', error_message) if match: raise InvalidTableError("'{0}' is not a valid table".format(match.group(1).lower())) raise InvalidQueryError("{0}\n{1}".format(error_type, error_message)) # Finally just return the full error message if we got here message = "\n".join(line for line in (error_type, error_message) if line is not None) raise RemoteServiceError(message)
def query_region_async(self, coordinates=None, *, catalog=None, spatial='Cone', radius=10 * u.arcsec, width=None, polygon=None, get_query_payload=False, selcols=None, verbose=False, cache=True): """ This function serves the same purpose as :meth:`~astroquery.irsa.IrsaClass.query_region`, but returns the raw HTTP response rather than the results in a `~astropy.table.Table`. Parameters ---------- coordinates : str, `astropy.coordinates` object Gives the position of the center of the cone or box if performing a cone or box search. The string can give coordinates in various coordinate systems, or the name of a source that will be resolved on the server (see `here <https://irsa.ipac.caltech.edu/search_help.html>`_ for more details). Required if spatial is ``'Cone'`` or ``'Box'``. Optional if spatial is ``'Polygon'``. catalog : str The catalog to be used. To list the available catalogs, use :meth:`~astroquery.irsa.IrsaClass.print_catalogs`. spatial : str Type of spatial query: ``'Cone'``, ``'Box'``, ``'Polygon'``, and ``'All-Sky'``. If missing then defaults to ``'Cone'``. radius : str or `~astropy.units.Quantity` object, [optional for spatial is ``'Cone'``] The string must be parsable by `~astropy.coordinates.Angle`. The appropriate `~astropy.units.Quantity` object from `astropy.units` may also be used. Defaults to 10 arcsec. width : str, `~astropy.units.Quantity` object [Required for spatial is ``'Polygon'``.] The string must be parsable by `~astropy.coordinates.Angle`. The appropriate `~astropy.units.Quantity` object from `astropy.units` may also be used. polygon : list, [Required for spatial is ``'Polygon'``] A list of ``(ra, dec)`` pairs (as tuples), in decimal degrees, outlining the polygon to search in. It can also be a list of `astropy.coordinates` object or strings that can be parsed by `astropy.coordinates.ICRS`. get_query_payload : bool, optional If `True` then returns the dictionary sent as the HTTP request. Defaults to `False`. selcols : str, optional Target column list with value separated by a comma(,) verbose : bool, optional. If `True` then displays warnings when the returned VOTable does not conform to the standard. Defaults to `False`. cache : bool, optional Use local cache when set to `True`. Returns ------- response : `requests.Response` The HTTP response returned from the service """ if catalog is None: raise InvalidQueryError("Catalog name is required!") request_payload = self._args_to_payload(catalog, selcols=selcols) request_payload.update( self._parse_spatial(spatial=spatial, coordinates=coordinates, radius=radius, width=width, polygon=polygon)) if get_query_payload: return request_payload response = self._request("GET", url=Irsa.IRSA_URL, params=request_payload, timeout=Irsa.TIMEOUT, cache=cache) return response
def query_object_async(self, object_name, *, table="ps", get_query_payload=False, cache=None, regularize=True, **criteria): """ Search the tables of confirmed exoplanets for information about a planet or planet host The tables available to this query are the following (more information can be found on the archive's documentation pages [1]_): - ``ps``: This table compiles parameters derived from a multiple published references on separate rows, each row containing self-consistent values from one reference. - ``pscomppars``: This table compiles all parameters of confirmed exoplanets from multiple, published references in one row (not all self-consistent) per object. Parameters ---------- object_name : str The name of the planet or star. If ``regularize`` is ``True``, an attempt will be made to regularize this name using the ``aliastable`` table. Defaults to ``True``. table : [``"ps"`` or ``"pscomppars"``], optional The table to query, must be one of the supported tables: ``"ps"`` or ``"pscomppars"``. Defaults to ``"ps"``. get_query_payload : bool, optional Just return the dict of HTTP request parameters. Defaults to ``False``. cache : bool, optional Should the request result be cached? This can be useful for large repeated queries, but since the data in the archive is updated regularly, this defaults to ``False``. regularize : bool, optional If ``True``, the ``aliastable`` will be used to regularize the target name. **criteria Any other filtering criteria to apply. Values provided using the ``where`` keyword will be ignored. Returns ------- response : `requests.Response` The HTTP response returned from the service. References ---------- .. [1] `NASA Exoplanet Archive TAP Documentation <https://exoplanetarchive.ipac.caltech.edu/docs/TAP/usingTAP.html>`_ .. [2] `NASA Exoplanet Archive API Documentation <https://exoplanetarchive.ipac.caltech.edu/docs/program_interfaces.html>`_ """ prefix = OBJECT_TABLES.get(table, None) if prefix is None: raise InvalidQueryError(f"Invalid table '{table}'. The allowed options are: 'ps' and 'pscomppars'") if regularize: object_name = self._regularize_object_name(object_name) if "where" in criteria: warnings.warn("Any filters using the 'where' argument are ignored " "in ``query_object``. Consider using ``query_criteria`` instead.", InputWarning) if table in self.TAP_TABLES: criteria["where"] = "hostname='{1}' OR {0}name='{1}'".format(prefix, object_name.strip()) else: criteria["where"] = "{0}hostname='{1}' OR {0}name='{1}'".format(prefix, object_name.strip()) return self.query_criteria_async(table, get_query_payload=get_query_payload, cache=cache, **criteria)
def query_criteria_async(self, table, get_query_payload=False, cache=None, **criteria): """ Search a table given a set of criteria or return the full table The syntax for these queries is described on the Exoplanet Archive TAP[1]_ API[2]_ documentation pages. In particular, the most commonly used criteria will be ``select`` and ``where``. Parameters ---------- table : str The name of the table to query. A list of the tables on the Exoplanet Archive can be found on the documentation pages [1]_, [2]_. get_query_payload : bool, optional Just return the dict of HTTP request parameters. Defaults to ``False``. cache : bool, optional Should the request result be cached? This can be useful for large repeated queries, but since the data in the archive is updated regularly, this defaults to ``False``. **criteria The filtering criteria to apply. These are described in detail in the archive documentation [1]_, [2]_ but some examples include ``select="*"`` to return all columns of the queried table or ``where=pl_name='K2-18 b'`` to filter a specific column. Returns ------- response : `requests.Response` The HTTP response returned from the service. References ---------- .. [1] `NASA Exoplanet Archive TAP Documentation <https://exoplanetarchive.ipac.caltech.edu/docs/TAP/usingTAP.html>`_ .. [2] `NASA Exoplanet Archive API Documentation <https://exoplanetarchive.ipac.caltech.edu/docs/program_interfaces.html>`_ """ # Make sure table is lower-case table = table.lower() # Warn if old table is requested if table in MAP_TABLEWARNINGS.keys(): raise InvalidTableError( "The `{0}` table is no longer updated and has been replacedby the `{1}` table, which" " is connected to the Exoplanet Archive TAP service. Although the argument keywords " "of the called method should still work on the new table, the allowed values could " "have changed since the database column names have changed; this document contains " "the current definitions and a mapping between the new and deprecated names: " "https://exoplanetarchive.ipac.caltech.edu/docs/API_PS_columns.html. You might also " "want to review the TAP User Guide for help on creating a new query for the most " "current data: https://exoplanetarchive.ipac.caltech.edu/docs/TAP/usingTAP.html." .format(table, MAP_TABLEWARNINGS[table])) # Deal with lists of columns instead of comma separated strings criteria = copy.copy(criteria) if "select" in criteria: select = criteria["select"] if not isinstance(select, str): select = ",".join(select) criteria["select"] = select # We prefer to work with IPAC format so that we get units, but everything it should work # with the other options too # Get the format, or set it to "ipac" if not given. Makes more sense to use CSV here. criteria["format"] = criteria.get("format", "ipac") # Less formats are allowed for TAP, so this needs to be updated. Default # is VOTable (vot?, xml?), also csv and tsv are allowed if "json" in criteria["format"].lower(): raise InvalidQueryError("The 'json' format is not supported") # Build the query (and return it if requested) request_payload = dict(table=table, **criteria) if get_query_payload: return request_payload # Use the default cache setting if one was not provided if cache is None: cache = self.CACHE if table in self.TAP_TABLES: tap = pyvo.dal.tap.TAPService(baseurl=self.URL_TAP) # construct query from table and request_payload (including format) tap_query = self._request_to_sql(request_payload) try: response = tap.search(query=tap_query, language='ADQL') # Note that this returns a VOTable except Exception as err: raise InvalidQueryError(str(err)) else: response = self._request( "GET", self.URL_API, params=request_payload, timeout=self.TIMEOUT, cache=cache, ) response.requested_format = criteria["format"] return response
def query_lines_async(self, min_frequency, max_frequency, min_strength=-500, max_lines=2000, molecule='All', flags=0, parse_name_locally=False, get_query_payload=False, cache=True): """ Creates an HTTP POST request based on the desired parameters and returns a response. Parameters ---------- min_frequency : `astropy.units` Minimum frequency (or any spectral() equivalent) max_frequency : `astropy.units` Maximum frequency (or any spectral() equivalent) min_strength : int, optional Minimum strength in catalog units, the default is -500 max_lines : int, optional Maximum number of lines to query, the default is 2000. The most the query allows is 100000 molecule : list, string of regex if parse_name_locally=True, optional Identifiers of the molecules to search for. If this parameter is not provided the search will match any species. Default is 'All'. flags : int, optional Regular expression flags. Default is set to 0 parse_name_locally : bool, optional When set to True it allows the method to parse through catdir.cat in order to match the regex inputted in the molecule parameter and request the corresponding tags of the matches instead. Default is set to False get_query_payload : bool, optional When set to `True` the method should return the HTTP request parameters as a dict. Default value is set to False Returns ------- response : `requests.Response` The HTTP response returned from the service. Examples -------- >>> table = JPLSpec.query_lines(min_frequency=100*u.GHz, ... max_frequency=200*u.GHz, ... min_strength=-500, molecule=18003) # doctest: +REMOTE_DATA >>> print(table) # doctest: +SKIP FREQ ERR LGINT DR ELO GUP TAG QNFMT QN' QN" ----------- ------ -------- --- --------- --- ------ ----- -------- -------- 115542.5692 0.6588 -13.2595 3 4606.1683 35 18003 1404 17 810 0 18 513 0 139614.293 0.15 -9.3636 3 3080.1788 87 -18003 1404 14 6 9 0 15 312 0 177317.068 0.15 -10.3413 3 3437.2774 31 -18003 1404 15 610 0 16 313 0 183310.087 0.001 -3.6463 3 136.1639 7 -18003 1404 3 1 3 0 2 2 0 0 """ # first initialize the dictionary of HTTP request parameters payload = dict() if min_frequency is not None and max_frequency is not None: # allow setting payload without having *ANY* valid frequencies set min_frequency = min_frequency.to(u.GHz, u.spectral()) max_frequency = max_frequency.to(u.GHz, u.spectral()) if min_frequency > max_frequency: min_frequency, max_frequency = max_frequency, min_frequency payload['MinNu'] = min_frequency.value payload['MaxNu'] = max_frequency.value if max_lines is not None: payload['MaxLines'] = max_lines payload['UnitNu'] = 'GHz' payload['StrLim'] = min_strength if molecule is not None: if parse_name_locally: self.lookup_ids = build_lookup() payload['Mol'] = tuple( self.lookup_ids.find(molecule, flags).values()) if len(molecule) == 0: raise InvalidQueryError( 'No matching species found. Please ' 'refine your search or read the Docs ' 'for pointers on how to search.') else: payload['Mol'] = molecule self.maxlines = max_lines payload = list(payload.items()) if get_query_payload: return payload # BaseQuery classes come with a _request method that includes a # built-in caching system response = self._request(method='POST', url=self.URL, data=payload, timeout=self.TIMEOUT, cache=cache) return response
def query_lines_async(self, min_frequency, max_frequency, *, min_strength=-500, molecule='All', temperature_for_intensity=300, flags=0, parse_name_locally=False, get_query_payload=False, cache=True): """ Creates an HTTP POST request based on the desired parameters and returns a response. Parameters ---------- min_frequency : `astropy.units.Quantity` or None Minimum frequency (or any spectral() equivalent). ``None`` can be interpreted as zero. max_frequency : `astropy.units.Quantity` or None Maximum frequency (or any spectral() equivalent). ``None`` can be interpreted as infinite. min_strength : int, optional Minimum strength in catalog units, the default is -500 molecule : list, string of regex if parse_name_locally=True, optional Identifiers of the molecules to search for. If this parameter is not provided the search will match any species. Default is 'All'. temperature_for_intensity : float The temperature to use when computing the intensity Smu^2. Set to 300 by default for compatibility with JPL and the native catalog format, which defaults to 300. ** If temperature is set to zero, the return value in this column will be the Einstein A value ** flags : int, optional Regular expression flags. Default is set to 0 parse_name_locally : bool, optional When set to True it allows the method to parse through catdir.cat (see `get_species_table`) in order to match the regex inputted in the molecule parameter and request the corresponding tags of the matches instead. Default is set to False get_query_payload : bool, optional When set to `True` the method should return the HTTP request parameters as a dict. Default value is set to False cache : bool Cache the request and, for repeat identical requests, reuse the cache? Returns ------- response : `requests.Response` The HTTP response returned from the service. Examples -------- >>> table = CDMS.query_lines(min_frequency=100*u.GHz, ... max_frequency=110*u.GHz, ... min_strength=-500, ... molecule="018505 H2O+") # doctest: +REMOTE_DATA >>> print(table) # doctest: +SKIP FREQ ERR LGINT DR ELO GUP TAG QNFMT Ju Ku vu Jl Kl vl F name MHz MHz MHz nm2 1 / cm ----------- ----- ------- --- -------- --- ----- ----- --- --- --- --- --- --- ----------- ---- 103614.4941 2.237 -4.1826 3 202.8941 8 18505 2356 4 1 4 4 0 4 3 2 1 3 0 3 H2O+ 107814.8763 148.6 -5.4438 3 878.1191 12 18505 2356 6 5 1 7 1 6 7 4 4 8 1 7 H2O+ 107822.3481 148.6 -5.3846 3 878.1178 14 18505 2356 6 5 1 7 1 7 7 4 4 8 1 8 H2O+ 107830.1216 148.6 -5.3256 3 878.1164 16 18505 2356 6 5 1 7 1 8 7 4 4 8 1 9 H2O+ """ # first initialize the dictionary of HTTP request parameters payload = dict() if min_frequency is not None and max_frequency is not None: # allow setting payload without having *ANY* valid frequencies set min_frequency = min_frequency.to(u.GHz, u.spectral()) max_frequency = max_frequency.to(u.GHz, u.spectral()) if min_frequency > max_frequency: raise InvalidQueryError( "min_frequency must be less than max_frequency") payload['MinNu'] = min_frequency.value payload['MaxNu'] = max_frequency.value payload['UnitNu'] = 'GHz' payload['StrLim'] = min_strength payload['temp'] = temperature_for_intensity payload['logscale'] = 'yes' payload['mol_sort_query'] = 'tag' payload['sort'] = 'frequency' payload['output'] = 'text' payload['but_action'] = 'Submit' # changes interpretation of query self._last_query_temperature = temperature_for_intensity if molecule is not None: if parse_name_locally: self.lookup_ids = build_lookup() luts = self.lookup_ids.find(molecule, flags) payload['Molecules'] = tuple(f"{val:06d} {key}" for key, val in luts.items())[0] if len(molecule) == 0: raise InvalidQueryError( 'No matching species found. Please ' 'refine your search or read the Docs ' 'for pointers on how to search.') else: payload['Molecules'] = molecule payload = list(payload.items()) if get_query_payload: return payload # BaseQuery classes come with a _request method that includes a # built-in caching system response = self._request(method='POST', url=self.URL, data=payload, timeout=self.TIMEOUT, cache=cache) response.raise_for_status() soup = BeautifulSoup(response.text, 'html.parser') ok = False urls = [x.attrs['src'] for x in soup.findAll('frame', )] for url in urls: if 'tab' in url and 'head' not in url: ok = True break if not ok: raise EmptyResponseError("Did not find table in response") baseurl = self.URL.split('cgi-bin')[0] fullurl = f'{baseurl}/{url}' response2 = self._request(method='GET', url=fullurl, timeout=self.TIMEOUT, cache=cache) return response2
def query_region_async(self, coordinate=None, where=None, mission=None, dataset=None, table=None, columns=None, width=None, height=None, action='search', intersect='OVERLAPS', most_centered=False): """ For certain missions, this function can be used to search for image and catalog files based on a point, a box (bounded by great circles) and/or an SQL-like ``where`` clause. If ``coordinates`` is specified, then the optional ``width`` and ``height`` arguments control the width and height of the search box. If neither ``width`` nor ``height`` are provided, then the search area is a point. If only one of ``width`` or ``height`` are specified, then the search area is a square with that side length centered at the coordinate. Parameters ---------- coordinate : str, `astropy.coordinates` object Gives the position of the center of the box if performing a box search. If it is a string, then it must be a valid argument to `~astropy.coordinates.SkyCoord`. Required if ``where`` is absent. where : str SQL-like query string. Required if ``coordinates`` is absent. mission : str The mission to be used (if not the default mission). dataset : str The dataset to be used (if not the default dataset). table : str The table to be queried (if not the default table). columns : str, list A space-separated string or a list of strings of the names of the columns to return. width : str or `~astropy.units.Quantity` object Width of the search box if ``coordinates`` is present. The string must be parsable by `~astropy.coordinates.Angle`. The appropriate `~astropy.units.Quantity` object from `astropy.units` may also be used. height : str, `~astropy.units.Quantity` object Height of the search box if ``coordinates`` is present. The string must be parsable by `~astropy.coordinates.Angle`. The appropriate `~astropy.units.Quantity` object from `astropy.units` may also be used. intersect : ``'COVERS'``, ``'ENCLOSED'``, ``'CENTER'``, ``'OVERLAPS'`` Spatial relationship between search box and image footprint. ``'COVERS'``: X must completely contain S. Equivalent to ``'CENTER'`` and ``'OVERLAPS'`` if S is a point. ``'ENCLOSED'``: S must completely contain X. If S is a point, the query will always return an empty image table. ``'CENTER'``: X must contain the center of S. If S is a point, this is equivalent to ``'COVERS'`` and ``'OVERLAPS'``. ``'OVERLAPS'``: The intersection of S and X is non-empty. If S is a point, this is equivalent to ``'CENTER'`` and ``'COVERS'``. most_centered : bool If True, then only the most centered image is returned. action : ``'search'``, ``'data'``, or ``'sia'`` The action to perform at the server. The default is ``'search'``, which returns a table of the available data. ``'data'`` requires advanced path construction that is not yet supported. ``'sia'`` provides access to the 'simple image access' IVOA protocol Returns ------- response : `~requests.Response` The HTTP response returned from the service """ if coordinate is None and where is None: raise InvalidQueryError( 'At least one of `coordinate` or `where` is required') intersect = intersect.upper() if intersect not in ('COVERS', 'ENCLOSED', 'CENTER', 'OVERLAPS'): raise InvalidQueryError( "Invalid value for `intersects` " + "(must be 'COVERS', 'ENCLOSED', 'CENTER', or 'OVERLAPS')") if action not in ('sia', 'data', 'search'): raise InvalidQueryError("Valid actions are: sia, data, search.") if action == 'data': raise NotImplementedError( "The action='data' option is a placeholder for future " + "functionality.") args = {'INTERSECT': intersect} # Note: in IBE, if 'mcen' argument is present, it is true. # If absent, it is false. if most_centered: args['mcen'] = '1' if coordinate is not None: c = commons.parse_coordinates(coordinate).transform_to(coord.ICRS) args['POS'] = '{0},{1}'.format(c.ra.deg, c.dec.deg) if width and height: args['SIZE'] = '{0},{1}'.format( coord.Angle(width).value, coord.Angle(height).value) elif width or height: args['SIZE'] = str(coord.Angle(width or height).value) if where: args['where'] = where if columns: if isinstance(columns, str): columns = columns.split() args['columns'] = ','.join(columns) url = "{URL}{action}/{mission}/{dataset}/{table}".format( URL=self.URL, action=action, mission=mission or self.MISSION, dataset=dataset or self.DATASET, table=table or self.TABLE) return self._request('GET', url, args, timeout=self.TIMEOUT)