Source code for astroquery.noirlab.core

# Licensed under a 3-clause BSD style license - see LICENSE.rst
"""
Provide astroquery API access to NSF NOIRLab Astro Data Archive.

This does DB access through web-services.
"""
import astropy.io.fits as fits
import astropy.table
from ..query import BaseQuery
from ..exceptions import RemoteServiceError
from . import conf


__all__ = ['NOIRLab', 'NOIRLabClass']  # specifies what to import


[docs] class NOIRLabClass(BaseQuery): """Search functionality for the NSF NOIRLab Astro Data Archive. """ TIMEOUT = conf.timeout NAT_URL = conf.server def __init__(self): self._api_version = None super().__init__() @property def api_version(self): """Return version of REST API used by this module. If the REST API changes such that the major version increases, a new version of this module will likely need to be used. """ if self._api_version is None: self._api_version = float(self._version()) return self._api_version def _validate_version(self): """Ensure the API is compatible with the code. """ KNOWN_GOOD_API_VERSION = 7.0 if (int(self.api_version) - int(KNOWN_GOOD_API_VERSION)) >= 1: msg = (f'The astroquery.noirlab module is expecting an older ' f'version of the {self.NAT_URL} API services. ' f'Please upgrade to latest astroquery. ' f'Expected version {KNOWN_GOOD_API_VERSION} but got ' f'{self.api_version} from the API.') raise RemoteServiceError(msg)
[docs] def sia_url(self, hdu=False): """Return the URL for SIA queries. Parameters ---------- hdu : :class:`bool`, optional If ``True`` return the URL for HDU-based queries. Returns ------- :class:`str` The query URL. Notes ----- In other modules this is an attribute or property. However, NOIRLab has two separate SIA URLs for File-based and HDU-based queries, thus a method is needed here. """ return f'{self.NAT_URL}/api/sia/vohdu' if hdu else f'{self.NAT_URL}/api/sia/voimg'
def _fields_url(self, hdu=False, aux=False): """Return the URL for metadata queries. Parameters ---------- hdu : :class:`bool`, optional If ``True`` return the URL for HDU-based queries. aux : :class:`bool`, optional If ``True`` return metadata on AUX fields. Returns ------- :class:`str` The query URL. """ file = 'hdu' if hdu else 'file' core = 'aux' if aux else 'core' return f'{self.NAT_URL}/api/adv_search/{core}_{file}_fields' def _response_to_table(self, response_json, sia=False): """Convert a JSON response to a :class:`~astropy.table.Table`. Parameters ---------- response_json : :class:`list` A query response formatted as a list of objects. The query metadata is the first item in the list. sia : :class:`bool`, optional If ``True``, `response_json` came from a SIA query. Returns ------- :class:`~astropy.table.Table` The converted response. The column ordering will match the ordering of the `HEADER` metadata. Notes ----- * Metadata queries return columns that are qualified with ``file:`` or ``hdu:``, however SIA queries to not. * HDU queries will label HDU-specific fields with ``hdu:`` but other fields will be qualified with ``file:``. """ if sia: raw_names = [k for k in response_json[0]['HEADER'].keys()] names = raw_names else: raw_names = [k for k in response_json[0]['HEADER'].keys() if k.startswith('file:') or k.startswith('hdu:')] names = [n.split(':')[1] for n in raw_names] rows = [[row[n] for n in raw_names] for row in response_json[1:]] return astropy.table.Table(names=names, rows=rows) def _service_metadata(self, hdu=False, cache=True): """A SIA metadata query: no images are requested; only metadata should be returned. This feature is described in more detail in: https://www.ivoa.net/documents/PR/DAL/PR-SIA-1.0-20090521.html#mdquery Parameters ---------- hdu : :class:`bool`, optional If ``True`` return the URL for HDU-based queries. cache : :class:`bool`, optional If ``True`` cache the result locally. Returns ------- :class:`dict` A dictionary containing SIA metadata. """ url = f'{self.sia_url(hdu=hdu)}?FORMAT=METADATA&format=json' response = self._request('GET', url, timeout=self.TIMEOUT, cache=cache) return response.json()
[docs] def query_region(self, coordinate, *, radius=0.1, hdu=False, cache=True, async_=False): """Query for NOIRLab observations by region of the sky. Given a sky coordinate and radius, returns a `~astropy.table.Table` of NOIRLab observations. Parameters ---------- coordinate : :class:`str` or `~astropy.coordinates` object The target region which to search. It may be specified as a string or as the appropriate `~astropy.coordinates` object. radius : :class:`float` or :class:`str` or `~astropy.units.Quantity` object, optional Default 0.1 degrees. The string must be parsable by `~astropy.coordinates.Angle`. The appropriate `~astropy.units.Quantity` object from `~astropy.units` may also be used. hdu : :class:`bool`, optional If ``True``, perform the query on HDUs. cache : :class:`bool`, optional If ``True``, cache the result locally. async_ : :class:`bool`, optional If ``True``, return the raw query response instead of a Table. Returns ------- :class:`~astropy.table.Table` A table containing the results. """ self._validate_version() ra, dec = coordinate.to_string('decimal').split() url = f'{self.sia_url(hdu=hdu)}?POS={ra},{dec}&SIZE={radius}&VERB=3&format=json' response = self._request('GET', url, timeout=self.TIMEOUT, cache=cache) if async_: return response response.raise_for_status() return self._response_to_table(response.json(), sia=True)
[docs] def list_fields(self, *, aux=False, instrument=None, proctype=None, hdu=False, categorical=False, cache=True): """List the available fields for searches using :meth:`~astroquery.noirlab.NOIRLabClass.query_metadata`. The default is to return core fields for file-based queries. Parameters ---------- aux : :class:`bool`, optional If ``True``, return aux fields. ``instrument`` and ``proctype`` must also be specified. instrument : :class:`str`, optional The specific instrument, *e.g.* '90prime' or 'decam'. proctype : :class:`str`, optional A description of the type of image, *e.g.* 'raw' or 'instcal'. hdu : :class:`bool`, optional If ``True`` return the fields for HDU-based queries. categorical : :class:`bool`, optional If ``True`` return the categorical fields and their allowed values. cache : :class:`bool`, optional If ``True`` cache the result locally. Returns ------- :class:`list` or :class:`dict` A list of field descriptions, each a :class:`dict`. If ``categorical=True`` return a :class:`dict` describing the allowed values of each categorical field. Raises ------ ValueError If ``aux=True`` and ``instrument`` or ``proctype`` are not specified. Notes ----- * Core fields are faster to search than Aux fields. * The available fields depend on whether a File or a HDU query is requested. * Categorical fields can only take on one of a set of values. """ if categorical: url = f'{self.NAT_URL}/api/adv_search/cat_lists/?format=json' else: url = self._fields_url(hdu=hdu, aux=aux) if aux: if instrument is None: raise ValueError("instrument must be specified if aux=True.") if proctype is None: raise ValueError("instrument must be specified if aux=True.") url = f'{url}/{instrument}/{proctype}/' response = self._request('GET', url, timeout=self.TIMEOUT, cache=cache) response.raise_for_status() return response.json()
[docs] def query_metadata(self, qspec=None, sort=None, limit=1000, hdu=False, cache=True): """Query the archive database for details on available files. ``qspec`` should minimally contain a list of output columns and a list of search parameters, which could be empty. For example:: qspec = {"outfields": ["md5sum", ], "search": []} There are more details in the :ref:`NOIRLab overview document <astroquery.noirlab>`. Parameters ---------- qspec : :class:`dict`, optional The query that will be passed to the API. sort : :class:`str`, optional Sort the results on one of the columns in ``qspec``. limit : :class:`int`, optional The number of results to return, default 1000. hdu : :class:`bool`, optional If ``True`` return the URL for HDU-based queries. cache : :class:`bool`, optional If ``True`` cache the result locally. Returns ------- :class:`~astropy.table.Table` A Table containing the results. """ self._validate_version() rectype = 'hdu' if hdu else 'file' url = f'{self.NAT_URL}/api/adv_search/find/?rectype={rectype}&limit={limit}' if sort: # TODO: write a test for this, which may involve refactoring async versus sync. url += f'&sort={sort}' if qspec is None: jdata = {"outfields": ["md5sum", ], "search": []} else: jdata = qspec response = self._request('POST', url, json=jdata, timeout=self.TIMEOUT, cache=cache) response.raise_for_status() return self._response_to_table(response.json())
[docs] def get_file(self, fileid): """Simply fetch a file by MD5 ID. Parameters ---------- fileid : :class:`str` The MD5 ID of the file. Returns ------- :class:`~astropy.io.fits.HDUList` The open FITS file. Call ``.close()`` on this object when done. """ url = f'{self.NAT_URL}/api/retrieve/{fileid}/' hdulist = fits.open(url) return hdulist
def _version(self, cache=False): """Return the version of the REST API. Typically, users will use the ``api_version`` property instead of this method. Parameters ---------- cache : :class:`bool`, optional If ``True`` cache the result locally. Returns ------- :class:`float` The API version as a number. """ url = f'{self.NAT_URL}/api/version/' response = self._request('GET', url, timeout=self.TIMEOUT, cache=cache) response.raise_for_status() return response.json()
[docs] def get_token(self, email, password, cache=True): """Get an access token to use with proprietary data. Parameters ---------- email : :class:`str` Email for account access. password : :class:`str` Password associated with `email`. *Please* never hard-code your password *anywhere*. cache : :class:`bool`, optional If ``True`` cache the result locally. Returns ------- :class:`str` The access token as a string. """ url = f'{self.NAT_URL}/api/get_token/' response = self._request('POST', url, json={"email": email, "password": password}, timeout=self.TIMEOUT, cache=cache) response.raise_for_status() return response.json()
NOIRLab = NOIRLabClass()