Skip to content

Commit eabbf77

Browse files
committed
More tests, download from HLSPs
1 parent 24990a1 commit eabbf77

File tree

6 files changed

+287
-12
lines changed

6 files changed

+287
-12
lines changed

astroquery/mast/missions.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,19 @@ class MastMissionsClass(MastQueryWithLogin):
4242
_search = 'search'
4343
_list_products = 'list_products'
4444

45+
# Workaround so that observation_id is returned in ULLYSES queries that do not specify columns
46+
_default_ulysses_cols = ['target_name_ulysses', 'target_classification', 'targ_ra', 'targ_dec', 'host_galaxy_name',
47+
'spectral_type', 'bmv0_mag', 'u_mag', 'b_mag', 'v_mag', 'gaia_g_mean_mag', 'star_mass',
48+
'instrument', 'grating', 'filter', 'observation_id']
49+
4550
def __init__(self, *, mission='hst'):
4651
super().__init__()
4752

4853
self.dataset_kwds = { # column keywords corresponding to dataset ID
4954
'hst': 'sci_data_set_name',
50-
'jwst': 'fileSetName'
55+
'jwst': 'fileSetName',
56+
'classy': 'Target',
57+
'ullyses': 'observation_id'
5158
}
5259

5360
# Service attributes
@@ -69,7 +76,7 @@ def mission(self):
6976
@mission.setter
7077
def mission(self, value):
7178
# Need to update the service parameters if the mission is changed
72-
self._mission = value
79+
self._mission = value.lower()
7380
self._service_api_connection.set_service_params(self.service_dict, f'search/{self.mission}')
7481

