Skip to content

Commit 8d61176

Browse files
committed
Enable cloud by default, tests
1 parent c9f2538 commit 8d61176

6 files changed

Lines changed: 334 additions & 309 deletions

File tree

astroquery/exceptions.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,11 @@ class BlankResponseWarning(AstropyWarning):
119119
Astroquery warning to be raised if one or more rows in a table are bad, but
120120
not all rows are.
121121
"""
122+
pass
123+
124+
125+
class CloudAccessWarning(AstropyWarning):
126+
"""
127+
Astroquery warning to be raised if cloud access cannot be enabled.
128+
"""
129+
pass

astroquery/mast/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ class Conf(_config.ConfigNamespace):
2929
pagesize = _config.ConfigItem(
3030
50000,
3131
'Number of results to request at once from the STScI server.')
32+
enable_cloud_dataset = _config.ConfigItem(
33+
True,
34+
'Enable access to cloud-hosted datasets (e.g. on AWS S3) by default. '
35+
'Requires the `boto3` and `botocore` packages to be installed.')
3236

3337

3438
conf = Conf()

astroquery/mast/cloud.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,22 @@
1313
from astroquery import log
1414
from astropy.utils.console import ProgressBarOrSpinner
1515
from astropy.utils.exceptions import AstropyDeprecationWarning
16-
from botocore.exceptions import ClientError, BotoCoreError
1716

1817
from ..exceptions import RemoteServiceError, NoResultsWarning
1918

2019
from . import utils
2120

21+
try:
22+
import boto3
23+
HAS_BOTO3 = True
24+
except ImportError:
25+
HAS_BOTO3 = False
26+
try:
27+
import botocore
28+
from botocore.exceptions import ClientError, BotoCoreError
29+
HAS_BOTOCORE = True
30+
except ImportError:
31+
HAS_BOTOCORE = False
2232

2333
__all__ = []
2434

@@ -44,15 +54,14 @@ def __init__(self, provider="AWS", profile=None, verbose=False):
4454
verbose : bool
4555
Default False. Display extra info and warnings if true.
4656
"""
57+
if not HAS_BOTO3 or not HAS_BOTOCORE:
58+
raise ImportError("Please install the `boto3` and `botocore` packages to enable cloud dataset access.")
4759

4860
# Dealing with deprecated argument
4961
if profile is not None:
5062
warnings.warn(("MAST Open Data on AWS is now free to access and does "
5163
"not require an AWS account"), AstropyDeprecationWarning)
5264

53-
import boto3
54-
import botocore
55-
5665
self.boto3 = boto3
5766
self.botocore = botocore
5867
self.config = botocore.client.Config(signature_version=botocore.UNSIGNED)

astroquery/mast/observations.py

Lines changed: 61 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818

1919
import astropy.units as u
2020
import astropy.coordinates as coord
21-
from botocore.exceptions import ClientError, BotoCoreError
2221

2322
from astropy.table import Table, Row, vstack
2423
from astroquery import log
@@ -27,13 +26,17 @@
2726

2827
from ..utils import async_to_sync
2928
from ..utils.class_or_instance import class_or_instance
30-
from ..exceptions import (InvalidQueryError, RemoteServiceError, NoResultsWarning, InputWarning)
29+
from ..exceptions import (InvalidQueryError, RemoteServiceError, NoResultsWarning, InputWarning, CloudAccessWarning)
3130

32-
from . import utils
31+
from . import utils, conf
3332
from .core import MastQueryWithLogin
3433

35-
__all__ = ['Observations', 'ObservationsClass',
36-
'MastClass', 'Mast']
34+
try:
35+
from botocore.exceptions import ClientError, BotoCoreError
36+
except ImportError:
37+
ClientError = BotoCoreError = ()
38+
39+
__all__ = ['Observations', 'ObservationsClass', 'MastClass', 'Mast']
3740

3841

3942
@async_to_sync
@@ -51,6 +54,24 @@ class ObservationsClass(MastQueryWithLogin):
5154
_caom_filtered = 'Mast.Caom.Filtered'
5255
_caom_products = 'Mast.Caom.Products'
5356

