Skip to content

Commit eb21d69

Browse files
committed
WIP: nrao archive query - TAP only so far, based on ALMA
1 parent 7ffe123 commit eb21d69

File tree

2 files changed

+345
-0
lines changed

2 files changed

+345
-0
lines changed

astroquery/nrao/__init__.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Licensed under a 3-clause BSD style license - see LICENSE.rst
2+
"""
3+
NRAO Archive service.
4+
"""
5+
from astropy import config as _config
6+
7+
8+
# list the URLs here separately so they can be used in tests.
9+
_url_list = ['https://data.nrao.edu'
10+
]
11+
12+
tap_urls = ['https://data-query.nrao.edu/']
13+
14+
auth_urls = ['data.nrao.edu']
15+
16+
17+
class Conf(_config.ConfigNamespace):
18+
"""
19+
Configuration parameters for `astroquery.nrao`.
20+
"""
21+
22+
timeout = _config.ConfigItem(60, "Timeout in seconds.")
23+
24+
archive_url = _config.ConfigItem(
25+
_url_list,
26+
'The NRAO Archive mirror to use.')
27+
28+
auth_url = _config.ConfigItem(
29+
auth_urls,
30+
'NRAO Central Authentication Service URLs'
31+
)
32+
33+
username = _config.ConfigItem(
34+
"",
35+
'Optional default username for NRAO archive.')
36+
37+
38+
conf = Conf()
39+
40+
from .core import Nrao, NraoClass, NRAO_BANDS
41+
42+
__all__ = ['Nrao', 'NraoClass',
43+
'Conf', 'conf',
44+
]

