-
-
Notifications
You must be signed in to change notification settings - Fork 441
Access to NOIRLab Astro Data Archive #3359
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
37 commits
Select commit
Hold shift + click to select a range
1283eca
add files from NOAO fork
weaverba137 6ac7889
add noirlab.rst to index
weaverba137 7aef32f
update change log
weaverba137 6503611
update url and tests
weaverba137 c40d1a6
fix remote data import
weaverba137 89c8b8e
fix doc errors
weaverba137 f90205d
add local test
weaverba137 0291e6b
adding offline tests
weaverba137 45cc587
ongoing test refactoring
weaverba137 6c45e4e
fix test patching
weaverba137 10c5c90
adding more tests
weaverba137 acd0d30
fix docstring
weaverba137 ac0ead5
adding more tests
weaverba137 5efeb08
activate additional tests and add placeholders
weaverba137 991bdb0
fix style issue
weaverba137 eeb1267
working on doc strings
weaverba137 e110858
fix pformat issues
weaverba137 8dc644f
update docs
weaverba137 9705da9
add test coverage for some corner cases
weaverba137 e1d6387
fix style & add placeholders
weaverba137 24da457
tweak formatting
weaverba137 37cd840
update and sync docs
weaverba137 b56a52b
activate hdu metadata tests
weaverba137 66d39d0
all tests now active
weaverba137 fb6973b
fix remote data error
weaverba137 8df43b7
fix doc compilation errors
weaverba137 ac78b9e
updates based on initial review
weaverba137 b28e6d6
fix style check
weaverba137 5bbfd54
set up to ignore column prefixes
weaverba137 3669068
update tests with some workarounds
weaverba137 55e43c4
consolidate some methods
weaverba137 ec8713c
Fixed all remote tests
weaverba137 669d636
change header underlining
weaverba137 ea0677e
clean up some doc warnings
weaverba137 cbc3a21
promote sia_url to public method
weaverba137 ffef537
Fix references to async_ keyword
weaverba137 8457192
remove some strict requirements on remote tests
weaverba137 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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'] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,347 @@ | ||
| # 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 | ||
|
|
||
|
|
||
| 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) | ||
|
|
||
| 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() | ||
|
|
||
| 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) | ||
|
|
||
| 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() | ||
|
|
||
| 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()) | ||
|
|
||
| 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() | ||
|
|
||
| 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() | ||
Empty file.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.