7582
def _parse_result(self, response, *, verbose=False): # Used by the async_to_sync decorator functionality
@@ -184,6 +191,8 @@ def query_region_async(self, coordinates, *, radius=3*u.arcmin, limit=5000, offs
184191
# Dataset ID column should always be returned
185192
if select_cols:
186193
select_cols.append(self.dataset_kwds[self.mission])
194+
elif self.mission == 'ullyses':
195+
select_cols = self._default_ulysses_cols
187196

188197
# basic params
189198
params = {'target': [f"{coordinates.ra.deg} {coordinates.dec.deg}"],
@@ -259,6 +268,8 @@ def query_criteria_async(self, *, coordinates=None, objectname=None, radius=3*u.
259268
# Dataset ID column should always be returned
260269
if select_cols:
261270
select_cols.append(self.dataset_kwds[self.mission])
271+
elif self.mission == 'ullyses':
272+
select_cols = self._default_ulysses_cols
262273

263274
# build query
264275
params = {"limit": self.limit, "offset": offset, 'select_cols': select_cols}
@@ -452,10 +463,17 @@ def download_file(self, uri, *, local_path=None, cache=True, verbose=True):
452463
The full URL download path.
453464
"""
454465

455-
# Construct the full data URL
456-
base_url = self._service_api_connection.MISSIONS_DOWNLOAD_URL + self.mission + '/api/v0.1/retrieve_product'
457-
data_url = base_url + '?product_name=' + uri
458-
escaped_url = base_url + '?product_name=' + quote(uri, safe=':')
466+
# Construct the full data URL based on mission
467+
if self.mission in ['hst', 'jwst']:
468+
# HST and JWST have a dedicated endpoint for retrieving products
469+
base_url = self._service_api_connection.MISSIONS_DOWNLOAD_URL + self.mission + '/api/v0.1/retrieve_product'
470+
keyword = 'product_name'
471+
else:
472+
# HLSPs use MAST download URL
473+
base_url = self._service_api_connection.MAST_DOWNLOAD_URL
474+
keyword = 'uri'
475+
data_url = base_url + f'?{keyword}=' + uri
476+
escaped_url = base_url + f'?{keyword}=' + quote(uri, safe='')
459477

460478
# Determine local file path. Use current directory as default.
461479
filename = Path(uri).name

astroquery/mast/services.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from astropy.table import Table, MaskedColumn
1515
from astropy.utils.decorators import deprecated_renamed_argument
1616

17+
from .. import log
1718
from ..query import BaseQuery
1819
from ..utils import async_to_sync
1920
from ..utils.class_or_instance import class_or_instance
@@ -84,7 +85,12 @@ def _json_to_table(json_obj, data_key='data'):
8485
col_data = np.array([x[idx] for x in json_obj[data_key]], dtype=object)
8586
except KeyError:
8687
# it's not a data array, fall back to using column name as it is array of dictionaries
87-
col_data = np.array([x[col_name] for x in json_obj[data_key]], dtype=object)
88+
try:
89+
col_data = np.array([x[col_name] for x in json_obj[data_key]], dtype=object)
90+
except KeyError:
91+
# Skip column names not found in data
92+
log.debug('Column %s was not found in data. Skipping...', col_name)
93+
continue
8894
if ignore_value is not None:
8995
col_data[np.where(np.equal(col_data, None))] = ignore_value
9096

@@ -113,6 +119,7 @@ class ServiceAPI(BaseQuery):
113119
SERVICE_URL = conf.server
114120
REQUEST_URL = conf.server + "/api/v0.1/"
115121
MISSIONS_DOWNLOAD_URL = conf.server + "/search/"
122+
MAST_DOWNLOAD_URL = conf.server + "/api/v0.1/Download/file"
116123
SERVICES = {}
117124

118125
def __init__(self, session=None):

astroquery/mast/tests/data/README.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,14 @@ To generate `~astroquery.mast.tests.data.panstarrs_columns.json`, use the follow
2525
>>> resp = utils._simple_request('https://catalogs.mast.stsci.edu/api/v0.1/panstarrs/dr2/mean/metadata.json')
2626
>>> with open('panstarrs_columns.json', 'w') as file:
2727
... json.dump(resp.json(), file, indent=4) # doctest: +SKIP
28+
29+
To generate `~astroquery.mast.tests.data.mission_products.json`, use the following:
30+
31+
.. doctest-remote-data::
32+
33+
>>> import json
34+
>>> from astroquery.mast import utils
35+
...
36+
>>> resp = utils._simple_request('https://mast.stsci.edu/search/hst/api/v0.1/list_products', {'dataset_ids': 'Z14Z0104T'})
37+
>>> with open('panstarrs_columns.json', 'w') as file:
38+
... json.dump(resp.json(), file, indent=4) # doctest: +SKIP
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
{
2+
"products": [
3+
{
4+
"product_key": "Z14Z0104T_z14z0104t_shf.fits",
5+
"access": "PUBLIC",
6+
"dataset": "Z14Z0104T",
7+
"instrument_name": "HRS ",
8+
"filters": "MIRROR-N2",
9+
"filename": "z14z0104t_shf.fits",
10+
"uri": "Z14Z0104T/z14z0104t_shf.fits",
11+
"authz_primary_identifier": "Z14Z0104T",
12+
"authz_secondary_identifier": "CAL",
13+
"file_suffix": "SHF",
14+
"category": "UNCALIBRATED",
15+
"size": 31680,
16+
"type": "science"
17+
},
18+
{
19+
"product_key": "Z14Z0104T_z14z0104t_trl.fits",
20+
"access": "PUBLIC",
21+
"dataset": "Z14Z0104T",
22+
"instrument_name": "HRS ",
23+
"filters": "MIRROR-N2",
24+
"filename": "z14z0104t_trl.fits",
25+
"uri": "Z14Z0104T/z14z0104t_trl.fits",
26+
"authz_primary_identifier": "Z14Z0104T",
27+
"authz_secondary_identifier": "CAL",
28+
"file_suffix": "TRL",
29+
"category": "AUX",
30+
"size": 17280,
31+
"type": "science"
32+
},
33+
{
34+
"product_key": "Z14Z0104T_z14z0104t_ulf.fits",
35+
"access": "PUBLIC",
36+
"dataset": "Z14Z0104T",
37+
"instrument_name": "HRS ",
38+
"filters": "MIRROR-N2",
39+
"filename": "z14z0104t_ulf.fits",
40+
"uri": "Z14Z0104T/z14z0104t_ulf.fits",
41+
"authz_primary_identifier": "Z14Z0104T",
42+
"authz_secondary_identifier": "CAL",
43+
"file_suffix": "ULF",
44+
"category": "UNCALIBRATED",
45+
"size": 14400,
46+
"type": "science"
47+
},
48+
{
49+
"product_key": "Z14Z0104T_z14z0104t_pdq.fits",
50+
"access": "PUBLIC",
51+
"dataset": "Z14Z0104T",
52+
"instrument_name": "HRS ",
53+
"filters": "MIRROR-N2",
54+
"filename": "z14z0104t_pdq.fits",
55+
"uri": "Z14Z0104T/z14z0104t_pdq.fits",
56+
"authz_primary_identifier": "Z14Z0104T",
57+
"authz_secondary_identifier": "PDQ",
58+
"file_suffix": "PDQ",
59+
"category": "AUX",
60+
"size": 11520,
61+
"type": "science"
62+
},
63+
{
64+
"product_key": "Z14Z0104T_z14z0104x_ocx.fits",
65+
"access": "PUBLIC",
66+
"dataset": "Z14Z0104T",
67+
"instrument_name": "HRS ",
68+
"filters": "MIRROR-N2",
69+
"filename": "z14z0104x_ocx.fits",
70+
"uri": "Z14Z0104T/z14z0104x_ocx.fits",
71+
"authz_primary_identifier": "Z14Z0104X",
72+
"authz_secondary_identifier": "OCX",
73+
"file_suffix": "OCX",
74+
"category": "OTHER",
75+
"size": 11520,
76+
"type": "science"
77+
}
78+
]
79+
}

astroquery/mast/tests/test_mast.py

Lines changed: 138 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,23 @@
77

88
import pytest
99

10-
from astropy.table import Table
10+
from astropy.table import Table, unique
1111
from astropy.coordinates import SkyCoord
1212
from astropy.io import fits
1313

1414
import astropy.units as u
1515

1616
from astroquery.mast.services import _json_to_table
1717
from astroquery.utils.mocks import MockResponse
18-
from astroquery.exceptions import InvalidQueryError, InputWarning
18+
from astroquery.exceptions import InvalidQueryError, InputWarning, MaxResultsWarning, NoResultsWarning
1919

2020
from astroquery import mast
2121

2222
DATA_FILES = {'Mast.Caom.Cone': 'caom.json',
2323
'Mast.Name.Lookup': 'resolver.json',
2424
'mission_search_results': 'mission_results.json',
2525
'mission_columns': 'mission_columns.json',
26+
'mission_products': 'mission_products.json',
2627
'columnsconfig': 'columnsconfig.json',
2728
'ticcolumns': 'ticcolumns.json',
2829
'ticcol_filtered': 'ticcolumns_filtered.json',
@@ -72,6 +73,7 @@ def patch_post(request):
7273
mp.setattr(mast.Observations, '_download_file', download_mockreturn)
7374
mp.setattr(mast.Observations, 'download_file', download_mockreturn)
7475
mp.setattr(mast.Catalogs, '_download_file', download_mockreturn)
76+
mp.setattr(mast.MastMissions, '_download_file', download_mockreturn)
7577
mp.setattr(mast.Tesscut, '_download_file', tesscut_download_mockreturn)
7678
mp.setattr(mast.Zcut, '_download_file', zcut_download_mockreturn)
7779

@@ -108,7 +110,7 @@ def post_mockreturn(self, method="POST", url=None, data=None, timeout=10, **kwar
108110
return [MockResponse(content)]
109111

110112

111-
def service_mockreturn(self, method="POST", url=None, data=None, timeout=10, use_json=False, **kwargs):
113+
def service_mockreturn(self, method="POST", url=None, data=None, params=None, timeout=10, use_json=False, **kwargs):
112114
if "panstarrs" in url:
113115
filename = data_path(DATA_FILES["panstarrs"])
114116
elif "tesscut" in url:
@@ -121,6 +123,8 @@ def service_mockreturn(self, method="POST", url=None, data=None, timeout=10, use
121123
filename = data_path(DATA_FILES['z_survey'])
122124
else:
123125
filename = data_path(DATA_FILES['z_cutout_fit'])
126+
elif use_json and 'list_products' in url:
127+
filename = data_path(DATA_FILES['mission_products'])
124128
elif use_json and data['radius'] == 300:
125129
filename = data_path(DATA_FILES["mission_incorrect_results"])
126130
elif use_json:
@@ -211,7 +215,9 @@ def test_missions_query_object(patch_post):
211215

212216

213217
def test_missions_query_region(patch_post):
214-
result = mast.MastMissions.query_region(regionCoords, radius=0.002 * u.deg)
218+
result = mast.MastMissions.query_region(regionCoords,
219+
radius=0.002 * u.deg,
220+
select_cols=['sci_pep_id'])
215221
assert isinstance(result, Table)
216222
assert len(result) > 0
217223

@@ -242,6 +248,134 @@ def test_missions_query_criteria_async_with_missing_results(patch_post):
242248
_json_to_table(json.loads(responses), 'results')
243249

244250

251+
def test_missions_query_criteria(patch_post):
252+
result = mast.MastMissions.query_criteria(
253+
coordinates=regionCoords,
254+
radius=3,
255+
sci_pep_id=12556,
256+
sci_obs_type='SPECTRUM',
257+
sci_instrume='stis,acs,wfc3,cos,fos,foc,nicmos,ghrs',
258+
sci_aec='S',
259+
select_cols=['sci_pep_id', 'sci_instrume']
260+
)
261+
assert isinstance(result, Table)
262+
assert len(result) > 0
263+
264+
# Raise error if non-positional criteria is not supplied
265+
with pytest.raises(InvalidQueryError):
266+
mast.MastMissions.query_criteria(
267+
coordinates=regionCoords,
268+
radius=3
269+
)
270+
271+
# Raise error if invalid criteria is supplied
272+
with pytest.raises(InvalidQueryError):
273+
mast.MastMissions.query_criteria(
274+
coordinates=regionCoords,
275+
invalid=True
276+
)
277+
278+
# Maximum results warning
279+
with pytest.warns(MaxResultsWarning):
280+
mast.MastMissions.query_criteria(
281+
coordinates=regionCoords,
282+
sci_aec='S',
283+
limit=1
284+
)
285+
286+
287+
def test_missions_get_product_list_async(patch_post):
288+
# String input
289+
result = mast.MastMissions.get_product_list_async('Z14Z0104T')
290+
assert isinstance(result, MockResponse)
291+
292+
# List input
293+
in_datasets = ['Z14Z0104T', 'Z14Z0102T']
294+
result = mast.MastMissions.get_product_list_async(in_datasets)
295+
assert isinstance(result, MockResponse)
296+
297+
# Row input
298+
datasets = mast.MastMissions.query_object("M101", radius=".002 deg")
299+
result = mast.MastMissions.get_product_list_async(datasets[:3])
300+
assert isinstance(result, MockResponse)
301+
302+
# Table input
303+
result = mast.MastMissions.get_product_list_async(datasets[0])
304+
assert isinstance(result, MockResponse)
305+
306+
# Unsupported data type for datasets
307+
with pytest.raises(TypeError) as err_type:
308+
mast.MastMissions.get_product_list_async(1)
309+
assert 'Unsupported data type' in str(err_type.value)
310+
311+
# Empty dataset list
312+
with pytest.raises(InvalidQueryError) as err_empty:
313+
mast.MastMissions.get_product_list_async([' '])
314+
assert 'Dataset list is empty' in str(err_empty.value)
315+
316+
317+
def test_missions_get_product_list(patch_post):
318+
# String input
319+
result = mast.MastMissions.get_product_list('Z14Z0104T')
320+
assert isinstance(result, Table)
321+
322+
# List input
323+
in_datasets = ['Z14Z0104T', 'Z14Z0102T']
324+
result = mast.MastMissions.get_product_list(in_datasets)
325+
assert isinstance(result, Table)
326+
327+
# Row input
328+
datasets = mast.MastMissions.query_object("M101", radius=".002 deg")
329+
result = mast.MastMissions.get_product_list(datasets[:3])
330+
assert isinstance(result, Table)
331+
332+
# Table input
333+
result = mast.MastMissions.get_product_list(datasets[0])
334+
assert isinstance(result, Table)
335+
336+
337+
def test_missions_get_unique_product_list(patch_post, caplog):
338+
unique_products = mast.MastMissions.get_unique_product_list('Z14Z0104T')
339+
assert isinstance(unique_products, Table)
340+
assert (unique_products == unique(unique_products, keys='filename')).all()
341+
# No INFO messages should be logged
342+
with caplog.at_level('INFO', logger='astroquery'):
343+
assert caplog.text == ''
344+
345+
346+
def test_missions_filter_products(patch_post):
347+
# Filter products list by column
348+
products = mast.MastMissions.get_product_list('Z14Z0104T')
349+
filtered = mast.MastMissions.filter_products(products,
350+
category='CALIBRATED')
351+
assert isinstance(filtered, Table)
352+
assert all(filtered['category'] == 'CALIBRATED')
353+
354+
# Filter by non-existing column
355+
with pytest.warns(InputWarning):
356+
mast.MastMissions.filter_products(products,
357+
invalid=True)
358+
359+
360+
def test_missions_download_products(patch_post, tmp_path):
361+
# Check string input
362+
test_dataset_id = 'Z14Z0104T'
363+
result = mast.MastMissions.download_products(test_dataset_id,
364+
download_dir=tmp_path)
365+
assert isinstance(result, Table)
366+
367+
# Check Row input
368+
prods = mast.MastMissions.get_product_list('Z14Z0104T')
369+
result = mast.MastMissions.download_products(prods[0],
370+
download_dir=tmp_path)
371+
assert isinstance(result, Table)
372+
373+
# Warn about no products
374+
with pytest.warns(NoResultsWarning):
375+
result = mast.MastMissions.download_products(test_dataset_id,
376+
extension='jpg',
377+
download_dir=tmp_path)
378+
245379
###################
246380
# MastClass tests #
247381
###################

0 commit comments

Comments
 (0)