astroquery/nrao/core.py

Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
# Licensed under a 3-clause BSD style license - see LICENSE.rst
2+
3+
import os.path
4+
import keyring
5+
import numpy as np
6+
import re
7+
import tarfile
8+
import string
9+
import requests
10+
import warnings
11+
12+
from pkg_resources import resource_filename
13+
from bs4 import BeautifulSoup
14+
import pyvo
15+
from urllib.parse import urljoin
16+
17+
from astropy.table import Table, Column, vstack
18+
from astroquery import log
19+
from astropy.utils.console import ProgressBar
20+
from astropy import units as u
21+
from astropy.time import Time
22+
23+
try:
24+
from pyvo.dal.sia2 import SIA2_PARAMETERS_DESC, SIA2Service
25+
except ImportError:
26+
# Can be removed once min version of pyvo is 1.5
27+
from pyvo.dal.sia2 import SIA_PARAMETERS_DESC as SIA2_PARAMETERS_DESC
28+
from pyvo.dal.sia2 import SIAService as SIA2Service
29+
30+
from ..exceptions import LoginError
31+
from ..utils import commons
32+
from ..utils.process_asyncs import async_to_sync
33+
from ..query import BaseQuery, QueryWithLogin, BaseVOQuery
34+
from . import conf, auth_urls, tap_urls
35+
from astroquery.exceptions import CorruptDataWarning
36+
37+
__all__ = {'NraoClass',}
38+
39+
__doctest_skip__ = ['NraoClass.*']
40+
41+
NRAO_BANDS = {}
42+
43+
TAP_SERVICE_PATH = 'tap'
44+
45+
NRAO_FORM_KEYS = {
46+
'Position': {
47+
'Source name (astropy Resolver)': ['source_name_resolver',
48+
'SkyCoord.from_name', _gen_pos_sql],
49+
'Source name (NRAO)': ['source_name_alma', 'target_name', _gen_str_sql],
50+
'RA Dec (Sexagesimal)': ['ra_dec', 's_ra, s_dec', _gen_pos_sql],
51+
'Galactic (Degrees)': ['galactic', 'gal_longitude, gal_latitude',
52+
_gen_pos_sql],
53+
'Angular resolution (arcsec)': ['spatial_resolution',
54+
'spatial_resolution', _gen_numeric_sql],
55+
'Largest angular scale (arcsec)': ['spatial_scale_max',
56+
'spatial_scale_max', _gen_numeric_sql],
57+
'Field of view (arcsec)': ['fov', 's_fov', _gen_numeric_sql]
58+
},
59+
}
60+
61+
62+
def _gen_sql(payload):
63+
sql = 'select * from tap_schema.obscore'
64+
where = ''
65+
unused_payload = payload.copy()
66+
if payload:
67+
for constraint in payload:
68+
for attrib_category in NRAO_FORM_KEYS.values():
69+
for attrib in attrib_category.values():
70+
if constraint in attrib:
71+
# use the value and the second entry in attrib which
72+
# is the new name of the column
73+
val = payload[constraint]
74+
if constraint == 'em_resolution':
75+
# em_resolution does not require any transformation
76+
attrib_where = _gen_numeric_sql(constraint, val)
77+
else:
78+
attrib_where = attrib[2](attrib[1], val)
79+
if attrib_where:
80+
if where:
81+
where += ' AND '
82+
else:
83+
where = ' WHERE '
84+
where += attrib_where
85+
86+
# Delete this key to see what's left over afterward
87+
#
88+
# Use pop to avoid the slight possibility of trying to remove
89+
# an already removed key
90+
unused_payload.pop(constraint)
91+
92+
if unused_payload:
93+
# Left over (unused) constraints passed. Let the user know.
94+
remaining = [f'{p} -> {unused_payload[p]}' for p in unused_payload]
95+
raise TypeError(f'Unsupported arguments were passed:\n{remaining}')
96+
97+
return sql + where
98+
99+
100+
class NraoAuth(BaseVOQuery, BaseQuery):
101+
pass
102+
103+
class NraoClass(BaseQuery):
104+
TIMEOUT = conf.timeout
105+
archive_url = conf.archive_url
106+
USERNAME = conf.username
107+
108+
def __init__(self):
109+
# sia service does not need disambiguation but tap does
110+
super().__init__()
111+
self._sia = None
112+
self._tap = None
113+
self._datalink = None
114+
self._sia_url = None
115+
self._tap_url = None
116+
self._datalink_url = None
117+
self._auth = NraoAuth()
118+
119+
@property
120+
def auth(self):
121+
return self._auth
122+
123+
@property
124+
def datalink(self):
125+
if not self._datalink:
126+
self._datalink = pyvo.dal.adhoc.DatalinkService(self.datalink_url)
127+
return self._datalink
128+
129+
@property
130+
def datalink_url(self):
131+
if not self._datalink_url:
132+
try:
133+
self._datalink_url = urljoin(self._get_dataarchive_url(), DATALINK_SERVICE_PATH)
134+
except requests.exceptions.HTTPError as err:
135+
log.debug(
136+
f"ERROR getting the NRAO Archive URL: {str(err)}")
137+
raise err
138+
return self._datalink_url
139+
140+
@property
141+
def sia(self):
142+
if not self._sia:
143+
self._sia = SIA2Service(baseurl=self.sia_url)
144+
return self._sia
145+
146+
@property
147+
def sia_url(self):
148+
if not self._sia_url:
149+
try:
150+
self._sia_url = urljoin(self._get_dataarchive_url(), SIA_SERVICE_PATH)
151+
except requests.exceptions.HTTPError as err:
152+
log.debug(
153+
f"ERROR getting the NRAO Archive URL: {str(err)}")
154+
raise err
155+
return self._sia_url
156+
157+
@property
158+
def tap(self):
159+
if not self._tap:
160+
self._tap = pyvo.dal.tap.TAPService(baseurl=self.tap_url, session=self._session)
161+
return self._tap
162+
163+
@property
164+
def tap_url(self):
165+
if not self._tap_url:
166+
try:
167+
self._tap_url = urljoin(self._get_dataarchive_url(), TAP_SERVICE_PATH)
168+
except requests.exceptions.HTTPError as err:
169+
log.debug(
170+
f"ERROR getting the NRAO Archive URL: {str(err)}")
171+
raise err
172+
return self._tap_url
173+
174+
def query_tap(self, query, maxrec=None):
175+
"""
176+
Send query to the NRAO TAP. Results in pyvo.dal.TapResult format.
177+
result.table in Astropy table format
178+
179+
Parameters
180+
----------
181+
maxrec : int
182+
maximum number of records to return
183+
184+
"""
185+
log.debug('TAP query: {}'.format(query))
186+
return self.tap.search(query, language='ADQL', maxrec=maxrec)
187+
188+
def _get_dataarchive_url(self):
189+
return tap_urls[0]
190+
191+
def query_region_async(self, coordinate, radius, *, public=True,
192+
science=True, payload=None, **kwargs):
193+
"""
194+
Query the NRAO archive with a source name and radius
195+
196+
Parameters
197+
----------
198+
coordinates : str / `astropy.coordinates`
199+
the identifier or coordinates around which to query.
200+
radius : str / `~astropy.units.Quantity`, optional
201+
the radius of the region
202+
public : bool
203+
True to return only public datasets, False to return private only,
204+
None to return both
205+
science : bool
206+
True to return only science datasets, False to return only
207+
calibration, None to return both
208+
payload : dict
209+
Dictionary of additional keywords. See `help`.
210+
"""
211+
rad = radius
212+
if not isinstance(radius, u.Quantity):
213+
rad = radius*u.deg
214+
obj_coord = commons.parse_coordinates(coordinate).icrs
215+
ra_dec = '{}, {}'.format(obj_coord.to_string(), rad.to(u.deg).value)
216+
if payload is None:
217+
payload = {}
218+
if 'ra_dec' in payload:
219+
payload['ra_dec'] += ' | {}'.format(ra_dec)
220+
else:
221+
payload['ra_dec'] = ra_dec
222+
223+
return self.query_async(public=public, science=science,
224+
payload=payload, **kwargs)
225+
226+
def query_async(self, payload, *, get_query_payload=False,
227+
maxrec=None, **kwargs):
228+
"""
229+
Perform a generic query with user-specified payload
230+
231+
Parameters
232+
----------
233+
payload : dictionary
234+
Please consult the `help` method
235+
public : bool
236+
True to return only public datasets, False to return private only,
237+
None to return both
238+
science : bool
239+
True to return only science datasets, False to return only
240+
calibration, None to return both
241+
legacy_columns : bool
242+
True to return the columns from the obsolete NRAO advanced query,
243+
otherwise return the current columns based on ObsCore model.
244+
get_query_payload : bool
245+
Flag to indicate whether to simply return the payload.
246+
maxrec : integer
247+
Cap on the amount of records returned. Default is no limit.
248+
249+
Returns
250+
-------
251+
252+
Table with results. Columns are those in the NRAO ObsCore model
253+
(see ``help_tap``) unless ``legacy_columns`` argument is set to True.
254+
"""
255+
256+
if payload is None:
257+
payload = {}
258+
for arg in kwargs:
259+
value = kwargs[arg]
260+
if arg in payload:
261+
payload[arg] = '{} {}'.format(payload[arg], value)
262+
else:
263+
payload[arg] = value
264+
265+
query = _gen_sql(payload)
266+
267+
if get_query_payload:
268+
# Return the TAP query payload that goes out to the server rather
269+
# than the unprocessed payload dict from the python side
270+
return query
271+
272+
result = self.query_tap(query, maxrec=maxrec)
273+
274+
if result is not None:
275+
result = result.to_table()
276+
else:
277+
# Should not happen
278+
raise RuntimeError('BUG: Unexpected result None')
279+
if legacy_columns:
280+
legacy_result = Table()
281+
# add 'Observation date' column
282+
283+
for col_name in _OBSCORE_TO_NRAORESULT:
284+
if col_name in result.columns:
285+
if col_name == 't_min':
286+
legacy_result['Observation date'] = \
287+
[Time(_['t_min'], format='mjd').strftime(
288+
NRAO_DATE_FORMAT) for _ in result]
289+
else:
290+
legacy_result[_OBSCORE_TO_NRAORESULT[col_name]] = \
291+
result[col_name]
292+
else:
293+
log.error("Invalid column mapping in OBSCORE_TO_NRAORESULT: "
294+
"{}:{}. Please "
295+
"report this as an Issue."
296+
.format(col_name, _OBSCORE_TO_NRAORESULT[col_name]))
297+
return legacy_result
298+
return result
299+
300+
301+
Nrao = NraoClass()

0 commit comments

Comments
 (0)