Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ mast
- Added ``pass_id`` as an alias for the ``pass`` column in query functions for the Roman mission to avoid conflicts with
the reserved Python keyword. [#3588]
- Update the cutout format request parameter in ``Zcut.download_cutouts`` to reflect a recent service change. [#3608]
- Added a ``mission`` parameter to methods in ``MastMissions`` to allow overriding the default mission for a query. [#3618]
- The ``MastMissions.get_available_missions`` method retrieves the list of available missions and caches it for future use. [#3618]


jplspec
Expand Down
1,115 changes: 620 additions & 495 deletions astroquery/mast/missions.py

Large diffs are not rendered by default.

20 changes: 10 additions & 10 deletions astroquery/mast/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,16 @@
import warnings

import numpy as np

from astropy.table import Table, MaskedColumn
from astropy.table import MaskedColumn, Table
from astropy.utils.decorators import deprecated_renamed_argument

from .. import log
from ..exceptions import (BlankResponseWarning, InvalidQueryError, NoResultsWarning, TimeoutError)
from ..query import BaseQuery
from ..utils import async_to_sync
from ..utils.class_or_instance import class_or_instance
from ..exceptions import BlankResponseWarning, InvalidQueryError, TimeoutError, NoResultsWarning

from . import conf


__all__ = ["ServiceAPI"]


Expand Down Expand Up @@ -149,7 +146,7 @@ class ServiceAPI(BaseQuery):

SERVICE_URL = conf.server
REQUEST_URL = conf.server + "/api/v0.1/"
MISSIONS_DOWNLOAD_URL = conf.server + "/search/"
MISSIONS_URL = conf.server + "/search/"
MAST_DOWNLOAD_URL = conf.server + "/api/v0.1/Download/file"
SERVICES = {}

Expand Down Expand Up @@ -366,21 +363,24 @@ def service_request_async(self, service, params, pagesize=None, page=None, use_j
return response

@class_or_instance
def missions_request_async(self, service, params):
def missions_request_async(self, service, params, mission):
"""
Builds and executes an asynchronous query to the MAST Search API.
Parameters
----------
service : str
The MAST Search API service to query. Should be present in self.SERVICES.
The MAST Search API service to query. Should be present in self.SERVICES.
params : dict
JSON object containing service parameters.
JSON object containing service parameters.
mission : str
The mission for which to query.

Returns
-------
response : list of `~requests.Response`
"""
service_config = self.SERVICES.get(service.lower())
request_url = self.REQUEST_URL + service_config.get('path')
request_url = self.MISSIONS_URL + f"{mission}/api/v0.1/" + f"{service_config.get('path')}"

# Default headers
headers = {
Expand Down
10 changes: 10 additions & 0 deletions astroquery/mast/tests/data/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,13 @@ To generate `~astroquery.mast.tests.data.resolver.json`, use the following:
... {'name': objects, 'outputFormat': 'json', 'resolveAll': 'true'})
>>> with open('resolver.json', 'w') as file:
... json.dump(resp.json(), file, indent=4) # doctest: +SKIP

To generate `~astroquery.mast.tests.data.available_missions.html`, use the following:

.. doctest-remote-data::

>>> import requests
...
>>> html = requests.get(f"https://mast.stsci.edu/search/docs").text
>>> with open("available_missions.html", "w") as file:
... file.write(html) # doctest: +SKIP
51 changes: 51 additions & 0 deletions astroquery/mast/tests/data/available_missions.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<!DOCTYPE html>
<html>
<head>
<link type="text/css" rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css">

<title>MAST Search API Doc</title>
</head>
<body>
<div id="swagger-ui">
</div>

<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-standalone-preset.js"></script>
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js"></script>

<script>
var swagger_config = {
"presets": [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
"dom_id": "#swagger-ui",
"layout": "StandaloneLayout",
"deepLinking": true,
"showExtensions": true,
"showCommonExtensions": true
};

/*
Parse dynamic urls config into Javascript object. This gets generated from fastapi_doc.py.
This should look like
{
"urls": [
{
"url": "http://url/to/service1/openapi.json",
"name": "service1"
},
{
"url": "http://url/to/service2/openapi.json",
"name": "service2"
},
...
]
}
*/
var urls_config_obj = JSON.parse('{"urls": [{"name": "hst_api", "url": "hst_api_openapi.json"}, {"name": "jwst_api", "url": "jwst_api_openapi.json"}, {"name": "classy_api", "url": "classy_api_openapi.json"}, {"name": "ullyses_api", "url": "ullyses_api_openapi.json"}, {"name": "roman_api", "url": "roman_api_openapi.json"}, {"name": "roman_spectra_api", "url": "roman_spectra_api_openapi.json"}, {"name": "all_missions_api", "url": "all_missions_api_openapi.json"}, {"name": "iue_api", "url": "iue_api_openapi.json"}, {"name": "roman_cgi_api", "url": "roman_cgi_api_openapi.json"}]}');
var config_merged = {...swagger_config, ...urls_config_obj};

const ui = SwaggerUIBundle(config_merged);
</script>
</body>
</html>
71 changes: 51 additions & 20 deletions astroquery/mast/tests/test_mast.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@
import os
import re
import warnings
from pathlib import Path
from shutil import copyfile
from unittest.mock import MagicMock, patch
from pathlib import Path

import astropy.units as u
import pytest
import numpy as np
from astropy.table import Table, unique
import pytest
from astropy.coordinates import SkyCoord
from astropy.io import fits
from astropy.table import Table, unique
from astropy.utils.exceptions import AstropyDeprecationWarning
from requests import HTTPError, Response

Expand All @@ -35,6 +35,7 @@
'mission_search_results': 'mission_results.json',
'mission_columns': 'mission_columns.json',
'mission_products': 'mission_products.json',
'available_missions': 'available_missions.html',
'columnsconfig': 'columnsconfig.json',
'ticcolumns': 'ticcolumns.json',
'ticcol_filtered': 'ticcolumns_filtered.json',
Expand Down Expand Up @@ -179,6 +180,8 @@ def request_mockreturn(url, params={}):
filename = data_path(DATA_FILES['panstarrs_columns'])
elif 'path_lookup' in url:
filename = data_path(DATA_FILES['get_cloud_paths'])
elif 'search/docs' in url:
filename = data_path(DATA_FILES['available_missions'])
with open(filename, 'rb') as infile:
content = infile.read()
return MockResponse(content)
Expand Down Expand Up @@ -242,6 +245,30 @@ def zcut_download_mockreturn(url, file_path):
###########################


def test_missions_get_available_missions():
# Access attribute
missions = MastMissions.available_missions
assert isinstance(missions, list)
assert len(missions) == 9

result = MastMissions.get_available_missions()
assert missions == result


def test_missions_verify_mission():
MastMissions._verify_mission("hst")

# Invalid mission with a possible match
with pytest.raises(InvalidQueryError, match="Did you mean 'hst'?"):
MastMissions._verify_mission("hsp")

# Invalid mission with no match
with pytest.raises(InvalidQueryError) as err:
MastMissions._verify_mission("invalid")
assert "Did you mean" not in str(err.value)
assert "Mission 'invalid' is not available" in str(err.value)


def test_missions_query_region_async():
responses = MastMissions.query_region_async(regionCoords, radius=0.002, sci_pi_last_name='GORDON')
assert isinstance(responses, MockResponse)
Expand Down Expand Up @@ -332,50 +359,50 @@ def test_missions_query_criteria():

def test_missions_parse_select_cols():
# Default columns
cols = MastMissions._parse_select_cols(None) # Default columns for HST
assert cols is None
cols = MastMissions._parse_select_cols(None, mission='hst') # Default columns for HST
assert cols == []

# All columns
all_cols = MastMissions._parse_select_cols('all')
assert all_cols == MastMissions.get_column_list()['name'].value.tolist()
all_cols = MastMissions._parse_select_cols('all', mission='hst')
assert all_cols == MastMissions.get_column_list(mission='hst')['name'].value.tolist()

# Comma-separated string
string_cols = MastMissions._parse_select_cols('sci_pep_id, sci_instrume')
string_cols = MastMissions._parse_select_cols('sci_pep_id, sci_instrume', mission='hst')
for col in ['sci_pep_id', 'sci_instrume', 'sci_data_set_name']:
assert col in string_cols

# List of columns
list_cols = MastMissions._parse_select_cols(['sci_pep_id', 'sci_instrume'])
list_cols = MastMissions._parse_select_cols(['sci_pep_id', 'sci_instrume'], mission='hst')
for col in ['sci_pep_id', 'sci_instrume', 'sci_data_set_name']:
assert col in list_cols

# Tuple of columns
tuple_cols = MastMissions._parse_select_cols(('sci_pep_id', 'sci_instrume'))
tuple_cols = MastMissions._parse_select_cols(('sci_pep_id', 'sci_instrume'), mission='hst')
for col in ['sci_pep_id', 'sci_instrume', 'sci_data_set_name']:
assert col in tuple_cols

# Generator of columns
gen_cols = MastMissions._parse_select_cols(col for col in ['sci_pep_id', 'sci_instrume'])
gen_cols = MastMissions._parse_select_cols((col for col in ['sci_pep_id', 'sci_instrume']), mission='hst')
for col in ['sci_pep_id', 'sci_instrume', 'sci_data_set_name']:
assert col in gen_cols

# Error if invalid type
with pytest.raises(InvalidQueryError, match="`select_cols` must be an iterable of column names"):
MastMissions._parse_select_cols(123)
MastMissions._parse_select_cols(123, mission='hst')

# Error if an individual column is not a string
with pytest.raises(InvalidQueryError, match="`select_cols` must contain only strings"):
MastMissions._parse_select_cols(['sci_pep_id', 123])
MastMissions._parse_select_cols(['sci_pep_id', 123], mission='hst')

# Warning for invalid column names
with pytest.warns(InputWarning, match="Column 'invalid_column' not found."):
valid_cols = MastMissions._parse_select_cols(['sci_pep_id', 'invalid_column'])
valid_cols = MastMissions._parse_select_cols(['sci_pep_id', 'invalid_column'], mission='hst')
assert 'sci_pep_id' in valid_cols
assert 'invalid_column' not in valid_cols

# Workaround for Ullyses mission default columns
ullyses_mission = MastMissions(mission='ullyses')
ullyses_cols = ullyses_mission._parse_select_cols(None)
ullyses_cols = ullyses_mission._parse_select_cols(None, mission='ullyses')
for col in MastMissions._default_ullyses_cols:
assert col in ullyses_cols

Expand Down Expand Up @@ -469,8 +496,8 @@ def test_missions_get_product_list_async():
assert 'Dataset list is empty' in str(err_empty.value)

# No dataset keyword
with pytest.raises(InvalidQueryError, match='Dataset keyword not found for mission "invalid"'):
missions = MastMissions(mission='invalid')
with pytest.raises(InvalidQueryError, match='Dataset keyword not found for mission "roman_spectra".'):
missions = MastMissions(mission='roman_spectra')
missions.get_product_list_async(Table({'a': [1, 2, 3]}))


Expand All @@ -489,6 +516,10 @@ def test_missions_get_product_list():
result = MastMissions.get_product_list(datasets[:3])
assert isinstance(result, Table)

# Column input
result = MastMissions.get_product_list(datasets['sci_data_set_name'])
assert isinstance(result, Table)

# Table input
result = MastMissions.get_product_list(datasets[0])
assert isinstance(result, Table)
Expand Down Expand Up @@ -707,11 +738,11 @@ def test_missions_get_dataset_kwd(caplog):
assert m.get_dataset_kwd() == 'Target'

# Switch to an unknown
m.mission = 'Unknown'
assert m.mission == 'unknown'
m.mission = 'roman_spectra'
assert m.mission == 'roman_spectra'
assert m.get_dataset_kwd() is None
with caplog.at_level('WARNING', logger='astroquery'):
assert 'The mission "unknown" does not have a known dataset ID keyword' in caplog.text
assert 'The mission "roman_spectra" does not have a known dataset ID keyword' in caplog.text


@pytest.mark.parametrize(
Expand Down
Loading
Loading