Skip to content

Commit 8b6620e

Browse files
committed
MastMissions.read_product
1 parent c3e973f commit 8b6620e

3 files changed

Lines changed: 315 additions & 80 deletions

File tree

astroquery/mast/missions.py

Lines changed: 123 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"""
88

99
import difflib
10+
import importlib
1011
import json
1112
import warnings
1213
from collections.abc import Iterable
@@ -17,12 +18,14 @@
1718
import astropy.units as u
1819
import numpy as np
1920
from astropy.coordinates import Angle, BaseCoordinateFrame, SkyCoord
21+
from astropy.io import fits
2022
from astropy.table import Column, Row, Table, vstack
2123
from astropy.utils.decorators import deprecated_renamed_argument
2224
from requests import HTTPError, RequestException
2325

2426
from astroquery import log
2527
from astroquery.exceptions import (
28+
AuthenticationWarning,
2629
InputWarning,
2730
InvalidQueryError,
2831
MaxResultsWarning,
@@ -35,6 +38,16 @@
3538

3639
from . import conf
3740

41+
try:
42+
import fsspec
43+
except ImportError:
44+
fsspec = None
45+
46+
try:
47+
import asdf
48+
except ImportError:
49+
asdf = None
50+
3851
__all__ = ['MastMissionsClass', 'MastMissions']
3952

4053

@@ -713,35 +726,23 @@ def filter_products(self, products, *, extension=None, **filters):
713726

714727
return products[filter_mask]
715728

716-
def download_file(self, uri, *, local_path=None, cache=True, mission=None, verbose=True):
729+
def _get_download_url(self, uri, mission=None):
717730
"""
718-
Downloads a single file based on the data URI.
731+
Construct the full download URL for a given data URI based on the mission.
719732
720733
Parameters
721734
----------
722735
uri : str
723-
The product filename or URI to be downloaded.
724-
local_path : str
725-
Directory or filename to which the file will be downloaded. Defaults to current working directory.
726-
cache : bool
727-
Default is True. If file is found on disk, it will not be downloaded again.
736+
The product filename or URI for which to construct the download URL.
728737
mission : str, optional
729738
The mission to which the file belongs. If not provided, the current value of the ``mission`` attribute
730739
will be used.
731-
verbose : bool, optional
732-
Default is True. Whether to show download progress in the console.
733740
734741
Returns
735742
-------
736-
status: str
737-
Download status message. Either COMPLETE, SKIPPED, or ERROR.
738-
msg : str
739-
An error status message, if any.
740743
url : str
741744
The full URL download path.
742745
"""
743-
744-
# Construct the full data URL based on mission
745746
current_mission = mission.lower() if mission else self.mission
746747

747748
if current_mission in ['hst', 'jwst', 'roman', 'roman_spectra', 'roman_cgi']:
@@ -754,11 +755,62 @@ def download_file(self, uri, *, local_path=None, cache=True, mission=None, verbo
754755
base_url = self._service_api_connection.MAST_DOWNLOAD_URL
755756
keyword = 'uri'
756757
# These files require a MAST URI and not just a filename
757-
if not uri.startswith('mast:'):
758+
if not uri.startswith('mast'):
758759
raise InvalidQueryError(f'For mission "{current_mission}", a full MAST URI is required '
759760
f'for downloading. Got "{uri}".')
760-
data_url = base_url + f'?{keyword}=' + uri
761-
escaped_url = base_url + f'?{keyword}=' + quote(uri, safe='')
761+
762+
return base_url + f'?{keyword}=' + quote(uri, safe='')
763+
764+
def _warn_no_auth(self, download_url):
765+
"""
766+
Warn the user that they are not authenticated to download the file, and provide guidance on how to authenticate.
767+
768+
Parameters
769+
----------
770+
download_url : str
771+
The URL that the user attempted to download from, which requires authentication.
772+
"""
773+
no_auth_msg = f'You are not authorized to download from {download_url}.'
774+
if self._authenticated:
775+
no_auth_msg += ('\nYou do not have access to download this data, or your authentication '
776+
'token may be expired. You can generate a new token at '
777+
'https://auth.mast.stsci.edu/token?suggested_name=Astroquery&'
778+
'suggested_scope=mast:exclusive_access')
779+
else:
780+
no_auth_msg += ('\nPlease authenticate yourself using the `~astroquery.mast.MastMissions.login` '
781+
'function or initialize `~astroquery.mast.MastMissions` with an authentication '
782+
'token.')
783+
warnings.warn(no_auth_msg, AuthenticationWarning)
784+
785+
def download_file(self, uri, *, local_path=None, cache=True, mission=None, verbose=True):
786+
"""
787+
Downloads a single file based on the data URI.
788+
789+
Parameters
790+
----------
791+
uri : str
792+
The product filename or URI to be downloaded.
793+
local_path : str
794+
Directory or filename to which the file will be downloaded. Defaults to current working directory.
795+
cache : bool
796+
Default is True. If file is found on disk, it will not be downloaded again.
797+
mission : str, optional
798+
The mission to which the file belongs. If not provided, the current value of the ``mission`` attribute
799+
will be used.
800+
verbose : bool, optional
801+
Default is True. Whether to show download progress in the console.
802+
803+
Returns
804+
-------
805+
status: str
806+
Download status message. Either COMPLETE, SKIPPED, or ERROR.
807+
msg : str
808+
An error status message, if any.
809+
url : str
810+
The full URL download path.
811+
"""
812+
# Construct the full data URL based on mission
813+
download_url = self._get_download_url(uri, mission=mission)
762814

763815
# Determine local file path. Use current directory as default.
764816
filename = Path(uri).name
@@ -773,30 +825,20 @@ def download_file(self, uri, *, local_path=None, cache=True, mission=None, verbo
773825

774826
try:
775827
# Attempt file download
776-
self._download_file(escaped_url, local_path, cache=cache, verbose=verbose)
828+
self._download_file(download_url, local_path, cache=cache, verbose=verbose)
777829

778830
# Check if file exists
779831
if not local_path.is_file() and status != 'SKIPPED':
780832
status = 'ERROR'
781833
msg = 'File was not downloaded'
782-
url = data_url
834+
url = download_url
783835

784836
except HTTPError as err:
785837
if err.response.status_code == 401:
786-
no_auth_msg = f'You are not authorized to download from {data_url}.'
787-
if self._authenticated:
788-
no_auth_msg += ('\nYou do not have access to download this data, or your authentication '
789-
'token may be expired. You can generate a new token at '
790-
'https://auth.mast.stsci.edu/token?suggested_name=Astroquery&'
791-
'suggested_scope=mast:exclusive_access')
792-
else:
793-
no_auth_msg += ('\nPlease authenticate yourself using the `~astroquery.mast.MastMissions.login` '
794-
'function or initialize `~astroquery.mast.MastMissions` with an authentication '
795-
'token.')
796-
log.warning(no_auth_msg)
838+
self._warn_no_auth(download_url)
797839
status = 'ERROR'
798840
msg = f'HTTPError: {err}'
799-
url = data_url
841+
url = download_url
800842

801843
return status, msg, url
802844

@@ -823,7 +865,6 @@ def _download_files(self, products, base_dir, *, flat=False, cache=True, verbose
823865
response : `~astropy.table.Table`
824866
Table containing download results for each data product file.
825867
"""
826-
827868
manifest_entries = []
828869
base_dir = Path(base_dir)
829870