57+
def __init__(self, mast_token=None):
58+
super().__init__(mast_token)
59+
self._cloud_enabled_explicitly = None # Track whether cloud access was explicitly enabled by the user
60+
61+
def _ensure_cloud_access(self):
62+
"""Ensure cloud access is initialized if appropriate."""
63+
# User explicitly disabled
64+
if self._cloud_enabled_explicitly is False:
65+
return
66+
67+
# Already initialized
68+
if self._cloud_connection is not None:
69+
return
70+
71+
# Default behavior is to enable cloud access if the config option is set, so we check that here
72+
if self._cloud_enabled_explicitly is None and conf.enable_cloud_dataset:
73+
self.enable_cloud_dataset(_internal=True)
74+
5475
def _parse_result(self, responses, *, verbose=False): # Used by the async_to_sync decorator functionality
5576
"""
5677
Parse the results of a list of `~requests.Response` objects and returns an `~astropy.table.Table` of results.
@@ -180,7 +201,7 @@ def _parse_caom_criteria(self, *, resolver=None, **criteria):
180201

181202
return position, mashup_filters
182203

183-
def enable_cloud_dataset(self, provider="AWS", profile=None, verbose=True):
204+
def enable_cloud_dataset(self, provider="AWS", profile=None, verbose=True, *, _internal=False):
184205
"""
185206
Enable downloading public files from S3 instead of MAST.
186207
Requires the boto3 library to function.
@@ -196,13 +217,21 @@ def enable_cloud_dataset(self, provider="AWS", profile=None, verbose=True):
196217
Default True.
197218
Logger to display extra info and warning.
198219
"""
199-
self._cloud_connection = CloudAccess(provider, profile, verbose)
220+
try:
221+
self._cloud_connection = CloudAccess(provider, profile, verbose)
222+
if not _internal:
223+
self._cloud_enabled_explicitly = True
224+
except ImportError as e:
225+
# boto3 or botocore is not installed
226+
self._cloud_connection = None
227+
warnings.warn(e.msg, CloudAccessWarning)
200228

201229
def disable_cloud_dataset(self):
202230
"""
203231
Disables downloading public files from S3 instead of MAST.
204232
"""
205233
self._cloud_connection = None
234+
self._cloud_enabled_explicitly = False
206235

207236
@class_or_instance
208237
def query_region_async(self, coordinates, *, radius=0.2*u.deg, pagesize=None, page=None):
@@ -656,6 +685,9 @@ def download_file(self, uri, *, local_path=None, base_url=None, cache=True, clou
656685
url : str
657686
The full url download path
658687
"""
688+
# Ensure cloud access is enabled
689+
self._ensure_cloud_access()
690+
659691
if not uri or not isinstance(uri, str):
660692
raise InvalidQueryError("A valid data product URI must be provided.")
661693

@@ -693,8 +725,9 @@ def download_file(self, uri, *, local_path=None, base_url=None, cache=True, clou
693725
NoResultsWarning)
694726
return 'SKIPPED', None, None
695727

696-
warnings.warn(f'The product {uri} was not found in the cloud. '
697-
'Falling back to MAST download.', InputWarning)
728+
if self._cloud_enabled_explicitly:
729+
warnings.warn(f'The product {uri} was not found in the cloud. '
730+
'Falling back to MAST download.', InputWarning)
698731
self._download_file(escaped_url, local_path, cache=cache, head_safe=True, verbose=verbose)
699732
except (ClientError, BotoCoreError) as ex:
700733
# Should be in cloud, but download failed
@@ -703,8 +736,9 @@ def download_file(self, uri, *, local_path=None, base_url=None, cache=True, clou
703736
NoResultsWarning)
704737
return 'SKIPPED', None, None
705738

