diff --git a/CHANGES.rst b/CHANGES.rst index e3af10bd20..5da05204a4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -65,6 +65,12 @@ ipac.irsa in to return all TAP tables, including non-spatial and metadata ones, too. [#3334] +noirlab +^^^^^^^ + +- Restore access to the `NSF NOIRLab `_ + `Astro Data Archive `_ [#3359]. + SIMBAD ^^^^^^ diff --git a/astroquery/noirlab/__init__.py b/astroquery/noirlab/__init__.py new file mode 100644 index 0000000000..5061407a79 --- /dev/null +++ b/astroquery/noirlab/__init__.py @@ -0,0 +1,24 @@ +# Licensed under a 3-clause BSD style license - see LICENSE.rst +""" +NSF NOIRLab Astro Data Archive Query Tool +----------------------------------------- +""" +from astropy import config as _config + + +class Conf(_config.ConfigNamespace): + """ + Configuration parameters for `astroquery.noirlab`. + """ + server = _config.ConfigItem(['https://astroarchive.noirlab.edu',], + 'Name of the NSF NOIRLab server to use.') + timeout = _config.ConfigItem(30, + 'Time limit for connecting to NSF NOIRLab server.') + + +conf = Conf() + +from .core import NOIRLab, NOIRLabClass # noqa + +__all__ = ['NOIRLab', 'NOIRLabClass', + 'conf', 'Conf'] diff --git a/astroquery/noirlab/core.py b/astroquery/noirlab/core.py new file mode 100644 index 0000000000..3ef83209a7 --- /dev/null +++ b/astroquery/noirlab/core.py @@ -0,0 +1,373 @@ +# 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 ..utils import async_to_sync +from ..exceptions import RemoteServiceError +# from ..utils.class_or_instance import class_or_instance +from . import conf + + +__all__ = ['NOIRLab', 'NOIRLabClass'] # specifies what to import + + +@async_to_sync +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 = 6.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) + + 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. + """ + 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): + """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. + + Returns + ------- + :class:`~astropy.table.Table` + The converted response. The column ordering will match the + ordering of the `HEADER` metadata. + """ + names = list(response_json[0]['HEADER'].keys()) + rows = [[row[n] for n in 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() + + def query_region(self, coordinate, radius=0.1, hdu=False, cache=True): + """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:`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`` 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. + """ + response = self.query_region_async(coordinate, radius=radius, hdu=hdu, cache=cache) + response.raise_for_status() + return self._response_to_table(response.json()) + + def query_region_async(self, coordinate, radius=0.1, hdu=False, cache=True): + """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:`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`` return the URL for HDU-based queries. + cache : :class:`bool`, optional + If ``True`` cache the result locally. + + Returns + ------- + :class:`~requests.Response` + Response object. + """ + 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) + # response.raise_for_status() + return response + + def core_fields(self, hdu=False, cache=True): + """List the available CORE fields for file or HDU searches. + + CORE fields are faster to search than AUX fields. + + Parameters + ---------- + hdu : :class:`bool`, optional + If ``True`` return the fields for HDU-based queries. + cache : :class:`bool`, optional + If ``True`` cache the result locally. + + Returns + ------- + :class:`list` + A list of field descriptions, each a :class:`dict`. + """ + url = self._fields_url(hdu=hdu, aux=False) + response = self._request('GET', url, timeout=self.TIMEOUT, cache=cache) + response.raise_for_status() + return response.json() + + def aux_fields(self, instrument, proctype, hdu=False, cache=True): + """List the available AUX fields. + + AUX fields are any fields in the Archive FITS files that are not + CORE DB fields. These are generally common to a single instrument, + proctype combination. AUX fields are slower to search than CORE fields. + Acceptable values for ``instrument`` and ``proctype`` are listed in the + results of the :meth:`astroquery.noirlab.core.NOIRLabClass.categoricals` + method. + + Parameters + ---------- + instrument : :class:`str` + The specific instrument, *e.g.* '90prime' or 'decam'. + proctype : :class:`str` + 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. + cache : :class:`bool`, optional + If ``True`` cache the result locally. + + Returns + ------- + :class:`list` + A list of field descriptions, each a :class:`dict`. + """ + url = f'{self._fields_url(hdu=hdu, aux=True)}/{instrument}/{proctype}/' + response = self._request('GET', url, timeout=self.TIMEOUT, cache=cache) + response.raise_for_status() + return response.json() + + def categoricals(self, cache=True): + """List the currently acceptable values for each 'categorical field' + associated with Archive files. + + A 'categorical field' is one in which the values are restricted to a + specific set. The specific set may grow over time, but not often. + The categorical fields are: ``instrument``, ``obsmode``, ``obstype``, + ``proctype``, ``prodtype``, ``site``, ``survey``, ``telescope``. + + Parameters + ---------- + cache : :class:`bool`, optional + If ``True`` cache the result locally. + + Returns + ------- + :class:`dict` + A dictionary containing the category metadata. + """ + url = f'{self.NAT_URL}/api/adv_search/cat_lists/?format=json' + response = self._request('GET', url, timeout=self.TIMEOUT, cache=cache) + response.raise_for_status() + return response.json() + + 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": []} + + 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() + file = 'hdu' if hdu else 'file' + url = f'{self.NAT_URL}/api/adv_search/find/?rectype={file}&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()) + + def retrieve(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. + + 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() + + 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() diff --git a/astroquery/noirlab/tests/__init__.py b/astroquery/noirlab/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/astroquery/noirlab/tests/expected.py b/astroquery/noirlab/tests/expected.py new file mode 100644 index 0000000000..1204f2887d --- /dev/null +++ b/astroquery/noirlab/tests/expected.py @@ -0,0 +1,325 @@ +# Licensed under a 3-clause BSD style license - see LICENSE.rst +# flake8: noqa +""" +Expected values for online tests. For offline tests, these values +may be used to create mock responses. +""" + +service_metadata = [{'ParameterName': 'INPUT:DATE', + 'DataType': 'char', + 'DefaultValue': 'NA', + 'Doc': 'NA'}] + +query_region_1 = {'4066c45407961eab48eb16bcb9c5b4b7', + '554b2bcb3b2a4353c515d20d9fc29e4e', + '711ce162a7488a72b1629d994264eae7', + 'b9e211760ce74929919a55b225a9860c', + 'f350fff2e9db68f5d3ca74c91f0e374a', + 'f616cc460e51d2aa356ad21d79e22a21'} + +query_region_2 = {'78aed36ad9bfaa561d78845757d0aad6', + '1e04390654cf399781bbacccce78a790', + 'bf1aa2b1a3c6934bba18285572765d6f', + '7977c92b09724fb331b19263819962e4', + 'f990925c8d99a66f642e4463f1dde7f2', + '09535b88fc700227bb47979a670a7d85'} + +core_file_fields = [{'Field': 'archive_filename', 'Type': 'str', 'Desc': 'Filename assigned by the Archive'}, + {'Field': 'caldat', 'Type': 'datetime64', 'Desc': 'The local calendar date of the telescope, at the start of PM observing.'}, + {'Field': 'dateobs_center', 'Type': 'datetime64', 'Desc': 'DATE-OBS midpoint of range'}, + {'Field': 'dateobs_max', 'Type': 'datetime64', 'Desc': 'DATE-OBS min,max'}, + {'Field': 'dateobs_min', 'Type': 'datetime64', 'Desc': 'DATE-OBS min,max'},] + +aux_file_fields = [{'Field': 'AIRMASS', 'Type': 'str', 'Desc': 'airmass at approx. start of exposure'}, + {'Field': 'AOS', 'Type': 'str', 'Desc': 'AOS data available if true'}, + {'Field': 'ASTIG1', 'Type': 'str', 'Desc': '4MAPS correction 1'}, + {'Field': 'ASTIG2', 'Type': 'str', 'Desc': '4MAPS correction 2'}, + {'Field': 'ASTRMREF', 'Type': 'str', 'Desc': 'Astrometric ref. catalog'}, + {'Field': 'ATTNUM', 'Type': 'str', 'Desc': ''}, + {'Field': 'AVSIG', 'Type': 'str', 'Desc': ''}, + {'Field': 'AVSKY', 'Type': 'str', 'Desc': ''}, + {'Field': 'AZ', 'Type': 'str', 'Desc': ''}, + {'Field': 'BAND', 'Type': 'str', 'Desc': ''}] + +query_file_metadata = [' md5sum archive_filename original_filename instrument proc_type', + '-------------------------------- ----------------------------------------------------------------------------- ------------------------------------------------------------------------------------- ---------- ---------', + '0000f0c5015263610a75f0affed377d0 /net/archive/pipe/20161024/ct4m/2012B-0001/c4d_161025_034649_oki_z_v2.fits.fz /net/decdata1/deccp/NHPPS_DATA/DCP/Final/Submitted/c4d_161025_034649_oki_z_v2.fits.fz decam skysub', + '0001e5a5bcf039ebf0c53a6da32e8888 /net/archive/pipe/20160928/ct4m/2012B-0001/c4d_160929_090838_ooi_z_v2.fits.fz /net/decdata1/deccp/NHPPS_DATA/DCP/Final/Submitted/c4d_160929_090838_ooi_z_v2.fits.fz decam instcal', + '0003a1c853de73fc1796d2d7d77ca9cd /net/archive/pipe/20160909/ct4m/2012B-0001/c4d_160910_062930_opd_z_v2.fits.fz /net/decdata1/deccp/NHPPS_DATA/DCP/Final/Submitted/c4d_160910_062930_opd_z_v2.fits.fz decam resampled'] + +query_file_metadata_minimal = [' md5sum ', + '--------------------------------', + '0000004ab27d9e427bb93c640b358633', + '0000032cfbe72cc162eaec4c0a9ce6ec', + '0000041587917c9cb234c6116b396394', + '000005365aac087b505ce636c8ca573a', + '0000065ee227de201686b54706ba58e9'] + +query_file_meta_raw = [{'META': {'endpoint': 'adv_search/find'}, + 'PARAMETERS': {'rectype': 'file', + 'limit': 3, + 'default_limit': 1000, + 'default_offset': 0, + 'default_sort': 'md5sum', + 'oldest': None, + 'previd': None, + 'last': 3, + 'json_payload': {'outfields': ['md5sum', 'archive_filename', 'original_filename', 'instrument', 'proc_type'], + 'search': [['original_filename', 'c4d_', 'contains']]}}, + 'HEADER': {'md5sum': 'str', + 'archive_filename': 'str', + 'original_filename': 'str', + 'instrument': 'category', + 'proc_type': 'category'}}, + {'proc_type': 'skysub', + 'original_filename': '/net/decdata1/deccp/NHPPS_DATA/DCP/Final/Submitted/c4d_161025_034649_oki_z_v2.fits.fz', + 'md5sum': '0000f0c5015263610a75f0affed377d0', + 'instrument': 'decam', + 'archive_filename': '/net/archive/pipe/20161024/ct4m/2012B-0001/c4d_161025_034649_oki_z_v2.fits.fz'}, + {'proc_type': 'instcal', + 'original_filename': '/net/decdata1/deccp/NHPPS_DATA/DCP/Final/Submitted/c4d_160929_090838_ooi_z_v2.fits.fz', + 'md5sum': '0001e5a5bcf039ebf0c53a6da32e8888', + 'instrument': 'decam', + 'archive_filename': '/net/archive/pipe/20160928/ct4m/2012B-0001/c4d_160929_090838_ooi_z_v2.fits.fz'}, + {'proc_type': 'resampled', + 'original_filename': '/net/decdata1/deccp/NHPPS_DATA/DCP/Final/Submitted/c4d_160910_062930_opd_z_v2.fits.fz', + 'md5sum': '0003a1c853de73fc1796d2d7d77ca9cd', + 'instrument': 'decam', + 'archive_filename': '/net/archive/pipe/20160909/ct4m/2012B-0001/c4d_160910_062930_opd_z_v2.fits.fz'}] + +query_file_meta_raw_minimal = [{'META': {'endpoint': 'adv_search/find'}, + 'PARAMETERS': {'rectype': 'file', + 'limit': 5}, + 'HEADER': {'md5sum': 'str'}}, + {'md5sum': '0000004ab27d9e427bb93c640b358633',}, + {'md5sum': '0000032cfbe72cc162eaec4c0a9ce6ec',}, + {'md5sum': '0000041587917c9cb234c6116b396394',}, + {'md5sum': '000005365aac087b505ce636c8ca573a',}, + {'md5sum': '0000065ee227de201686b54706ba58e9',}] + +core_hdu_fields = [{'Field': 'dec_center', 'Type': 'np.float64', 'Desc': 'DEC center from pipeline processing'}, + {'Field': 'dec_max', 'Type': 'np.float64', 'Desc': 'DEC min,max range of HDU'}, + {'Field': 'dec_min', 'Type': 'np.float64', 'Desc': 'DEC min,max range of HDU'}, + {'Field': 'fitsfile', 'Type': 'str', 'Desc': ''}, + {'Field': 'hdu_idx', 'Type': 'np.int64', 'Desc': ''}, + {'Field': 'id', 'Type': 'str', 'Desc': ''}, + {'Field': 'ra_center', 'Type': 'np.float64', 'Desc': 'RA center from pipeline processing'}, + {'Field': 'ra_max', 'Type': 'np.float64', 'Desc': 'RA min,max range of HDU'}, + {'Field': 'ra_min', 'Type': 'np.float64', 'Desc': 'RA min,max range of HDU'}, + {'Field': 'updated', 'Type': 'datetime64', 'Desc': 'When Hdu created/updated'}] + +aux_hdu_fields = [{'Field': 'AMPMTCH', 'Type': 'str', 'Desc': ''}, + {'Field': 'ARAWGAIN', 'Type': 'str', 'Desc': '[e/adu] Average raw gain'}, + {'Field': 'AVSIG', 'Type': 'str', 'Desc': ''}, + {'Field': 'AVSKY', 'Type': 'str', 'Desc': ''}, + {'Field': 'BIASFIL', 'Type': 'str', 'Desc': 'Bias'}, + {'Field': 'BLDINTRP', 'Type': 'str', 'Desc': ''}, + {'Field': 'BPM', 'Type': 'str', 'Desc': ''}, + {'Field': 'BPMFIL', 'Type': 'str', 'Desc': 'BPM file used to build mask'}, + {'Field': 'CCDNUM', 'Type': 'str', 'Desc': ''}, + {'Field': 'CD1_1', 'Type': 'str', 'Desc': 'Coordinate matrix'}] + +query_hdu_metadata = ['EXPTIME md5sum caldat archive_filename hdu:EQUINOX instrument proc_type AIRMASS', + '------- -------------------------------- ---------- ----------------------------------------------------------------------- ----------- ---------- --------- -------', + ' 90.0 004a631d6f5493e8adeb7123255380cb 2017-08-15 /net/archive/mtn/20170815/ct4m/2017B-0110/c4d_170816_095757_ori.fits.fz 2000.0 decam raw 1.04', + ' 90.0 004a631d6f5493e8adeb7123255380cb 2017-08-15 /net/archive/mtn/20170815/ct4m/2017B-0110/c4d_170816_095757_ori.fits.fz 2000.0 decam raw 1.04', + ' 90.0 004a631d6f5493e8adeb7123255380cb 2017-08-15 /net/archive/mtn/20170815/ct4m/2017B-0110/c4d_170816_095757_ori.fits.fz 2000.0 decam raw 1.04'] + +query_hdu_metadata_raw = [{'META': {'endpoint': 'adv_search/find'}, + 'PARAMETERS': {'rectype': 'hdu', + 'limit': 3, + 'sort': 'md5sum', + 'default_limit': 1000, + 'default_offset': 0, + 'default_sort': 'fitsfile__md5sum,hdu_idx', + 'oldest': None, + 'previd': None, + 'last': 3, + 'json_payload': {'outfields': ['md5sum', 'archive_filename', 'caldat', 'instrument', 'proc_type', 'AIRMASS', 'EXPTIME'], + 'search': [['instrument', 'decam'], ['proc_type', 'raw'], ['caldat', '2017-08-14', '2017-08-16']]}}, + 'HEADER': {'EXPTIME': 'str', 'md5sum': 'str', 'caldat': 'datetime64', 'archive_filename': 'str', 'instrument': 'category', 'proc_type': 'category', 'AIRMASS': 'np.float64'}}, + {'EXPTIME': 90.0, 'md5sum': '004a631d6f5493e8adeb7123255380cb', 'caldat': '2017-08-15', 'archive_filename': '/net/archive/mtn/20170815/ct4m/2017B-0110/c4d_170816_095757_ori.fits.fz', 'instrument': 'decam', 'proc_type': 'raw', 'AIRMASS': 1.04}, + {'EXPTIME': 90.0, 'md5sum': '004a631d6f5493e8adeb7123255380cb', 'caldat': '2017-08-15', 'archive_filename': '/net/archive/mtn/20170815/ct4m/2017B-0110/c4d_170816_095757_ori.fits.fz', 'instrument': 'decam', 'proc_type': 'raw', 'AIRMASS': 1.04}, + {'EXPTIME': 90.0, 'md5sum': '004a631d6f5493e8adeb7123255380cb', 'caldat': '2017-08-15', 'archive_filename': '/net/archive/mtn/20170815/ct4m/2017B-0110/c4d_170816_095757_ori.fits.fz', 'instrument': 'decam', 'proc_type': 'raw', 'AIRMASS': 1.04}] + +query_hdu_metadata_raw = [{'META': {'endpoint': 'adv_search/find'}, + 'PARAMETERS': {'rectype': 'hdu', + 'limit': 3, + 'sort': 'md5sum', + 'default_limit': 1000, + 'default_offset': 0, + 'default_sort': 'fitsfile__md5sum,hdu_idx', + 'oldest': None, + 'previd': None, + 'last': 3, + 'json_payload': {'outfields': ['md5sum', 'archive_filename', 'caldat', 'instrument', 'proc_type', 'AIRMASS', 'EXPTIME', 'hdu:EQUINOX'], + 'search': [['instrument', 'decam'], ['proc_type', 'raw'], ['caldat', '2017-08-14', '2017-08-16']]}}, + 'HEADER': {'EXPTIME': 'str', 'md5sum': 'str', 'caldat': 'datetime64', 'archive_filename': 'str', 'hdu:EQUINOX': 'str', 'instrument': 'category', 'proc_type': 'category', 'AIRMASS': 'np.float64'}}, + {'hdu:EQUINOX': 2000.0, 'EXPTIME': 90.0, 'md5sum': '004a631d6f5493e8adeb7123255380cb', 'caldat': '2017-08-15', 'archive_filename': '/net/archive/mtn/20170815/ct4m/2017B-0110/c4d_170816_095757_ori.fits.fz', 'instrument': 'decam', 'proc_type': 'raw', 'AIRMASS': 1.04}, + {'hdu:EQUINOX': 2000.0, 'EXPTIME': 90.0, 'md5sum': '004a631d6f5493e8adeb7123255380cb', 'caldat': '2017-08-15', 'archive_filename': '/net/archive/mtn/20170815/ct4m/2017B-0110/c4d_170816_095757_ori.fits.fz', 'instrument': 'decam', 'proc_type': 'raw', 'AIRMASS': 1.04}, + {'hdu:EQUINOX': 2000.0, 'EXPTIME': 90.0, 'md5sum': '004a631d6f5493e8adeb7123255380cb', 'caldat': '2017-08-15', 'archive_filename': '/net/archive/mtn/20170815/ct4m/2017B-0110/c4d_170816_095757_ori.fits.fz', 'instrument': 'decam', 'proc_type': 'raw', 'AIRMASS': 1.04}] + +categoricals = {'instruments': ['(p)odi', + '90prime', + 'andicam', + 'arcoiris', + 'arcon', + 'bench', + 'ccd_imager', + 'ccd_spec', + 'chiron', + 'cosmos', + 'cpapir', + 'decam', + 'echelle', + 'falmingos', + 'flamingos', + 'ghts_blue', + 'ghts_blue_imager', + 'ghts_red', + 'ghts_red_imager', + 'goodman', + 'goodman spectrograph', + 'gtcam', + 'hdi', + 'hlsp_bok23m', + 'hlsp_decam', + 'hlsp_ir_imager', + 'hlsp_mosaic', + 'hlsp_mosaic3', + 'hlsp_mosaic_2', + 'ice', + 'ir_imager', + 'ispi', + 'kosmos', + 'minimo/ice', + 'mop/ice', + 'mosaic', + 'mosaic3', + 'mosaic_1', + 'mosaic_1_1', + 'mosaic_2', + 'newfirm', + 'osiris', + 'pfccd', + 'sami', + 'soi', + 'spartan', + 'spartan ir camera', + 'triplespec', + 'wfc', + 'whirc', + 'wildfire', + 'y4kcam'], + 'obsmodes': ['acq', + 'imaging', + 'sos_slit', + 'spec', + 'stare'], + 'obstypes': ['acq', + 'acquire', + 'arc', + 'bias', + 'calibration', + 'calibration or comparison', + 'comp', + 'comparison', + 'dark', + 'dflat', + 'dome flat', + 'dome or projector flat', + 'domeflat', + 'expose', + 'flat', + 'focus', + 'fringe', + 'fringecor', + 'guider', + 'illum', + 'illumcor', + 'illumination calibration', + 'junk', + 'lampflat', + 'none', + 'nota', + 'obj', + 'object', + 'phot flat', + 'photometric standard', + 'projector', + 'projector flat', + 'pupil', + 'red', + 'remap', + 'sflat', + 'sky', + 'sky flat', + 'skyflat', + 'spectrum', + 'standard', + 'std', + 'test', + 'unknown', + 'zero'], + 'proctypes': ['instcal', + 'mastercal', + 'nota', + 'projected', + 'raw', + 'resampled', + 'skysub', + 'stacked', + 'starsubtracted'], + 'prodtypes': ['chi2', + 'depth', + 'dqmask', + 'expmap', + 'flag', + 'galdepth', + 'graphics (size)', + 'image', + 'image 2nd version 1', + 'image1', + 'invvar', + 'maskbits', + 'model', + 'ncomb', + 'nexp', + 'nota', + 'nrej', + 'psfsize', + 'resampled', + 'rms', + 'sigma', + 'weight', + 'wtmap'], + 'sites': ['cp', + 'ct', + 'kp', + 'lp'], + 'surveys': [], + 'telescopes': ['bok23m', + 'ct09m', + 'ct13m', + 'ct15m', + 'ct1m', + 'ct4m', + 'ctlab', + 'kp09m', + 'kp21m', + 'kp35m', + 'kp4m', + 'kpcf', + 'lp25m', + 'soar', + 'wiyn']} + +retrieve = {'DATE': '2009-09-06T20:13:15', + 'TELESCOP': 'KPNO 4.0 meter telescope', + 'INSTRUME': 'newfirm', + 'CHECKSUM': '7GBmAGAm7GAmAGAm', + 'DATASUM': '0'} + +version = '6.0' + +get_token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9' diff --git a/astroquery/noirlab/tests/test_noirlab.py b/astroquery/noirlab/tests/test_noirlab.py new file mode 100644 index 0000000000..ef6bfa1b09 --- /dev/null +++ b/astroquery/noirlab/tests/test_noirlab.py @@ -0,0 +1,179 @@ +# Licensed under a 3-clause BSD style license - see LICENSE.rst +""" +Test astroquery.noirlab, but monkeypatch any HTTP requests. +""" +import json +import pytest +from astropy import units as u +from astropy.coordinates import SkyCoord +from ...utils.mocks import MockResponse +from ...exceptions import RemoteServiceError +from .. import NOIRLab +from . import expected as exp + + +def mock_content(method, url, **kwargs): + if 'FORMAT=METADATA' in url: + content = json.dumps(exp.service_metadata) + elif '/sia/voimg' in url: + raw_json = [{'HEADER': {'md5sum': 'str'}}] + [{'md5sum': m} for m in exp.query_region_1] + content = json.dumps(raw_json) + elif '/sia/vohdu' in url: + raw_json = [{'HEADER': {'md5sum': 'str'}}] + [{'md5sum': m} for m in exp.query_region_2] + content = json.dumps(raw_json) + elif '/core_file_fields' in url: + content = json.dumps(exp.core_file_fields) + elif '/aux_file_fields' in url: + content = json.dumps(exp.aux_file_fields) + elif '/find/?rectype=file&limit=3' in url: + content = json.dumps(exp.query_file_meta_raw) + elif '/find/?rectype=file&limit=5' in url: + content = json.dumps(exp.query_file_meta_raw_minimal) + elif '/find/?rectype=hdu&limit=3' in url: + content = json.dumps(exp.query_hdu_metadata_raw) + elif '/core_hdu_fields' in url: + content = json.dumps(exp.core_hdu_fields) + elif '/aux_hdu_fields' in url: + content = json.dumps(exp.aux_hdu_fields) + elif '/find?rectype=hdu' in url: + content = json.dumps(exp.query_hdu_metadata) + elif '/cat_lists/' in url: + content = json.dumps(exp.categoricals) + elif '/version/' in url: + content = exp.version + elif '/get_token/' in url: + content = f'"{exp.get_token}"' + return MockResponse(content=content.encode('utf-8'), url=url) + + +@pytest.fixture +def patch_request(monkeypatch): + monkeypatch.setattr(NOIRLab, '_request', mock_content) + return monkeypatch + + +@pytest.mark.parametrize('hdu', [(False,), (True,)]) +def test_service_metadata(patch_request, hdu): + """Test compliance with 6.1 of SIA spec v1.0. + """ + actual = NOIRLab.service_metadata(hdu=hdu) + assert actual[0] == exp.service_metadata[0] + + +@pytest.mark.parametrize('hdu,radius', [(False, '0.1'), (True, '0.07')]) +def test_query_region(patch_request, hdu, radius): + """Search a region. + + Ensure query gets at least the set of files we expect. + It is OK if more files have been added to the remote Archive. + """ + c = SkyCoord(ra=10.625*u.degree, dec=41.2*u.degree, frame='icrs') + r = NOIRLab.query_region(c, radius=radius, hdu=hdu) + actual = set(r['md5sum'].tolist()) + if hdu: + expected = exp.query_region_2 + else: + expected = exp.query_region_1 + assert expected.issubset(actual) + + +@pytest.mark.parametrize('hdu', [(False,), (True,)]) +def test_core_fields(patch_request, hdu): + """List the available CORE fields. + """ + actual = NOIRLab.core_fields(hdu=hdu) + if hdu: + assert actual == exp.core_hdu_fields + else: + assert actual == exp.core_file_fields + + +@pytest.mark.parametrize('hdu', [(False,), (True,)]) +def test_aux_fields(patch_request, hdu): + """List the available AUX fields. + """ + actual = NOIRLab.aux_fields('decam', 'instcal', hdu=hdu) + if hdu: + assert actual == exp.aux_hdu_fields + else: + assert actual == exp.aux_file_fields + + +def test_query_file_metadata(patch_request): + """Search FILE metadata. + """ + qspec = {"outfields": ["md5sum", + "archive_filename", + "original_filename", + "instrument", + "proc_type"], + "search": [['original_filename', 'c4d_', 'contains']]} + actual = NOIRLab.query_metadata(qspec, limit=3) + assert actual.pformat(max_width=-1) == exp.query_file_metadata + + +def test_query_file_metadata_minimal_input(patch_request): + """Search FILE metadata with minimum input parameters. + """ + actual = NOIRLab.query_metadata(qspec=None, limit=5) + assert actual.pformat(max_width=-1) == exp.query_file_metadata_minimal + + +def test_query_hdu_metadata(patch_request): + """Search HDU metadata. + """ + qspec = {"outfields": ["md5sum", + "archive_filename", + "caldat", + "instrument", + "proc_type", + "EXPTIME", + "AIRMASS", + "hdu:EQUINOX"], + "search": [["caldat", "2017-08-14", "2017-08-16"], + ["instrument", "decam"], + ["proc_type", "raw"]]} + actual = NOIRLab.query_metadata(qspec, sort='md5sum', limit=3, hdu=True) + assert actual.pformat(max_width=-1) == exp.query_hdu_metadata + + +def test_categoricals(patch_request): + """List categories. + """ + actual = NOIRLab.categoricals() + assert actual == exp.categoricals + + +def test_version(patch_request): + """Test the API version. + """ + actual = NOIRLab.version() + assert actual >= float(exp.version) + + +def test_api_version(patch_request): + """Test the API version as a property. + """ + actual = NOIRLab.api_version + assert actual >= float(exp.version) + + +def test__validate_version(patch_request): + """Check exception raised by outdated API version. + """ + actual_api = NOIRLab.api_version + NOIRLab._api_version = 9.8 + with pytest.raises(RemoteServiceError) as e: + NOIRLab._validate_version() + assert e.value.args[0] == ('The astroquery.noirlab module is expecting an older ' + 'version of the https://astroarchive.noirlab.edu API services. ' + 'Please upgrade to latest astroquery. ' + 'Expected version 6.0 but got 9.8 from the API.') + NOIRLab._api_version = actual_api + + +def test_get_token(patch_request): + """Test token retrieval. + """ + actual = NOIRLab.get_token('nobody@university.edu', '123456') + assert actual == exp.get_token diff --git a/astroquery/noirlab/tests/test_noirlab_remote.py b/astroquery/noirlab/tests/test_noirlab_remote.py new file mode 100644 index 0000000000..30f773967b --- /dev/null +++ b/astroquery/noirlab/tests/test_noirlab_remote.py @@ -0,0 +1,146 @@ +# Licensed under a 3-clause BSD style license - see LICENSE.rst +""" +Performs similar tests as test_noirlab.py, but performs +the actual HTTPS request rather than monkeypatching it. +Enable with *e.g.*:: + + tox -e py310-test-online -- -P noirlab +""" +import pytest +from astropy import units as u +from astropy.coordinates import SkyCoord +from .. import NOIRLab +from . import expected as exp + + +@pytest.mark.remote_data +@pytest.mark.parametrize('hdu', [(False,), (True,)]) +def test_service_metadata(hdu): + """Test compliance with 6.1 of SIA spec v1.0. + """ + actual = NOIRLab().service_metadata(hdu=hdu) + assert actual[0] == exp.service_metadata[0] + + +@pytest.mark.remote_data +@pytest.mark.parametrize('hdu,radius', [(False, '0.1'), (True, '0.07')]) +def test_query_region(hdu, radius): + """Search a region. + + Ensure query gets at least the set of files we expect. + It is OK if more files have been added to the remote Archive. + """ + c = SkyCoord(ra=10.625*u.degree, dec=41.2*u.degree, frame='icrs') + r = NOIRLab().query_region(c, radius=radius, hdu=hdu) + actual = set(r['md5sum'].tolist()) + if hdu: + expected = exp.query_region_2 + else: + expected = exp.query_region_1 + assert expected.issubset(actual) + + +@pytest.mark.remote_data +@pytest.mark.parametrize('hdu', [(False,), (True,)]) +def test_core_fields(hdu): + """List the available CORE fields. + """ + actual = NOIRLab().core_fields(hdu=hdu) + if hdu: + assert actual == exp.core_hdu_fields + else: + assert actual[:5] == exp.core_file_fields + + +@pytest.mark.remote_data +@pytest.mark.parametrize('hdu', [(False,), (True,)]) +def test_aux_fields(hdu): + """List the available AUX fields. + """ + actual = NOIRLab().aux_fields('decam', 'instcal', hdu=hdu) + if hdu: + assert actual[:10] == exp.aux_hdu_fields + else: + assert actual[:10] == exp.aux_file_fields + + +@pytest.mark.remote_data +def test_categoricals(): + """List categories. + """ + actual = NOIRLab().categoricals() + assert actual == exp.categoricals + + +@pytest.mark.remote_data +def test_query_file_metadata(): + """Search FILE metadata. + """ + qspec = {"outfields": ["md5sum", + "archive_filename", + "original_filename", + "instrument", + "proc_type"], + "search": [['original_filename', 'c4d_', 'contains']]} + actual = NOIRLab().query_metadata(qspec, sort='md5sum', limit=3) + assert actual.pformat(max_width=-1) == exp.query_file_metadata + + +@pytest.mark.remote_data +def test_query_file_metadata_minimal_input(): + """Search FILE metadata with minimum input parameters. + """ + actual = NOIRLab().query_metadata(qspec=None, sort='md5sum', limit=5) + assert actual.pformat(max_width=-1) == exp.query_file_metadata_minimal + + +@pytest.mark.remote_data +def test_query_hdu_metadata(): + """Search HDU metadata. + """ + qspec = {"outfields": ["md5sum", + "archive_filename", + "caldat", + "instrument", + "proc_type", + "EXPTIME", + "AIRMASS", + "hdu:EQUINOX"], + "search": [["caldat", "2017-08-14", "2017-08-16"], + ["instrument", "decam"], + ["proc_type", "raw"]]} + actual = NOIRLab().query_metadata(qspec, sort='md5sum', limit=3, hdu=True) + assert actual.pformat(max_width=-1) == exp.query_hdu_metadata + + +@pytest.mark.remote_data +def test_retrieve(): + hdulist = NOIRLab().retrieve('f92541fdc566dfebac9e7d75e12b5601') + for key in exp.retrieve: + assert key in hdulist[0].header + assert hdulist[0].header[key] == exp.retrieve[key] + hdulist.close() + + +@pytest.mark.remote_data +def test_version(): + """Test the API version. + """ + actual = NOIRLab().version() + assert actual >= float(exp.version) + + +@pytest.mark.remote_data +def test_api_version(): + """Test the API version as a property. + """ + actual = NOIRLab().api_version + assert actual >= float(exp.version) + + +@pytest.mark.remote_data +def test_get_token(): + """Test token retrieval. + """ + actual = NOIRLab().get_token('nobody@university.edu', '123456') + assert actual.split('.')[0] == exp.get_token diff --git a/docs/index.rst b/docs/index.rst index 1f6f5e7850..207f3388ab 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -284,6 +284,7 @@ The following modules have been completed using a common API: nasa_ads/nasa_ads.rst ipac/ned/ned.rst nist/nist.rst + noirlab/noirlab.rst nvas/nvas.rst simbad/simbad.rst skyview/skyview.rst diff --git a/docs/noirlab/noirlab.rst b/docs/noirlab/noirlab.rst new file mode 100644 index 0000000000..71d0372943 --- /dev/null +++ b/docs/noirlab/noirlab.rst @@ -0,0 +1,171 @@ +.. _astroquery.noirlab: + +************************************** +NOIRLab Queries (`astroquery.noirlab`) +************************************** + +Introduction +============ + +The methods in this module are wrappers around a set of web services +described in the +`REST API documentation `_. +This data archive is hosted at the +`Community Science and Data Center (CDSC) `_. + + +About the NSF NOIRLab Astro Data Archive +======================================== + +The NOIRLab Astro Data Archive (formerly NOAO Science Archive) provides +access to data taken with more than 40 telescope and instrument combinations, +including those operated in partnership with the WIYN, SOAR and SMARTS +consortia, from semester 2004B to the present. In addition to raw data, +pipeline-reduced data products from the DECam, Mosaic and NEWFIRM imagers +are also available, as well as advanced data products delivered by teams +carrying out surveys and other large observing programs with NSF NOIRLab +facilities. + +For more info about our holdings see the +`NSF NOIRLab Astro Data Archive `_. + +Acknowledgment +~~~~~~~~~~~~~~ + +This research uses services or data provided by the Astro Data Archive at +NSF's NOIRLab. NOIRLab is operated by the Association of Universities for +Research in Astronomy (AURA), Inc. under a cooperative agreement with the +National Science Foundation. + + +Getting started (SIA) +===================== + +This module supports fetching the table of observation summaries from +the `NOIRLab data archive `_ given +your *Region Of Interest* query values. + +In general, you can query the +`NOIRLab data archive `_ +against full FITS files or against HDUs of those FITS files. Most +users will likely prefer results against full FITS files (the +default). The search will likely be faster when searching files +compared to searching HDUs. If you are trying to retrieve HDU specific +image data from large files that contain many HDUs (such as DECam), +you can reduce your download time considerably by getting only +matching HDUs. + +The results are returned in a `~astropy.table.Table`. The service +can be queried using the :meth:`~astroquery.noirlab.NOIRLabClass.query_region`. The +only required argument to this is the target coordinates around which +to query. Specify the coordinates using the appropriate coordinate system from +`astropy.coordinates`. Here is a basic example: + +.. doctest-remote-data:: + + >>> from astroquery.noirlab import NOIRLab + >>> from astropy import units as u + >>> from astropy.coordinates import SkyCoord + >>> coord = SkyCoord(ra=10.625*u.degree, dec=41.2*u.degree, frame='icrs') + >>> results_file = NOIRLab.query_region(coord, radius='0.1') + >>> mosaic_filter = results_file['instrument'] == 'mosaic3' + >>> print(results_file[mosaic_filter][['archive_filename', 'ra_center', 'dec_center']]) + archive_filename ra_center dec_center + ----------------------------------------------------------------------- ------------------ ----------------- + /net/archive/mtn/20151120/kp4m/2015B-2001/k4m_151121_041031_ori.fits.fz 10.579374999999999 41.19416666666666 + /net/archive/mtn/20151120/kp4m/2015B-2001/k4m_151121_031258_ori.fits.fz 10.579374999999999 41.19416666666666 + /net/archive/mtn/20151027/kp4m/2015B-2001/k4m_151028_085849_ori.fits.fz 10.672041666666665 41.24944166666667 + /net/archive/mtn/20151027/kp4m/2015B-2001/k4m_151028_084112_ori.fits.fz 10.672041666666665 41.24944166666667 + /net/archive/mtn/20151120/kp4m/2015B-2001/k4m_151121_033641_ori.fits.fz 10.579374999999999 41.19416666666666 + /net/archive/mtn/20151027/kp4m/2015B-2001/k4m_151028_091327_ori.fits.fz 10.672041666666665 41.24944166666667 + +This is an example of searching by HDU. + +.. note:: + + Only some instruments have pipeline processing that populates the RA, DEC fields used for this search. + +.. doctest-remote-data:: + + >>> results_hdu = NOIRLab.query_region(coord, radius='0.1', hdu=True) + >>> mosaic_hdu_filter = results_hdu['instrument'] == 'mosaic3' + >>> print(results_hdu[mosaic_hdu_filter][['archive_filename', 'hdu_idx']]) + archive_filename hdu_idx + ------------------------------------------------------------------------------- ------- + /net/archive/pipe/20180211/kp4m/2016A-0453/k4m_180212_030259_ooi_zd_v2.fits.fz 0 + /net/archive/pipe/20180211/kp4m/2016A-0453/k4m_180212_030259_ooi_zd_v2.fits.fz 1 + /net/archive/pipe/20180211/kp4m/2016A-0453/k4m_180212_021444_ooi_zd_ls9.fits.fz 2 + /net/archive/pipe/20151120/kp4m/2015B-2001/k4m_151121_031124_ooi_zd_v1.fits.fz 3 + /net/archive/pipe/20151120/kp4m/2015B-2001/k4m_151121_031819_ooi_zd_v1.fits.fz 0 + /net/archive/pipe/20151120/kp4m/2015B-2001/k4m_151121_031426_ooi_zd_v1.fits.fz 0 + /net/archive/pipe/20151120/kp4m/2015B-2001/k4m_151121_031426_ooi_zd_v1.fits.fz 1 + /net/archive/pipe/20151120/kp4m/2015B-2001/k4m_151121_031038_ooi_zd_v1.fits.fz 2 + /net/archive/pipe/20151120/kp4m/2015B-2001/k4m_151121_031038_ooi_zd_v1.fits.fz 3 + /net/archive/pipe/20151120/kp4m/2015B-2001/k4m_151121_031646_ooi_zd_v1.fits.fz 0 + /net/archive/pipe/20180211/kp4m/2016A-0453/k4m_180212_030259_ooi_zd_ls9.fits.fz 0 + /net/archive/pipe/20180211/kp4m/2016A-0453/k4m_180212_030259_ooi_zd_ls9.fits.fz 1 + /net/archive/pipe/20151120/kp4m/2015B-2001/k4m_151121_030855_ooi_zd_v1.fits.fz 2 + /net/archive/pipe/20151120/kp4m/2015B-2001/k4m_151121_031731_ooi_zd_v1.fits.fz 0 + /net/archive/pipe/20180211/kp4m/2016A-0453/k4m_180212_023526_ooi_zd_v2.fits.fz 1 + /net/archive/pipe/20180211/kp4m/2016A-0453/k4m_180212_030041_ooi_zd_ls9.fits.fz 2 + /net/archive/pipe/20180211/kp4m/2016A-0453/k4m_180212_030041_ooi_zd_ls9.fits.fz 3 + /net/archive/pipe/20180206/kp4m/2016A-0453/k4m_180207_024709_ooi_zd_v2.fits.fz 2 + /net/archive/pipe/20180211/kp4m/2016A-0453/k4m_180212_021444_ooi_zd_v2.fits.fz 2 + /net/archive/pipe/20151120/kp4m/2015B-2001/k4m_151121_031342_ooi_zd_v1.fits.fz 0 + /net/archive/pipe/20151120/kp4m/2015B-2001/k4m_151121_031342_ooi_zd_v1.fits.fz 1 + /net/archive/pipe/20180211/kp4m/2016A-0453/k4m_180212_030041_ooi_zd_v2.fits.fz 2 + /net/archive/pipe/20180211/kp4m/2016A-0453/k4m_180212_030041_ooi_zd_v2.fits.fz 3 + /net/archive/pipe/20151120/kp4m/2015B-2001/k4m_151121_030945_ooi_zd_v1.fits.fz 2 + /net/archive/pipe/20151120/kp4m/2015B-2001/k4m_151121_030809_ooi_zd_v1.fits.fz 2 + /net/archive/pipe/20180211/kp4m/2016A-0453/k4m_180212_023526_ooi_zd_ls9.fits.fz 1 + /net/archive/pipe/20151120/kp4m/2015B-2001/k4m_151121_031511_ooi_zd_v1.fits.fz 1 + /net/archive/pipe/20180206/kp4m/2016A-0453/k4m_180207_024709_ooi_zd_ls9.fits.fz 2 + /net/archive/pipe/20151120/kp4m/2015B-2001/k4m_151121_031904_ooi_zd_v1.fits.fz 0 + /net/archive/pipe/20151120/kp4m/2015B-2001/k4m_151121_030717_ooi_zd_v1.fits.fz 2 + /net/archive/pipe/20151120/kp4m/2015B-2001/k4m_151121_031558_ooi_zd_v1.fits.fz 1 + + +Advanced Search +=============== + +This set of methods supports *arbitrary searches of any fields* +stored in the FITS headers of the Archive. Common fields ("core" +fields) are optimized for search speed. Less common fields ("aux" +fields) will be slower to search. You can search by File or HDU. The +primary method for doing the search is +:meth:`~astroquery.noirlab.NOIRLabClass.query_metadata`. That query +requires a JSON_ structure to define the query. We often call this +the *JSON search spec*. Additional methods in this module +provide information needed to construct the JSON_ structure. +Summaries of the mechanisms available in the JSON search spec for +`File search `_ +and for `HDU search +`_ +are on the NSF NOIRLab Data Archive website. + +These methods provide information needed to fill in a JSON_ query structure: + +#. :meth:`~astroquery.noirlab.NOIRLabClass.aux_fields` +#. :meth:`~astroquery.noirlab.NOIRLabClass.core_fields` +#. :meth:`~astroquery.noirlab.NOIRLabClass.categoricals` + +See the Reference/API below for details. The +:meth:`~astroquery.noirlab.NOIRLabClass.categoricals` method +returns a list of all the "category strings" such as names of +Instruments and Telescopes. The :meth:`~astroquery.noirlab.NOIRLabClass.aux_fields` +and :meth:`~astroquery.noirlab.NOIRLabClass.core_fields` methods +tell you what fields are available to search. The core fields are +available for all instruments; searching on these fields is optimized +for speed. The aux fields require you to specify instrument and proctype. +The set of aux fields available is highly dependent on those two fields. The +instrument determines aux fields in raw files. Proctype determines +what kind of pipeline processing was done. Pipeline processing often +adds important additional aux fields. + +.. _JSON: https://www.json.org/json-en.html + +Reference/API +============= + +.. automodapi:: astroquery.noirlab + :no-inheritance-diagram: