77"""
88
99import difflib
10+ import importlib
1011import json
1112import warnings
1213from collections .abc import Iterable
1718import astropy .units as u
1819import numpy as np
1920from astropy .coordinates import Angle , BaseCoordinateFrame , SkyCoord
21+ from astropy .io import fits
2022from astropy .table import Column , Row , Table , vstack
2123from astropy .utils .decorators import deprecated_renamed_argument
2224from requests import HTTPError , RequestException
2325
2426from astroquery import log
2527from astroquery .exceptions import (
28+ AuthenticationWarning ,
2629 InputWarning ,
2730 InvalidQueryError ,
2831 MaxResultsWarning ,
3538
3639from . 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 += ('\n You 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 += ('\n Please 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 += ('\n You 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 += ('\n Please 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