706-
warnings.warn(f'Could not download {uri} from cloud: {ex}. Falling back to MAST download.',
707-
InputWarning)
739+
if self._cloud_enabled_explicitly:
740+
warnings.warn(f'Could not download {uri} from cloud: {ex}. Falling back to MAST download.',
741+
InputWarning)
708742
self._download_file(escaped_url, local_path, cache=cache, head_safe=True, verbose=verbose)
709743
else:
710744
if cloud_only:
@@ -771,7 +805,6 @@ def _download_files(self, products, base_dir, *, flat=False, cache=True, cloud_o
771805
status, msg, url = 'ERROR', None, None
772806

773807
cloud_uri = cloud_uri_map.get(mast_uri) if cloud_uri_map else None
774-
775808
if cloud_uri:
776809
try:
777810
self._cloud_connection.download_file_from_cloud(cloud_uri, local_path, cache, verbose)
@@ -784,8 +817,9 @@ def _download_files(self, products, base_dir, *, flat=False, cache=True, cloud_o
784817
status = 'SKIPPED'
785818
msg = str(ex)
786819
else:
787-
warnings.warn(f'Could not download {cloud_uri} from cloud: {ex}. '
788-
'Falling back to MAST download.', InputWarning)
820+
if self._cloud_enabled_explicitly:
821+
warnings.warn(f'Could not download {cloud_uri} from cloud: {ex}. '
822+
'Falling back to MAST download.', InputWarning)
789823
status, msg, url = self.download_file(mast_uri, local_path=local_path, cache=cache,
790824
force_on_prem=True, verbose=verbose)
791825
else:
@@ -797,8 +831,9 @@ def _download_files(self, products, base_dir, *, flat=False, cache=True, cloud_o
797831
status = 'SKIPPED'
798832
msg = 'Product not found in cloud'
799833
else:
800-
warnings.warn(f'The product {mast_uri} was not found in the cloud. '
801-
'Falling back to MAST download.', InputWarning)
834+
if self._cloud_enabled_explicitly:
835+
warnings.warn(f'The product {mast_uri} was not found in the cloud. '
836+
'Falling back to MAST download.', InputWarning)
802837
status, msg, url = self.download_file(mast_uri, local_path=local_path, cache=cache,
803838
force_on_prem=True, verbose=verbose)
804839
else:
@@ -899,6 +934,9 @@ def download_products(self, products, *, download_dir=None, flat=False,
899934
response : `~astropy.table.Table`
900935
The manifest of files downloaded, or status of files on disk if curl option chosen.
901936
"""
937+
# Ensure cloud access is enabled
938+
self._ensure_cloud_access()
939+
902940
# If the products list is a row we need to cast it as a table
903941
if isinstance(products, Row):
904942
products = Table(products, masked=True)
@@ -961,6 +999,9 @@ def list_cloud_datasets(self):
961999
response : list
9621000
List of dataset prefixes that support cloud data access.
9631001
"""
1002+
# Ensure cloud access is enabled
1003+
self._ensure_cloud_access()
1004+
9641005
if self._cloud_connection is None:
9651006
raise RemoteServiceError(
9661007
'Please enable anonymous cloud access by calling `enable_cloud_dataset` method. '
@@ -1027,6 +1068,8 @@ def get_cloud_uris(self, data_products=None, *, include_bucket=True, full_url=Fa
10271068
List of URIs generated from the data products. May contain entries that are None
10281069
if data_products includes products not found in the cloud.
10291070
"""
1071+
# Ensure cloud access is enabled
1072+
self._ensure_cloud_access()
10301073

10311074
if self._cloud_connection is None:
10321075
raise RemoteServiceError(
@@ -1110,6 +1153,8 @@ def get_cloud_uri(self, data_product, *, include_bucket=True, full_url=False):
11101153
Cloud URI generated from the data product. If the product cannot be
11111154
found in the cloud, None is returned.
11121155
"""
1156+
# Ensure cloud access is enabled
1157+
self._ensure_cloud_access()
11131158

11141159
if self._cloud_connection is None:
11151160
raise RemoteServiceError(

0 commit comments

Comments
 (0)