@@ -945,6 +986,55 @@ def download_products(self, products, *, download_dir=None, flat=False,
945986

946987
return manifest
947988

989+
def read_product(self, uri, *, mission=None, **kwargs):
990+
"""
991+
Reads a data product file directly from a URI into an appropriate in-memory object based on file type.
992+
993+
Parameters
994+
----------
995+
uri : str
996+
The product filename or URI to be read.
997+
mission : str, optional
998+
The mission to which the file belongs. If not provided, the current value of the ``mission`` attribute
999+
will be used.
1000+
**kwargs
1001+
Additional keyword arguments to be passed to the file reading function (e.g., `astropy.io.fits.open`
1002+
or `asdf.open`).
1003+
"""
1004+
# Construct the full data URL based on mission
1005+
download_url = self._get_download_url(uri, mission=mission)
1006+
1007+
try:
1008+
if uri.endswith((".fits", ".fits.gz")):
1009+
# Read in a
1010+
return fits.open(download_url, **kwargs)
1011+
elif uri.endswith(".asdf"):
1012+
if fsspec is None:
1013+
raise ImportError('The "fsspec" package is required to read products directly from a URI. '
1014+
'Please install it with `pip install fsspec` or install astroquery with '
1015+
'optional dependencies using `pip install astroquery[all]`.')
1016+
if asdf is None:
1017+
raise ImportError('The "asdf" package is required to read ASDF files. Please install it with '
1018+
'`pip install asdf` or install astroquery with optional dependencies using '
1019+
'`pip install astroquery[all]`.')
1020+
for package in ["gwcs", "lz4", "roman_datamodels"]:
1021+
if importlib.util.find_spec(package) is None:
1022+
warnings.warn(f'The "{package}" package is encouraged when reading ASDF files from the '
1023+
'Roman Space Telescope mission. Please install it with `pip install {package}` '
1024+
'or install astroquery with optional dependencies using '
1025+
'`pip install astroquery[all]`.', ImportWarning)
1026+
# Use fsspec to open the file directly from the
1027+
fs = fsspec.filesystem("https")
1028+
f = fs.open(download_url, "rb")
1029+
return asdf.open(f, **kwargs)
1030+
else:
1031+
raise InvalidQueryError(f"Unsupported file type for reading: {uri}. "
1032+
"Supported types are .fits and .asdf.")
1033+
except HTTPError as err:
1034+
if err.response.status_code == 401:
1035+
self._warn_no_auth(download_url)
1036+
raise
1037+
9481038
@class_or_instance
9491039
def get_column_list(self):
9501040
"""

0 commit comments

Comments
 (0)