From 434c4ec2222d68743ceb66f9da432faeee13749b Mon Sep 17 00:00:00 2001 From: Adrian Damian Date: Wed, 18 Oct 2023 16:41:54 -0700 Subject: [PATCH 01/35] Enhanced results feature in ALMA --- CHANGES.rst | 5 + astroquery/alma/__init__.py | 4 +- astroquery/alma/core.py | 131 +++++++++++++++++++-- astroquery/alma/tests/data/alma-shapes.xml | 65 ++++++++++ astroquery/alma/tests/test_alma.py | 101 +++++++++++++++- astroquery/alma/tests/test_alma_remote.py | 21 +++- docs/alma/alma.rst | 61 +++++++++- docs/alma/footprint.png | Bin 0 -> 81203 bytes 8 files changed, 373 insertions(+), 15 deletions(-) create mode 100644 astroquery/alma/tests/data/alma-shapes.xml create mode 100644 docs/alma/footprint.png diff --git a/CHANGES.rst b/CHANGES.rst index 191c945556..8d6480219a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,6 +8,11 @@ New Tools and Services Service fixes and enhancements ------------------------------ +alma +^^^^ + +- Added method to return quantities instead of values and regions footprint in alma [#2855] + mpc ^^^ diff --git a/astroquery/alma/__init__.py b/astroquery/alma/__init__.py index 552a073a8c..e30235f2a4 100644 --- a/astroquery/alma/__init__.py +++ b/astroquery/alma/__init__.py @@ -38,8 +38,8 @@ class Conf(_config.ConfigNamespace): conf = Conf() -from .core import Alma, AlmaClass, ALMA_BANDS +from .core import Alma, AlmaClass, ALMA_BANDS, get_enhanced_table __all__ = ['Alma', 'AlmaClass', - 'Conf', 'conf', 'ALMA_BANDS' + 'Conf', 'conf', 'ALMA_BANDS', 'get_enhanced_table' ] diff --git a/astroquery/alma/core.py b/astroquery/alma/core.py index a1a7e4b47d..99e3aea501 100644 --- a/astroquery/alma/core.py +++ b/astroquery/alma/core.py @@ -19,6 +19,7 @@ from astropy.utils.console import ProgressBar from astropy import units as u from astropy.time import Time +from astropy.coordinates import SkyCoord from pyvo.dal.sia2 import SIA2_PARAMETERS_DESC, SIA2Service @@ -32,7 +33,7 @@ from . import conf, auth_urls from astroquery.exceptions import CorruptDataWarning -__all__ = {'AlmaClass', 'ALMA_BANDS'} +__all__ = {'AlmaClass', 'ALMA_BANDS', 'get_enhanced_table'} __doctest_skip__ = ['AlmaClass.*'] @@ -207,6 +208,106 @@ def _gen_sql(payload): return sql + where +def get_enhanced_table(result): + """ + Returns an enhanced table with quantities instead of values and regions for footprints. + Note that this function is dependent on the ``astropy`` - affiliated ``regions`` package. + """ + try: + import regions + except ImportError: + print( + "Could not import astropy-regions, which is a requirement for get_enhanced_table function in alma." + "Please refer to http://astropy-regions.readthedocs.io/en/latest/installation.html for how to install it.") + + def _parse_stcs_string(input): + csys = 'icrs' + + def _get_region(tokens): + if tokens[0] == 'polygon': + if csys == tokens[1].lower(): + tokens = tokens[2:] + else: + tokens = tokens[1:] + points = SkyCoord( + [(float(tokens[ii]), float(tokens[ii + 1])) * u.deg + for ii in range(0, len(tokens), 2)], frame=csys) + return regions.PolygonSkyRegion(points) + elif tokens[0] == 'circle': + if csys == tokens[1].lower(): + tokens = tokens[2:] + else: + tokens = tokens[1:] + return regions.CircleSkyRegion( + SkyCoord(float(tokens[0]), float(tokens[1]), unit=u.deg), + float(tokens[2]) * u.deg) + else: + raise ValueError("Unrecognized shape: " + tokens[0]) + s_region = input.lower().strip() + if s_region.startswith('union'): + res = None + # skip the union operator + input_regions = s_region[s_region.index('(') + 1:s_region.rindex( + ')')].strip() + # omit the first char in the string to force it look for the second + # occurrence + last_pos = None + not_operation = False # not operation - signals that the next substring is just the not operation + not_shape = False # not shape - signals that the next substring is a not shape + for shape in re.finditer(r'not|polygon|circle', input_regions): + pos = shape.span()[0] + if last_pos is None: + last_pos = pos + continue # this is the first elem + if shape.group() == 'not': + not_operation = True + elif not_operation: + not_shape = True + not_operation = False + last_pos = pos + continue + if res is not None: + next_shape = _get_region( + input_regions[last_pos:pos - 1].strip(' ()').split()) + if not_shape: + res = (res or next_shape) ^ next_shape + not_shape = False + else: + res = res | next_shape + else: + res = _get_region( + input_regions[last_pos:pos - 1].strip().split()) + last_pos = pos + if last_pos: + next_shape = _get_region( + input_regions[last_pos:].strip(' ()').split()) + res = res | next_shape + return res + elif 'not' in s_region: + # shape with "holes" + comps = s_region.split('not') + result = _get_region(comps[0].strip(' ()').split()) + for comp in comps[1:]: + hole = _get_region(comp.strip(' ()').split()) + result = (result or hole) ^ hole + return result + else: + return _get_region(s_region.split()) + prep_table = result.to_qtable() + s_region_parser = None + for field in result.resultstable.fields: + if ('s_region' == field.ID) and \ + ('obscore:Char.SpatialAxis.Coverage.Support.Area' == field.utype): + if 'adql:REGION' == field.xtype: + s_region_parser = _parse_stcs_string + # this is where to add other xtype parsers such as shape + break + if (s_region_parser): + for row in prep_table: + row['s_region'] = s_region_parser(row['s_region']) + return prep_table + + class AlmaAuth(BaseVOQuery, BaseQuery): """Authentication session information for passing credentials to an OIDC instance @@ -376,7 +477,8 @@ def tap_url(self): return self._tap_url def query_object_async(self, object_name, *, public=True, - science=True, payload=None, **kwargs): + science=True, payload=None, enhanced_results=False, + **kwargs): """ Query the archive for a source name. @@ -390,6 +492,9 @@ def query_object_async(self, object_name, *, public=True, science : bool True to return only science datasets, False to return only calibration, None to return both + enhanced_results : bool + True to return a table with quantities instead of just values. It + also returns the footprint as `regions` objects. payload : dict Dictionary of additional keywords. See `help`. """ @@ -398,10 +503,12 @@ def query_object_async(self, object_name, *, public=True, else: payload = {'source_name_resolver': object_name} return self.query_async(public=public, science=science, - payload=payload, **kwargs) + payload=payload, enhanced_results=enhanced_results, + **kwargs) def query_region_async(self, coordinate, radius, *, public=True, - science=True, payload=None, **kwargs): + science=True, payload=None, enhanced_results=False, + **kwargs): """ Query the ALMA archive with a source name and radius @@ -419,6 +526,9 @@ def query_region_async(self, coordinate, radius, *, public=True, calibration, None to return both payload : dict Dictionary of additional keywords. See `help`. + enhanced_results : bool + True to return a table with quantities instead of just values. It + also returns the footprints as `regions` objects. """ rad = radius if not isinstance(radius, u.Quantity): @@ -433,11 +543,12 @@ def query_region_async(self, coordinate, radius, *, public=True, payload['ra_dec'] = ra_dec return self.query_async(public=public, science=science, - payload=payload, **kwargs) + payload=payload, enhanced_results=enhanced_results, + **kwargs) def query_async(self, payload, *, public=True, science=True, legacy_columns=False, get_query_payload=False, - maxrec=None, **kwargs): + maxrec=None, enhanced_results=False, **kwargs): """ Perform a generic query with user-specified payload @@ -458,6 +569,9 @@ def query_async(self, payload, *, public=True, science=True, Flag to indicate whether to simply return the payload. maxrec : integer Cap on the amount of records returned. Default is no limit. + enhanced_results : bool + True to return a table with quantities instead of just values. It + also returns the footprints as `regions` objects. Returns ------- @@ -492,7 +606,10 @@ def query_async(self, payload, *, public=True, science=True, result = self.query_tap(query, maxrec=maxrec) if result is not None: - result = result.to_table() + if enhanced_results: + result = get_enhanced_table(result) + else: + result = result.to_table() else: # Should not happen raise RuntimeError('BUG: Unexpected result None') diff --git a/astroquery/alma/tests/data/alma-shapes.xml b/astroquery/alma/tests/data/alma-shapes.xml new file mode 100644 index 0000000000..b5d40b63cd --- /dev/null +++ b/astroquery/alma/tests/data/alma-shapes.xml @@ -0,0 +1,65 @@ + + + + + + + + + publisher dataset identifier + + + RA of central coordinates + + + DEC of central coordinates + + + Observed (tuned) reference frequency on the sky. + + + region bounded by observation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ADS/JAO.ALMA#2017.1.00358.S337.25073645418405-69.17507615555645103.4188615997524Circle ICRS 337.250736 -69.175076 0.008223
ADS/JAO.ALMA#2013.1.01014.S266.3375206596382-29.04661351679843102.53500890383512Union ICRS ( Polygon 266.149398 -29.290586 266.136226 -29.288373 266.119371 -29.256173 266.139594 -29.227875 266.179424 -29.227495 266.200095 -29.255356 266.200602 -29.262616 266.180920 -29.290650 Polygon 266.201178 -29.180809 266.185467 -29.157212 266.203140 -29.128221 266.210443 -29.124729 266.248282 -29.126396 266.267989 -29.156792 266.250347 -29.186328 266.243653 -29.189773 266.209701 -29.189725 Polygon 266.277718 -29.102223 266.255292 -29.101217 266.236836 -29.068844 266.257370 -29.039510 266.295961 -29.038762 266.318687 -29.073177 266.299686 -29.101619 Polygon 266.675227 -28.729991 266.662115 -28.727829 266.645189 -28.695695 266.665158 -28.667319 266.704771 -28.666784 266.725471 -28.694565 266.726013 -28.701823 266.706579 -28.729933)
ADS/JAO.ALMA#2019.1.00458.S83.83095833330503-5.260619444445853218.0000053819096Polygon ICRS 83.831185 -5.264207 83.827356 -5.260845 83.830732 -5.257032 83.834561 -5.260394
ADS/JAO.ALMA#2021.1.00547.S53.11983108973377-27.807152863261976104.63751498393975Polygon ICRS 53.095155 -27.862094 53.079461 -27.868350 53.070554 -27.864641 53.071634 -27.854716 53.063795 -27.848756 53.065662 -27.840191 53.057830 -27.834233 53.059697 -27.825669 53.051864 -27.819711 53.053728 -27.811149 53.045898 -27.805188 53.047766 -27.796624 53.040165 -27.791131 53.040727 -27.783321 53.168803 -27.740304 53.176556 -27.741168 53.180746 -27.747006 53.178683 -27.754479 53.186636 -27.761019 53.183259 -27.768949 53.192133 -27.774067 53.189238 -27.783465 53.198118 -27.788584 53.196264 -27.797148 53.204102 -27.803100 53.202244 -27.811668 53.209857 -27.817148 53.209307 -27.824959 Not (Polygon 53.160470 -27.801462 53.170993 -27.798574 53.173032 -27.789459 53.183944 -27.785173 53.175851 -27.781416 53.177962 -27.770657 53.168833 -27.764876 53.148836 -27.770820 53.146803 -27.779935 53.136929 -27.783386 53.144761 -27.789340 53.143267 -27.798785 53.153398 -27.795816)
ADS/JAO.ALMA#2016.1.00298.S30.57219940476191412.388698412698416106.99696191911804Union ICRS ( Polygon 24.688037 10.311983 24.686396 10.310080 24.684212 10.308752 24.681715 10.308155 24.679150 10.308346 24.675920 10.309890 24.673492 10.312999 24.669332 10.312311 24.665417 10.313483 24.663126 10.315516 24.661753 10.318233 24.661454 10.320747 24.661960 10.323226 24.663552 10.325814 24.666441 10.327836 24.666411 10.331617 24.667968 10.334789 24.671222 10.337213 24.675213 10.337795 24.677255 10.340016 24.680013 10.341357 24.683594 10.341533 24.686482 10.340470 24.688448 10.338832 24.689759 10.336779 24.692298 10.336273 24.694935 10.334697 24.697356 10.330856 24.697547 10.327333 24.696046 10.323872 24.697052 10.320175 24.696122 10.316262 24.692952 10.313013 24.690552 10.312113 Not (Polygon 24.681021 10.324923 24.681816 10.324257 24.682229 10.324855 24.681434 10.325521) Polygon 36.472346 14.440666 36.470405 14.438548 36.468128 14.437316 36.465567 14.436828 36.462472 14.437285 36.459379 14.439158 36.457576 14.441702 36.454774 14.442332 36.451320 14.444138 36.449514 14.445972 36.448380 14.448255 36.448210 14.452274 36.449149 14.454632 36.450564 14.456378 36.450365 14.459666 36.451674 14.462953 36.454306 14.465382 36.456726 14.466327 36.459320 14.466501 36.461261 14.468619 36.464035 14.470010 36.467148 14.470312 36.470623 14.469241 36.472651 14.467639 36.474091 14.465464 36.476894 14.464834 36.479112 14.463490 36.481645 14.459695 36.481908 14.456175 36.480365 14.452572 36.481385 14.449020 36.480517 14.445090 36.477360 14.441784 36.474940 14.440839 Not (Polygon 36.465340 14.453623 36.465668 14.452977 36.466327 14.453544 36.465998 14.454189))
+ +
+
diff --git a/astroquery/alma/tests/test_alma.py b/astroquery/alma/tests/test_alma.py index 3c81b18b77..c6c1177ac8 100644 --- a/astroquery/alma/tests/test_alma.py +++ b/astroquery/alma/tests/test_alma.py @@ -7,12 +7,15 @@ from astropy import units as u from astropy import coordinates as coord +from astropy import wcs from astropy.table import Table +from astropy.io import votable from astropy.coordinates import SkyCoord from astropy.time import Time +import pyvo from astroquery.alma import Alma -from astroquery.alma.core import _gen_sql, _OBSCORE_TO_ALMARESULT +from astroquery.alma.core import _gen_sql, _OBSCORE_TO_ALMARESULT, get_enhanced_table from astroquery.alma.tapsql import _val_parse @@ -354,6 +357,102 @@ def test_query(): ) +@pytest.mark.filterwarnings("ignore::astropy.utils.exceptions.AstropyUserWarning") +def test_enhanced_table(): + pytest.importorskip('regions') + import regions # to silence checkstyle + data = votable.parse(os.path.join(DATA_DIR, 'alma-shapes.xml')) + result = pyvo.dal.DALResults(data) + assert len(result) == 5 + enhanced_result = get_enhanced_table(result) + assert len(enhanced_result) == 5 + # generic ALMA WCS + ww = wcs.WCS(naxis=2) + ww.wcs.crpix = [250.0, 250.0] + ww.wcs.cdelt = [-7.500000005754e-05, 7.500000005754e-05] + ww.wcs.ctype = ['RA---SIN', 'DEC--SIN'] + for row in enhanced_result: + # check other quantities + assert row['s_ra'].unit == u.deg + assert row['s_dec'].unit == u.deg + assert row['frequency'].unit == u.GHz + ww.wcs.crval = [row['s_ra'].value, row['s_dec'].value] + sky_center = SkyCoord(row['s_ra'].value, row['s_dec'].value, unit=u.deg) + pix_center = regions.PixCoord.from_sky(sky_center, ww) + s_region = row['s_region'] + pix_region = s_region.to_pixel(ww) + if isinstance(s_region, regions.CircleSkyRegion): + # circle: https://almascience.org/aq/?mous=uid:%2F%2FA001%2FX1284%2FX146e + assert s_region.center.name == 'icrs' + assert s_region.center.ra.value == 337.250736 + assert s_region.center.ra.unit == u.deg + assert s_region.center.dec.value == -69.175076 + assert s_region.center.dec.unit == u.deg + assert s_region.radius.unit == u.deg + assert s_region.radius.value == 0.008223 + x_min = pix_region.center.x - pix_region.radius + x_max = pix_region.center.x + pix_region.radius + y_min = pix_region.center.y - pix_region.radius + y_max = pix_region.center.y + pix_region.radius + assert pix_region.contains(pix_center) + elif isinstance(s_region, regions.PolygonSkyRegion): + # simple polygon: https://almascience.org/aq/?mous=uid:%2F%2FA001%2FX1296%2FX193 + assert s_region.vertices.name == 'icrs' + x_min = pix_region.vertices.x.min() + x_max = pix_region.vertices.x.max() + y_min = pix_region.vertices.y.min() + y_max = pix_region.vertices.y.max() + assert pix_region.contains(pix_center) + elif isinstance(s_region, regions.CompoundSkyRegion): + x_min = pix_region.bounding_box.ixmin + x_max = pix_region.bounding_box.ixmax + y_min = pix_region.bounding_box.iymin + y_max = pix_region.bounding_box.iymax + if row['obs_publisher_did'] == 'ADS/JAO.ALMA#2013.1.01014.S': + # Union type of footprint: https://almascience.org/aq/?mous=uid:%2F%2FA001%2FX145%2FX3d6 + # image center is outside + assert not pix_region.contains(pix_center) + # arbitrary list of points inside each of the 4 polygons + inside_pts = ['17:46:43.655 -28:41:43.956', + '17:45:06.173 -29:04:01.549', + '17:44:53.675 -29:09:19.382', + '17:44:38.584 -29:15:31.836'] + for inside in [SkyCoord(coords, unit=(u.hourangle, u.deg)) for coords in inside_pts]: + pix_inside = regions.PixCoord.from_sky(inside, ww) + assert pix_region.contains(pix_inside) + elif row['obs_publisher_did'] == 'ADS/JAO.ALMA#2016.1.00298.S': + # pick random points inside and outside + inside = SkyCoord('1:38:44 10:18:55', unit=(u.hourangle, u.deg)) + hole = SkyCoord('1:38:44 10:19:31.5', unit=(u.hourangle, u.deg)) + pix_inside = regions.PixCoord.from_sky(inside, ww) + pix_outside = regions.PixCoord.from_sky(hole, ww) + assert pix_region.contains(pix_inside) + # assert not pix_region.contains(pix_outside) + else: + # polygon with "hole": https://almascience.org/aq/?mous=uid:%2F%2FA001%2FX158f%2FX745 + assert pix_region.contains(pix_center) + # this is an arbitrary point in the footprint "hole" + outside_point = SkyCoord('03:32:38.689 -27:47:32.750', + unit=(u.hourangle, u.deg)) + pix_outside = regions.PixCoord.from_sky(outside_point, ww) + assert not pix_region.contains(pix_outside) + else: + assert False, "Unsupported shape" + assert x_min <= x_max + assert y_min <= y_max + + # example of how to plot the footprints + # artist = pix_region.as_artist() + # import matplotlib.pyplot as plt + # axes = plt.subplot(projection=ww) + # axes.set_aspect('equal') + # axes.add_artist(artist) + # axes.set_xlim([x_min, x_max]) + # axes.set_ylim([y_min, y_max]) + # pix_region.plot() + # plt.show() + + def test_sia(): sia_mock = Mock() empty_result = Table.read(os.path.join(DATA_DIR, 'alma-empty.txt'), diff --git a/astroquery/alma/tests/test_alma_remote.py b/astroquery/alma/tests/test_alma_remote.py index 3db8969899..db8362a57e 100644 --- a/astroquery/alma/tests/test_alma_remote.py +++ b/astroquery/alma/tests/test_alma_remote.py @@ -14,7 +14,7 @@ from pyvo.dal.exceptions import DALOverflowWarning from astroquery.exceptions import CorruptDataWarning -from .. import Alma +from astroquery.alma import Alma, get_enhanced_table # ALMA tests involving staging take too long, leading to travis timeouts # TODO: make this a configuration item @@ -62,14 +62,27 @@ def test_public(self, alma): for row in results: assert row['data_rights'] == 'Proprietary' + @pytest.mark.filterwarnings( + "ignore::astropy.utils.exceptions.AstropyUserWarning") + def test_s_region(self, alma): + pytest.importorskip('regions') + import regions # to silence checkstyle + alma.help_tap() + result = alma.query_tap("select top 3 s_region from ivoa.obscore") + enhanced_result = get_enhanced_table(result) + for row in enhanced_result: + assert isinstance(row['s_region'], (regions.CircleSkyRegion, + regions.PolygonSkyRegion, + regions.CompoundSkyRegion)) + + @pytest.mark.filterwarnings( + "ignore::astropy.utils.exceptions.AstropyUserWarning") def test_SgrAstar(self, tmp_path, alma): alma.cache_location = tmp_path - result_s = alma.query_object('Sgr A*', legacy_columns=True) + result_s = alma.query_object('Sgr A*', legacy_columns=True, enhanced_results=True) assert '2013.1.00857.S' in result_s['Project code'] - # "The Brick", g0.253, is in this one - # assert b'2011.0.00217.S' in result_c['Project code'] # missing cycle 1 data def test_freq(self, alma): payload = {'frequency': '85..86'} diff --git a/docs/alma/alma.rst b/docs/alma/alma.rst index ea97bb777f..5dd9a86e91 100644 --- a/docs/alma/alma.rst +++ b/docs/alma/alma.rst @@ -211,7 +211,7 @@ One can also query by keyword, spatial resolution, etc: ... "AND science_observation='T'") # doctest: +IGNORE_OUTPUT -Use the ''help_tap'' method to learn about the ALMA 'ObsCore' keywords and +Use the ``help_tap`` method to learn about the ALMA 'ObsCore' keywords and their types. .. doctest-remote-data:: @@ -294,6 +294,65 @@ their types. velocity_resolution double m/s Estimated velocity resolution from all the spectral windows, from frequency resolution. +Query Results +============= + +Results of queries are returned in tabular format. For convenience, +the `~astroquery.alma.get_enhanced_table` function can be used to have the initial result +in a more useful format, i.e., turn values into quantities, footprint into +shape, etc. (Note: this require the `regions` Python package to be installed. + + +.. doctest-remote-data:: + + >>> from astroquery.alma import Alma, get_enhanced_table + >>> alma = Alma() + >>> alma.archive_url = 'https://almascience.eso.org' # optional to make doctest work + >>> res = alma.query_tap("select top 1 * from ivoa.ObsCore where obs_publisher_did='ADS/JAO.ALMA#2011.0.00087.S'") + >>> enhanced_res = get_enhanced_table(res) + >>> enhanced_res[0]['s_ra'] + + >>> enhanced_res[0]['s_region'] + )> + +To further draw the footprint: + +.. doctest-skip:: + + >>> from astropy import wcs + >>> import matplotlib.pyplot as plt + >>> # Create a WCS; for plotting, all that matters is that it is centered on our target region + >>> ww = wcs.WCS(naxis=2) + >>> ww.wcs.crpix = [250.0, 250.0] + >>> ww.wcs.cdelt = [-7.500000005754e-05, 7.500000005754e-05] + >>> ww.wcs.ctype = ['RA---SIN', 'DEC--SIN'] + >>> ww.wcs.crval = [enhanced_res[0]['s_ra'].value, enhanced_res[0]['s_dec'].value] + >>> pix_region = enhanced_res[0]['s_region'].to_pixel(ww) + >>> artist = pix_region.as_artist() + >>> axes = plt.subplot(projection=ww) + >>> axes.set_aspect('equal') + >>> axes.add_artist(artist) + >>> axes.axis(pix_region.bounding_box.extent) + >>> pix_region.plot() + >>> plt.show() + + +.. image:: footprint.png + :align: center + :scale: 75% + :alt: observation footprint + + +The above footprint could be transformed into a pixel region and have the mask +extracted or combined with other regions. Refer to the Astropy affiliated +''regions'' package for more details. + + Downloading Data ================ diff --git a/docs/alma/footprint.png b/docs/alma/footprint.png new file mode 100644 index 0000000000000000000000000000000000000000..d4066c4e5f1c7813e633706ca1acd11aa0892635 GIT binary patch literal 81203 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 Q_rO18c@4P|nTw(S3r*TjJpcdz literal 0 HcmV?d00001 From 630c700371543fddea186360996ac81fc4a9a61c Mon Sep 17 00:00:00 2001 From: Sam Bianco Date: Thu, 30 May 2024 15:57:32 -0400 Subject: [PATCH 02/35] append filename to local path --- astroquery/mast/observations.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/astroquery/mast/observations.py b/astroquery/mast/observations.py index a3e0a3ea5e..7bddb00b8e 100644 --- a/astroquery/mast/observations.py +++ b/astroquery/mast/observations.py @@ -533,9 +533,8 @@ def download_file(self, uri, *, local_path=None, base_url=None, cache=True, clou data_url = base_url + "?uri=" + uri # create a local file path if none is input. Use current directory as default. - if not local_path: - filename = os.path.basename(uri) - local_path = os.path.join(os.path.abspath('.'), filename) + filename = os.path.basename(uri) + local_path = os.path.join(os.path.abspath('.') if not local_path else local_path, filename) # recreate the data_product key for cloud connection check data_product = {'dataURI': uri} From 8464c1742ed2582c42425155f0cb34894deca15d Mon Sep 17 00:00:00 2001 From: Sam Bianco Date: Thu, 30 May 2024 16:17:00 -0400 Subject: [PATCH 03/35] Update docs --- docs/mast/mast_obsquery.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/mast/mast_obsquery.rst b/docs/mast/mast_obsquery.rst index 86a444cc67..55511ff66c 100644 --- a/docs/mast/mast_obsquery.rst +++ b/docs/mast/mast_obsquery.rst @@ -373,9 +373,9 @@ curl script that can be used to download the files at a later time. Downloading a Single File ------------------------- -You can download a single data product file using the `~astroquery.mast.ObservationsClass.download_file` -method, and passing in a MAST Data URI. The default is to download the file the current working directory, -which can be changed with the ``local_path`` keyword argument. +You can download a single data product file by using the `~astroquery.mast.ObservationsClass.download_file` +method and passing in a MAST Data URI. The default is to download the file to the current working directory, but +you can specify the download directory or filepath with the ``local_path`` keyword argument. .. doctest-remote-data:: From e24ac99b33379a5322e1ac00bc8ffa2d25fc745c Mon Sep 17 00:00:00 2001 From: Sam Bianco Date: Thu, 30 May 2024 16:45:21 -0400 Subject: [PATCH 04/35] Parse a directory or filename --- astroquery/mast/observations.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/astroquery/mast/observations.py b/astroquery/mast/observations.py index 7bddb00b8e..c0c4500d22 100644 --- a/astroquery/mast/observations.py +++ b/astroquery/mast/observations.py @@ -508,7 +508,7 @@ def download_file(self, uri, *, local_path=None, base_url=None, cache=True, clou uri : str The product dataURI, e.g. mast:JWST/product/jw00736-o039_t001_miri_ch1-long_x1d.fits local_path : str - Directory in which the files will be downloaded. Defaults to current working directory. + Directory or filename to which the file will be downloaded. Defaults to current working directory. base_url: str A base url to use when downloading. Default is the MAST Portal API cache : bool @@ -532,9 +532,12 @@ def download_file(self, uri, *, local_path=None, base_url=None, cache=True, clou base_url = base_url if base_url else self._portal_api_connection.MAST_DOWNLOAD_URL data_url = base_url + "?uri=" + uri - # create a local file path if none is input. Use current directory as default. + # parse a local file path from local_path parameter. Use current directory as default. filename = os.path.basename(uri) - local_path = os.path.join(os.path.abspath('.') if not local_path else local_path, filename) + if not local_path: # local file path is not defined + local_path = os.path.join(os.path.abspath('.'), filename) + elif os.path.isdir(local_path): # local file path is directory + local_path = os.path.join(local_path, filename) # append filename # recreate the data_product key for cloud connection check data_product = {'dataURI': uri} From 53656683ef1002670caa5a9cb3dfb8022e9b769e Mon Sep 17 00:00:00 2001 From: Sam Bianco Date: Thu, 30 May 2024 16:46:52 -0400 Subject: [PATCH 05/35] style fixes --- astroquery/mast/observations.py | 6 +++--- astroquery/mast/tests/test_mast_remote.py | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/astroquery/mast/observations.py b/astroquery/mast/observations.py index c0c4500d22..348a56a919 100644 --- a/astroquery/mast/observations.py +++ b/astroquery/mast/observations.py @@ -534,10 +534,10 @@ def download_file(self, uri, *, local_path=None, base_url=None, cache=True, clou # parse a local file path from local_path parameter. Use current directory as default. filename = os.path.basename(uri) - if not local_path: # local file path is not defined + if not local_path: # local file path is not defined local_path = os.path.join(os.path.abspath('.'), filename) - elif os.path.isdir(local_path): # local file path is directory - local_path = os.path.join(local_path, filename) # append filename + elif os.path.isdir(local_path): # local file path is directory + local_path = os.path.join(local_path, filename) # append filename # recreate the data_product key for cloud connection check data_product = {'dataURI': uri} diff --git a/astroquery/mast/tests/test_mast_remote.py b/astroquery/mast/tests/test_mast_remote.py index 39ea9e8805..41ff2a52f5 100644 --- a/astroquery/mast/tests/test_mast_remote.py +++ b/astroquery/mast/tests/test_mast_remote.py @@ -399,6 +399,20 @@ def test_observations_download_file(self, tmp_path): # download it result = mast.Observations.download_file(uri, local_path=local_path) assert result == ('COMPLETE', None, None) + assert os.path.exists(Path(os.getcwd(), filename)) + Path.unlink(filename) # clean up file + + # download with directory as local_path parameter + local_path = Path(tmp_path, filename) + result = mast.Observations.download_file(uri, local_path=tmp_path) + assert result == ('COMPLETE', None, None) + assert os.path.exists(local_path) + + # download with filename as local_path parameter + local_path_file = Path(tmp_path, "test.fits") + result = mast.Observations.download_file(uri, local_path=local_path_file) + assert result == ('COMPLETE', None, None) + assert os.path.exists(local_path_file) @pytest.mark.parametrize("test_data_uri, expected_cloud_uri", [ ("mast:HST/product/u24r0102t_c1f.fits", From c37e06133adfb15bdcbc4c1f51e118790daacdc7 Mon Sep 17 00:00:00 2001 From: Sam Bianco Date: Fri, 31 May 2024 17:36:20 -0400 Subject: [PATCH 06/35] update changes file Added pull request number to changes --- CHANGES.rst | 5 +++++ astroquery/mast/tests/test_mast_remote.py | 7 ++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index d6d3ac29a3..6aaa42d705 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -53,6 +53,11 @@ jplhorizons - Add missing column definitions, especially for ``refraction=True`` and ``extra_precision=True``. [#2986] +mast +^^^^ + +- Fix bug in which the ``local_path`` parameter for the ``mast.observations.download_file`` method does not accept a directory. [#3016] + 0.4.7 (2024-03-08) ================== diff --git a/astroquery/mast/tests/test_mast_remote.py b/astroquery/mast/tests/test_mast_remote.py index 41ff2a52f5..2af0b229eb 100644 --- a/astroquery/mast/tests/test_mast_remote.py +++ b/astroquery/mast/tests/test_mast_remote.py @@ -394,10 +394,11 @@ def test_observations_download_file(self, tmp_path): # pull the URI of a single product uri = products['dataURI'][0] - local_path = Path(tmp_path, Path(uri).name) + filename = Path(uri).name - # download it - result = mast.Observations.download_file(uri, local_path=local_path) + # download with unspecified local_path parameter + # should download to current working directory + result = mast.Observations.download_file(uri) assert result == ('COMPLETE', None, None) assert os.path.exists(Path(os.getcwd(), filename)) Path.unlink(filename) # clean up file From 22a7907c174ef8c7df484758dc0ca0e1a311c876 Mon Sep 17 00:00:00 2001 From: Sam Bianco Date: Fri, 31 May 2024 17:58:26 -0400 Subject: [PATCH 07/35] clean up test artifacts --- astroquery/mast/observations.py | 2 +- astroquery/mast/tests/test_mast.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/astroquery/mast/observations.py b/astroquery/mast/observations.py index 348a56a919..c045570cd3 100644 --- a/astroquery/mast/observations.py +++ b/astroquery/mast/observations.py @@ -535,7 +535,7 @@ def download_file(self, uri, *, local_path=None, base_url=None, cache=True, clou # parse a local file path from local_path parameter. Use current directory as default. filename = os.path.basename(uri) if not local_path: # local file path is not defined - local_path = os.path.join(os.path.abspath('.'), filename) + local_path = filename elif os.path.isdir(local_path): # local file path is directory local_path = os.path.join(local_path, filename) # append filename diff --git a/astroquery/mast/tests/test_mast.py b/astroquery/mast/tests/test_mast.py index dd34af24fa..6baf3bc5c4 100644 --- a/astroquery/mast/tests/test_mast.py +++ b/astroquery/mast/tests/test_mast.py @@ -498,7 +498,8 @@ def test_observations_download_products(patch_post, tmpdir): # passing row product products = mast.Observations.get_product_list('2003738726') - result1 = mast.Observations.download_products(products[0]) + result1 = mast.Observations.download_products(products[0], + download_dir=str(tmpdir)) assert isinstance(result1, Table) From 1a69e0709f58e423cac7c16f32dead250168d05b Mon Sep 17 00:00:00 2001 From: Sam Bianco Date: Mon, 3 Jun 2024 22:25:23 -0400 Subject: [PATCH 08/35] create dir if it does not exist --- astroquery/mast/observations.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/astroquery/mast/observations.py b/astroquery/mast/observations.py index c045570cd3..7517f858ec 100644 --- a/astroquery/mast/observations.py +++ b/astroquery/mast/observations.py @@ -6,6 +6,7 @@ This module contains various methods for querying MAST observations. """ +from pathlib import Path import warnings import time import os @@ -536,8 +537,12 @@ def download_file(self, uri, *, local_path=None, base_url=None, cache=True, clou filename = os.path.basename(uri) if not local_path: # local file path is not defined local_path = filename - elif os.path.isdir(local_path): # local file path is directory - local_path = os.path.join(local_path, filename) # append filename + else: + path = Path(local_path) + if not path.suffix: # local_path is a directory + local_path = path / filename # append filename + if not path.exists(): # create directory if it doesn't exist + path.mkdir(parents=True, exist_ok=True) # recreate the data_product key for cloud connection check data_product = {'dataURI': uri} From ed85ba8d5b2ce844151d844539b80adf2d6d5008 Mon Sep 17 00:00:00 2001 From: Robert Stein Date: Wed, 12 Jun 2024 14:56:09 -0700 Subject: [PATCH 09/35] Update VSA url --- astroquery/vsa/core.py | 2 +- astroquery/vsa/tests/test_vista_remote.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/astroquery/vsa/core.py b/astroquery/vsa/core.py index 09690dc80c..1488b5e6f5 100644 --- a/astroquery/vsa/core.py +++ b/astroquery/vsa/core.py @@ -65,7 +65,7 @@ def __init__(self, *, username=None, password=None, community=None, community=community, password=password) - self.BASE_URL = 'http://horus.roe.ac.uk:8080/vdfs/' + self.BASE_URL = "http://vsa.roe.ac.uk:8080/vdfs/" self.LOGIN_URL = self.BASE_URL + "DBLogin" self.IMAGE_URL = self.BASE_URL + "GetImage" self.ARCHIVE_URL = self.BASE_URL + "ImageList" diff --git a/astroquery/vsa/tests/test_vista_remote.py b/astroquery/vsa/tests/test_vista_remote.py index 2ccea18252..f8c709b010 100644 --- a/astroquery/vsa/tests/test_vista_remote.py +++ b/astroquery/vsa/tests/test_vista_remote.py @@ -19,7 +19,7 @@ class TestVista: @pytest.mark.dependency(name='vsa_up') def test_is_vsa_up(self): try: - vista._request("GET", "http://horus.roe.ac.uk:8080/vdfs/VgetImage_form.jsp") + vista._request("GET", "http://vsa.roe.ac.uk:8080/vdfs/VgetImage_form.jsp") except Exception as ex: pytest.fail("VISTA appears to be down. Exception was: {0}".format(ex)) From 874b2c0a5c07a6106639fc7f1054edd4d6b34306 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Brigitta=20Sip=C5=91cz?= Date: Wed, 12 Jun 2024 15:30:16 -0700 Subject: [PATCH 10/35] Adding changelog [skip ci] --- CHANGES.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 0818cdbfd6..84a5c4e628 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -41,6 +41,11 @@ skyview - Overlay arguments ``lut``, ``grid``, and ``gridlabel`` are removed, as they only apply to output types not returned by Astroquery [#2979] +vsa +^^^ + +- Updating base URL to fix 404 responses. [#3033] + Infrastructure, Utility and Other Changes and Additions ------------------------------------------------------- From affe3cebb7f7f4a8a551c12e8d7b25a4e4c5fe29 Mon Sep 17 00:00:00 2001 From: Sam Bianco Date: Fri, 14 Jun 2024 11:22:46 -0400 Subject: [PATCH 11/35] Reduce execution time of remote test suite --- CHANGES.rst | 1 + astroquery/mast/tests/test_mast_remote.py | 1051 ++++++++++----------- 2 files changed, 495 insertions(+), 557 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 84a5c4e628..4f156350bd 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -87,6 +87,7 @@ mast ^^^^ - Fix bug in which the ``local_path`` parameter for the ``mast.observations.download_file`` method does not accept a directory. [#3016] +- Optimize remote test suite to improve performance and reduce execution time. [#3036] 0.4.7 (2024-03-08) diff --git a/astroquery/mast/tests/test_mast_remote.py b/astroquery/mast/tests/test_mast_remote.py index 2af0b229eb..e1ddfea34b 100644 --- a/astroquery/mast/tests/test_mast_remote.py +++ b/astroquery/mast/tests/test_mast_remote.py @@ -12,21 +12,23 @@ from astropy.io import fits import astropy.units as u -from astroquery import mast +from astroquery.mast import Observations, utils, Mast, Catalogs, Hapcut, Tesscut, Zcut from ..utils import ResolverError from ...exceptions import (InputWarning, InvalidQueryError, MaxResultsWarning, NoResultsWarning) -OBSID = '1647157' - - @pytest.fixture(scope="module") def msa_product_table(): # Pull products for a JWST NIRSpec MSA observation with 6 known # duplicates of the MSA configuration file, propID=2736 - products = mast.Observations.get_product_list("87602009") + obs = Observations.query_criteria(proposal_id=2736, + obs_collection='JWST', + instrument_name='NIRSPEC/MSA', + filters='F170LP;G235M', + target_name='MPTCAT-0628') + products = Observations.get_product_list(obs['obsid'][0]) # Filter out everything but the MSA config file mask = np.char.find(products["dataURI"], "_msa.fits") != -1 @@ -43,10 +45,10 @@ class TestMast: ############### def test_resolve_object(self): - m101_loc = mast.utils.resolve_object("M101") - assert round(m101_loc.separation(SkyCoord("210.80227 54.34895", unit='deg')).value, 4) == 0 + m101_loc = utils.resolve_object("M101") + assert round(m101_loc.separation(SkyCoord("210.80243 54.34875", unit='deg')).value, 4) == 0 - ticobj_loc = mast.utils.resolve_object("TIC 141914082") + ticobj_loc = utils.resolve_object("TIC 141914082") assert round(ticobj_loc.separation(SkyCoord("94.6175354 -72.04484622", unit='deg')).value, 4) == 0 ################### @@ -57,22 +59,22 @@ def test_mast_service_request_async(self): service = 'Mast.Caom.Cone' params = {'ra': 184.3, 'dec': 54.5, - 'radius': 0.2} - responses = mast.Mast.service_request_async(service, params) + 'radius': 0.001} + responses = Mast.service_request_async(service, params) assert isinstance(responses, list) def test_mast_service_request(self): # clear columns config - mast.Mast._column_configs = dict() + Mast._column_configs = dict() service = 'Mast.Caom.Cone' params = {'ra': 184.3, 'dec': 54.5, - 'radius': 0.2} + 'radius': 0.001} - result = mast.Mast.service_request(service, params) + result = Mast.service_request(service, params) # Is result in the right format assert isinstance(result, Table) @@ -85,9 +87,9 @@ def test_mast_service_request(self): def test_mast_query(self): # clear columns config - mast.Mast._column_configs = dict() + Mast._column_configs = dict() - result = mast.Mast.mast_query('Mast.Caom.Cone', ra=184.3, dec=54.5, radius=0.2) + result = Mast.mast_query('Mast.Caom.Cone', ra=184.3, dec=54.5, radius=0.2) # Is result in the right format assert isinstance(result, Table) @@ -99,7 +101,7 @@ def test_mast_query(self): assert len(result[np.where(result["obs_id"] == "6374399093149532160")]) == 2 def test_mast_session_info(self): - sessionInfo = mast.Mast.session_info(verbose=False) + sessionInfo = Mast.session_info(verbose=False) assert sessionInfo['ezid'] == 'anonymous' assert sessionInfo['token'] is None @@ -108,21 +110,21 @@ def test_mast_session_info(self): ########################### def test_observations_list_missions(self): - missions = mast.Observations.list_missions() + missions = Observations.list_missions() assert isinstance(missions, list) for m in ['HST', 'HLA', 'GALEX', 'Kepler']: assert m in missions def test_get_metadata(self): # observations - meta_table = mast.Observations.get_metadata("observations") + meta_table = Observations.get_metadata("observations") assert isinstance(meta_table, Table) assert "Column Name" in meta_table.colnames assert "Mission" in meta_table["Column Label"] assert "obsid" in meta_table["Column Name"] # products - meta_table = mast.Observations.get_metadata("products") + meta_table = Observations.get_metadata("products") assert isinstance(meta_table, Table) assert "Column Name" in meta_table.colnames assert "Observation ID" in meta_table["Column Label"] @@ -131,137 +133,136 @@ def test_get_metadata(self): # query functions def test_observations_query_region_async(self): - responses = mast.Observations.query_region_async("322.49324 12.16683", radius="0.005 deg") + responses = Observations.query_region_async("322.49324 12.16683", radius="0.005 deg") assert isinstance(responses, list) def test_observations_query_region(self): - # clear columns config - mast.Observations._column_configs = dict() + Observations._column_configs = dict() - result = mast.Observations.query_region("322.49324 12.16683", radius="0.005 deg") + result = Observations.query_region("322.49324 12.16683", radius="0.005 deg") assert isinstance(result, Table) assert len(result) > 500 assert result[np.where(result['obs_id'] == '00031992001')] - result = mast.Observations.query_region("322.49324 12.16683", radius="0.005 deg", - pagesize=1, page=1) + result = Observations.query_region("322.49324 12.16683", radius="0.005 deg", + pagesize=1, page=1) assert isinstance(result, Table) assert len(result) == 1 def test_observations_query_object_async(self): - responses = mast.Observations.query_object_async("M8", radius=".02 deg") + responses = Observations.query_object_async("M8", radius=".02 deg") assert isinstance(responses, list) def test_observations_query_object(self): - # clear columns config - mast.Observations._column_configs = dict() + Observations._column_configs = dict() - result = mast.Observations.query_object("M8", radius=".04 deg") + result = Observations.query_object("M8", radius=".04 deg") assert isinstance(result, Table) assert len(result) > 150 assert result[np.where(result['obs_id'] == 'ktwo200071160-c92_lc')] def test_observations_query_criteria_async(self): # without position - responses = mast.Observations.query_criteria_async(dataproduct_type=["image"], - proposal_pi="*Ost*", - s_dec=[43.5, 45.5]) + responses = Observations.query_criteria_async(dataproduct_type=["image"], + proposal_pi="*Ost*", + s_dec=[43.5, 43.6]) assert isinstance(responses, list) # with position - responses = mast.Observations.query_criteria_async(filters=["NUV", "FUV"], - objectname="M101") + responses = Observations.query_criteria_async(filters=["NUV", "FUV"], + objectname="M10") assert isinstance(responses, list) def test_observations_query_criteria(self): # clear columns config - mast.Observations._column_configs = dict() + Observations._column_configs = dict() # without position - result = mast.Observations.query_criteria(instrument_name="*WFPC2*", - proposal_id=8169, - t_min=[49335, 51499]) + result = Observations.query_criteria(instrument_name="*WFPC2*", + proposal_id=8169, + t_min=[51361, 51362]) assert isinstance(result, Table) - assert len(result) == 57 + assert len(result) == 13 assert ((result['obs_collection'] == 'HST') | (result['obs_collection'] == 'HLA')).all() # with position - result = mast.Observations.query_criteria(filters=["NUV", "FUV"], - obs_collection="GALEX", - objectname="M101") + result = Observations.query_criteria(objectname="M10", + filters=["NUV", "FUV"], + obs_collection="GALEX") assert isinstance(result, Table) - assert len(result) == 12 + assert len(result) == 7 assert (result['obs_collection'] == 'GALEX').all() - assert sum(result['filters'] == 'NUV') == 6 + assert sum(result['filters'] == 'NUV') == 4 - result = mast.Observations.query_criteria(objectname="M101", - dataproduct_type="IMAGE", intentType="calibration") + result = Observations.query_criteria(objectname="M10", + dataproduct_type="IMAGE", + intentType="calibration") assert (result["intentType"] == "calibration").all() # count functions def test_observations_query_region_count(self): - maxRes = mast.Observations.query_criteria_count() - result = mast.Observations.query_region_count("322.49324 12.16683", radius="0.4 deg") + maxRes = Observations.query_criteria_count() + result = Observations.query_region_count("322.49324 12.16683", + radius=1e-10 * u.deg) assert isinstance(result, (np.int64, int)) - assert result > 1800 + assert result > 840 assert result < maxRes def test_observations_query_object_count(self): - maxRes = mast.Observations.query_criteria_count() - result = mast.Observations.query_object_count("M8", radius=".02 deg") + maxRes = Observations.query_criteria_count() + result = Observations.query_object_count("M8", radius=".02 deg") assert isinstance(result, (np.int64, int)) assert result > 150 assert result < maxRes def test_observations_query_criteria_count(self): - maxRes = mast.Observations.query_criteria_count() - result = mast.Observations.query_criteria_count(proposal_pi="*Osten*", - proposal_id=8880) + maxRes = Observations.query_criteria_count() + result = Observations.query_criteria_count(proposal_id=8880) assert isinstance(result, (np.int64, int)) - assert result == 7 + assert result == 8 assert result < maxRes # product functions def test_observations_get_product_list_async(self): - test_obs = mast.Observations.query_criteria(filters=["NUV", "FUV"], objectname="M101") + test_obs = Observations.query_criteria(filters=["NUV", "FUV"], objectname="M10") - responses = mast.Observations.get_product_list_async(test_obs[0]["obsid"]) + responses = Observations.get_product_list_async(test_obs[0]["obsid"]) assert isinstance(responses, list) - responses = mast.Observations.get_product_list_async(test_obs[2:3]) + responses = Observations.get_product_list_async(test_obs[2:3]) assert isinstance(responses, list) - observations = mast.Observations.query_object("M8", radius=".02 deg") - responses = mast.Observations.get_product_list_async(observations[0]) + observations = Observations.query_object("M8", radius=".02 deg") + responses = Observations.get_product_list_async(observations[0]) assert isinstance(responses, list) - responses = mast.Observations.get_product_list_async(observations[0:4]) + responses = Observations.get_product_list_async(observations[0:4]) assert isinstance(responses, list) def test_observations_get_product_list(self): # clear columns config - mast.Observations._column_configs = dict() + Observations._column_configs = dict() - observations = mast.Observations.query_object("M8", radius=".04 deg") + observations = Observations.query_object("M8", radius=".04 deg") test_obs_id = str(observations[0]['obsid']) mult_obs_ids = str(observations[0]['obsid']) + ',' + str(observations[1]['obsid']) - result1 = mast.Observations.get_product_list(test_obs_id) - result2 = mast.Observations.get_product_list(observations[0]) + result1 = Observations.get_product_list(test_obs_id) + result2 = Observations.get_product_list(observations[0]) filenames1 = list(result1['productFilename']) filenames2 = list(result2['productFilename']) assert isinstance(result1, Table) assert len(result1) == len(result2) assert set(filenames1) == set(filenames2) - result1 = mast.Observations.get_product_list(mult_obs_ids) - result2 = mast.Observations.get_product_list(observations[0:2]) + result1 = Observations.get_product_list(mult_obs_ids) + result2 = Observations.get_product_list(observations[0:2]) filenames1 = result1['productFilename'] filenames2 = result2['productFilename'] assert isinstance(result1, Table) @@ -269,54 +270,56 @@ def test_observations_get_product_list(self): assert set(filenames1) == set(filenames2) obsLoc = np.where(observations["obs_id"] == 'ktwo200071160-c92_lc') - result = mast.Observations.get_product_list(observations[obsLoc]) + result = Observations.get_product_list(observations[obsLoc]) assert isinstance(result, Table) assert len(result) == 1 obsLocs = np.where((observations['target_name'] == 'NGC6523') & (observations['obs_collection'] == "IUE")) - result = mast.Observations.get_product_list(observations[obsLocs]) + result = Observations.get_product_list(observations[obsLocs]) obs_collection = np.unique(list(result['obs_collection'])) assert isinstance(result, Table) assert len(obs_collection) == 1 assert obs_collection[0] == 'IUE' def test_observations_filter_products(self): - observations = mast.Observations.query_object("M8", radius=".04 deg") + observations = Observations.query_object("M8", radius=".04 deg") obsLoc = np.where(observations["obs_id"] == 'ktwo200071160-c92_lc') - products = mast.Observations.get_product_list(observations[obsLoc]) - result = mast.Observations.filter_products(products, - productType=["SCIENCE"], - mrp_only=False) + products = Observations.get_product_list(observations[obsLoc]) + result = Observations.filter_products(products, + productType=["SCIENCE"], + mrp_only=False) assert isinstance(result, Table) assert len(result) == sum(products['productType'] == "SCIENCE") - # test downloads 150+ files, 50MB+, TODO: revise OBSID to query only a few, small files for download - @pytest.mark.skip("Tests should not download this much data. Skipping until revised.") def test_observations_download_products(self, tmpdir): - test_obs_id = OBSID - result = mast.Observations.download_products(test_obs_id, - download_dir=str(tmpdir), - productType=["SCIENCE"], - mrp_only=False) + def check_filepath(path): + assert os.path.isfile(path) + os.remove(path) + + test_obs_id = '25119363' + result = Observations.download_products(test_obs_id, + download_dir=str(tmpdir), + productType=["SCIENCE"], + mrp_only=False) assert isinstance(result, Table) for row in result: if row['Status'] == 'COMPLETE': - assert os.path.isfile(row['Local Path']) + check_filepath(row['Local Path']) # just get the curl script - result = mast.Observations.download_products(test_obs_id[0]["obsid"], - download_dir=str(tmpdir), - curl_flag=True, - productType=["SCIENCE"], - mrp_only=False) + result = Observations.download_products(test_obs_id, + download_dir=str(tmpdir), + curl_flag=True, + productType=["SCIENCE"], + mrp_only=False) assert isinstance(result, Table) - assert os.path.isfile(result['Local Path'][0]) + check_filepath(result['Local Path'][0]) # check for row input - result1 = mast.Observations.get_product_list(test_obs_id[0]["obsid"]) - result2 = mast.Observations.download_products(result1[0]) + result1 = Observations.get_product_list(test_obs_id) + result2 = Observations.download_products(result1[0]) assert isinstance(result2, Table) - assert os.path.isfile(result2['Local Path'][0]) + check_filepath(result2['Local Path'][0]) assert len(result2) == 1 def test_observations_download_products_flat(self, tmp_path, msa_product_table): @@ -327,8 +330,7 @@ def test_observations_download_products_flat(self, tmp_path, msa_product_table): assert len(products) == 6 # Download with flat=True - manifest = mast.Observations.download_products(products, flat=True, - download_dir=tmp_path) + manifest = Observations.download_products(products, flat=True, download_dir=tmp_path) assert Path(manifest["Local Path"][0]).parent == tmp_path @@ -341,9 +343,9 @@ def test_observations_download_products_flat_curl(self, tmp_path, msa_product_ta # Download with flat=True, curl_flag=True, look for warning with pytest.warns(InputWarning): - mast.Observations.download_products(products, flat=True, - curl_flag=True, - download_dir=tmp_path) + Observations.download_products(products, flat=True, + curl_flag=True, + download_dir=tmp_path) def test_observations_download_products_no_duplicates(self, tmp_path, caplog, msa_product_table): @@ -353,8 +355,7 @@ def test_observations_download_products_no_duplicates(self, tmp_path, caplog, ms assert len(products) == 6 # Download the product - manifest = mast.Observations.download_products(products, - download_dir=tmp_path) + manifest = Observations.download_products(products, download_dir=tmp_path) # Check that it downloads the MSA config file only once assert len(manifest) == 1 @@ -371,26 +372,30 @@ def test_observations_get_cloud_uris_no_duplicates(self, msa_product_table): assert len(products) == 6 # enable access to public AWS S3 bucket - mast.Observations.enable_cloud_dataset(provider='AWS') + Observations.enable_cloud_dataset(provider='AWS') # Check for cloud URIs. Accept a NoResultsWarning if AWS S3 # doesn't have the file. It doesn't matter as we're only checking # that the duplicate products have been culled to a single one. with pytest.warns(NoResultsWarning): - uris = mast.Observations.get_cloud_uris(products) + uris = Observations.get_cloud_uris(products) assert len(uris) == 1 def test_observations_download_file(self, tmp_path): + def check_result(result, path): + assert result == ('COMPLETE', None, None) + assert os.path.exists(path) + # get observations from GALEX instrument with query_criteria - observations = mast.Observations.query_criteria(objectname='M1', - radius=0.2, - instrument_name='GALEX') + observations = Observations.query_criteria(objectname='M10', + radius=0.001, + instrument_name='GALEX') assert len(observations) > 0, 'No results found for GALEX query.' # pull data products from a single observation - products = mast.Observations.get_product_list(observations['obsid'][0]) + products = Observations.get_product_list(observations['obsid'][0]) # pull the URI of a single product uri = products['dataURI'][0] @@ -398,22 +403,19 @@ def test_observations_download_file(self, tmp_path): # download with unspecified local_path parameter # should download to current working directory - result = mast.Observations.download_file(uri) - assert result == ('COMPLETE', None, None) - assert os.path.exists(Path(os.getcwd(), filename)) + result = Observations.download_file(uri) + check_result(result, Path(os.getcwd(), filename)) Path.unlink(filename) # clean up file # download with directory as local_path parameter local_path = Path(tmp_path, filename) - result = mast.Observations.download_file(uri, local_path=tmp_path) - assert result == ('COMPLETE', None, None) - assert os.path.exists(local_path) + result = Observations.download_file(uri, local_path=tmp_path) + check_result(result, local_path) # download with filename as local_path parameter local_path_file = Path(tmp_path, "test.fits") - result = mast.Observations.download_file(uri, local_path=local_path_file) - assert result == ('COMPLETE', None, None) - assert os.path.exists(local_path_file) + result = Observations.download_file(uri, local_path=local_path_file) + check_result(result, local_path_file) @pytest.mark.parametrize("test_data_uri, expected_cloud_uri", [ ("mast:HST/product/u24r0102t_c1f.fits", @@ -428,10 +430,10 @@ def test_get_cloud_uri(self, test_data_uri, expected_cloud_uri): product = Table() product['dataURI'] = [test_data_uri] # enable access to public AWS S3 bucket - mast.Observations.enable_cloud_dataset() + Observations.enable_cloud_dataset() # get uri - uri = mast.Observations.get_cloud_uri(product[0]) + uri = Observations.get_cloud_uri(product[0]) assert len(uri) > 0, f'Product for dataURI {test_data_uri} was not found in the cloud.' assert uri == expected_cloud_uri, f'Cloud URI does not match expected. ({uri} != {expected_cloud_uri})' @@ -441,16 +443,16 @@ def test_get_cloud_uris(self): test_obs_id = '25568122' # get a product list - products = mast.Observations.get_product_list(test_obs_id)[24:] + products = Observations.get_product_list(test_obs_id)[24:] assert len(products) > 0, (f'No products found for OBSID {test_obs_id}. ' 'Unable to move forward with getting URIs from the cloud.') # enable access to public AWS S3 bucket - mast.Observations.enable_cloud_dataset() + Observations.enable_cloud_dataset() # get uris - uris = mast.Observations.get_cloud_uris(products) + uris = Observations.get_cloud_uris(products) assert len(uris) > 0, f'Products for OBSID {test_obs_id} were not found in the cloud.' @@ -460,335 +462,324 @@ def test_get_cloud_uris(self): # query functions def test_catalogs_query_region_async(self): - responses = mast.Catalogs.query_region_async("158.47924 -7.30962", catalog="Galex") + in_rad = 0.001 * u.deg + responses = Catalogs.query_region_async("158.47924 -7.30962", + radius=in_rad, + catalog="Galex") assert isinstance(responses, list) # Default catalog is HSC - responses = mast.Catalogs.query_region_async("322.49324 12.16683", - radius="0.02 deg") + responses = Catalogs.query_region_async("322.49324 12.16683", + radius=in_rad) assert isinstance(responses, list) - responses = mast.Catalogs.query_region_async("322.49324 12.16683", - radius="0.02 deg", - catalog="panstarrs", table="mean") + responses = Catalogs.query_region_async("322.49324 12.16683", + radius=in_rad, + catalog="panstarrs", + table="mean") assert isinstance(responses, Response) def test_catalogs_query_region(self): + def check_result(result, row, exp_values): + assert isinstance(result, Table) + for k, v in exp_values.items(): + assert result[row][k] == v # clear columns config - mast.Catalogs._column_configs = dict() + Catalogs._column_configs = dict() in_radius = 0.1 * u.deg - result = mast.Catalogs.query_region("158.47924 -7.30962", - radius=in_radius, - catalog="Gaia") + result = Catalogs.query_region("158.47924 -7.30962", + radius=in_radius, + catalog="Gaia") row = np.where(result['source_id'] == '3774902350511581696') - assert isinstance(result, Table) - assert result[row]['solution_id'] == '1635721458409799680' - - result = mast.Catalogs.query_region("322.49324 12.16683", - radius=0.01*u.deg, - catalog="HSC", - magtype=2) - row = np.where(result['MatchID'] == '78095437') - - with pytest.warns(MaxResultsWarning): - result = mast.Catalogs.query_region("322.49324 12.16683", catalog="HSC", magtype=2) - - assert isinstance(result, Table) - assert result[row]['NumImages'] == 1 - assert result[row]['TargetName'] == 'M15' + check_result(result, row, {'solution_id': '1635721458409799680'}) - result = mast.Catalogs.query_region("322.49324 12.16683", - radius=0.01*u.deg, - catalog="HSC", - version=2, - magtype=2) - row = np.where(result['MatchID'] == '82368728') + result = Catalogs.query_region("322.49324 12.16683", + radius=0.001*u.deg, + catalog="HSC", + magtype=2) + row = np.where(result['MatchID'] == '8150896') with pytest.warns(MaxResultsWarning): - result = mast.Catalogs.query_region("322.49324 12.16683", catalog="HSC", - version=2, magtype=2) - assert isinstance(result, Table) - assert result[row]['NumImages'] == 11 - assert result[row]['TargetName'] == 'NGC7078' - - result = mast.Catalogs.query_region("322.49324 12.16683", - radius=in_radius, - catalog="Gaia", - version=1) + result = Catalogs.query_region("322.49324 12.16683", catalog="HSC", magtype=2, nr=5) + + check_result(result, row, {'NumImages': 14, 'TargetName': 'M15'}) + + result = Catalogs.query_region("322.49324 12.16683", + radius=0.001*u.deg, + catalog="HSC", + version=2, + magtype=2) + row = np.where(result['MatchID'] == '82361658') + check_result(result, row, {'NumImages': 11, 'TargetName': 'NGC7078'}) + + result = Catalogs.query_region("322.49324 12.16683", + radius=in_radius, + catalog="Gaia", + version=1) row = np.where(result['source_id'] == '1745948323734098688') - assert isinstance(result, Table) - assert result[row]['solution_id'] == '1635378410781933568' - - result = mast.Catalogs.query_region("322.49324 12.16683", - radius=in_radius, - catalog="Gaia", - version=2) - - row = np.where(result['source_id'] == '1745973204477191424') - assert isinstance(result, Table) - assert result[row]['solution_id'] == '1635721458409799680' - - result = mast.Catalogs.query_region("322.49324 12.16683", - radius=in_radius, catalog="panstarrs", - table="mean") + check_result(result, row, {'solution_id': '1635378410781933568'}) + result = Catalogs.query_region("322.49324 12.16683", + radius=0.01*u.deg, + catalog="Gaia", + version=2) + + row = np.where(result['source_id'] == '1745947739618544000') + check_result(result, row, {'solution_id': '1635721458409799680'}) + + result = Catalogs.query_region("322.49324 12.16683", + radius=0.01*u.deg, catalog="panstarrs", + table="mean") row = np.where((result['objName'] == 'PSO J322.4622+12.1920') & (result['yFlags'] == 16777496)) assert isinstance(result, Table) np.testing.assert_allclose(result[row]['distance'], 0.039381703406789904) - result = mast.Catalogs.query_region("322.49324 12.16683", - radius=in_radius, catalog="panstarrs", - table="mean", - pagesize=3) + result = Catalogs.query_region("322.49324 12.16683", + radius=0.01*u.deg, catalog="panstarrs", + table="mean", + page_size=3) assert isinstance(result, Table) assert len(result) == 3 - result = mast.Catalogs.query_region("158.47924 -7.30962", - radius=in_radius, - catalog="Galex") + result = Catalogs.query_region("158.47924 -7.30962", + radius=in_radius, + catalog="Galex") in_radius_arcmin = 0.1*u.deg.to(u.arcmin) distances = list(result['distance_arcmin']) assert isinstance(result, Table) assert max(distances) <= in_radius_arcmin - result = mast.Catalogs.query_region("158.47924 -7.30962", - radius=in_radius, - catalog="tic") + result = Catalogs.query_region("158.47924 -7.30962", + radius=in_radius, + catalog="tic") row = np.where(result['ID'] == '841736289') - assert isinstance(result, Table) + check_result(result, row, {'gaiaqflag': 1}) np.testing.assert_allclose(result[row]['RA_orig'], 158.475246786483) - assert result[row]['gaiaqflag'] == 1 - result = mast.Catalogs.query_region("158.47924 -7.30962", - radius=in_radius, - catalog="ctl") + result = Catalogs.query_region("158.47924 -7.30962", + radius=in_radius, + catalog="ctl") row = np.where(result['ID'] == '56662064') - assert isinstance(result, Table) - assert result[row]['TYC'] == '4918-01335-1' + check_result(result, row, {'TYC': '4918-01335-1'}) - result = mast.Catalogs.query_region("210.80227 54.34895", - radius=1*u.deg, - catalog="diskdetective") + result = Catalogs.query_region("210.80227 54.34895", + radius=1*u.deg, + catalog="diskdetective") row = np.where(result['designation'] == 'J140544.95+535941.1') - assert isinstance(result, Table) - assert result[row]['ZooniverseID'] == 'AWI0000r57' + check_result(result, row, {'ZooniverseID': 'AWI0000r57'}) def test_catalogs_query_object_async(self): - responses = mast.Catalogs.query_object_async("M10", - radius=.02, - catalog="TIC") + responses = Catalogs.query_object_async("M10", + radius=.02, + catalog="TIC") assert isinstance(responses, list) def test_catalogs_query_object(self): + def check_result(result, exp_values): + assert isinstance(result, Table) + for k, v in exp_values.items(): + assert v in result[k] # clear columns config - mast.Catalogs._column_configs = dict() - - result = mast.Catalogs.query_object("M10", - radius=".02 deg", - catalog="TIC") - assert isinstance(result, Table) - assert '189844449' in result['ID'] - - result = mast.Catalogs.query_object("M10", - radius=.001, - catalog="HSC", - magtype=1) - assert isinstance(result, Table) - assert '60112519' in result['MatchID'] - - result = mast.Catalogs.query_object("M10", - radius=.001, - catalog="panstarrs", - table="mean") - assert isinstance(result, Table) - assert 'PSO J254.2872-04.0991' in result['objName'] - - result = mast.Catalogs.query_object("M101", - radius=1, - catalog="diskdetective") - assert isinstance(result, Table) - assert 'J140758.82+534902.4' in result['designation'] - - result = mast.Catalogs.query_object("M10", - radius=0.01, - catalog="Gaia", - version=1) + Catalogs._column_configs = dict() + + result = Catalogs.query_object("M10", + radius=.001, + catalog="TIC") + check_result(result, {'ID': '1305764225'}) + + result = Catalogs.query_object("M10", + radius=.001, + catalog="HSC", + magtype=1) + check_result(result, {'MatchID': '667727'}) + + result = Catalogs.query_object("M10", + radius=.001, + catalog="panstarrs", + table="mean") + check_result(result, {'objName': 'PSO J254.2873-04.1006'}) + + result = Catalogs.query_object("M10", + radius=0.18, + catalog="diskdetective") + check_result(result, {'designation': 'J165749.79-040315.1'}) + + result = Catalogs.query_object("M10", + radius=0.001, + catalog="Gaia", + version=1) distances = list(result['distance']) radius_arcmin = 0.01 * u.deg.to(u.arcmin) assert isinstance(result, Table) assert max(distances) < radius_arcmin - result = mast.Catalogs.query_object("TIC 441662144", - radius=0.01, - catalog="ctl") - assert isinstance(result, Table) - assert '441662144' in result['ID'] + result = Catalogs.query_object("TIC 441662144", + radius=0.001, + catalog="ctl") + check_result(result, {'ID': '441662144'}) - result = mast.Catalogs.query_object('M1', - radius=0.2, - catalog='plato') + result = Catalogs.query_object('M10', + radius=0.08, + catalog='plato') assert 'PICidDR1' in result.colnames def test_catalogs_query_criteria_async(self): # without position - responses = mast.Catalogs.query_criteria_async(catalog="Tic", - Bmag=[30, 50], - objType="STAR") + responses = Catalogs.query_criteria_async(catalog="Tic", + Bmag=[30, 50], + objType="STAR") assert isinstance(responses, list) - responses = mast.Catalogs.query_criteria_async(catalog="ctl", - Bmag=[30, 50], - objType="STAR") + responses = Catalogs.query_criteria_async(catalog="ctl", + Bmag=[30, 50], + objType="STAR") assert isinstance(responses, list) - responses = mast.Catalogs.query_criteria_async(catalog="DiskDetective", - state=["inactive", "disabled"], - oval=[8, 10], - multi=[3, 7]) + responses = Catalogs.query_criteria_async(catalog="DiskDetective", + state=["inactive", "disabled"], + oval=[8, 10], + multi=[3, 7]) assert isinstance(responses, list) # with position - responses = mast.Catalogs.query_criteria_async(catalog="Tic", - objectname="M10", - objType="EXTENDED") + responses = Catalogs.query_criteria_async(catalog="Tic", + objectname="M10", + objType="EXTENDED") assert isinstance(responses, list) - responses = mast.Catalogs.query_criteria_async(catalog="CTL", - objectname="M10", - objType="EXTENDED") + responses = Catalogs.query_criteria_async(catalog="CTL", + objectname="M10", + objType="EXTENDED") assert isinstance(responses, list) - responses = mast.Catalogs.query_criteria_async(catalog="DiskDetective", - objectname="M10", - radius=2, - state="complete") + responses = Catalogs.query_criteria_async(catalog="DiskDetective", + objectname="M10", + radius=2, + state="complete") assert isinstance(responses, list) - responses = mast.Catalogs.query_criteria_async(catalog="panstarrs", - table="mean", - objectname="M10", - radius=.02, - qualityFlag=48) + responses = Catalogs.query_criteria_async(catalog="panstarrs", + table="mean", + objectname="M10", + radius=.02, + qualityFlag=48) assert isinstance(responses, Response) def test_catalogs_query_criteria(self): + def check_result(result, exp_vals): + assert isinstance(result, Table) + for k, v in exp_vals.items(): + assert v in result[k] # clear columns config - mast.Catalogs._column_configs = dict() + Catalogs._column_configs = dict() # without position - result = mast.Catalogs.query_criteria(catalog="Tic", - Bmag=[30, 50], - objType="STAR") - assert isinstance(result, Table) - assert '81609218' in result['ID'] - - result = mast.Catalogs.query_criteria(catalog="ctl", - Tmag=[10.5, 11], - POSflag="2mass") - assert isinstance(result, Table) - assert '291067184' in result['ID'] - - result = mast.Catalogs.query_criteria(catalog="DiskDetective", - state=["inactive", "disabled"], - oval=[8, 10], - multi=[3, 7]) - assert isinstance(result, Table) - assert 'J003920.04-300132.4' in result['designation'] + result = Catalogs.query_criteria(catalog="Tic", + Bmag=[30, 50], + objType="STAR") + check_result(result, {'ID': '81609218'}) + + result = Catalogs.query_criteria(catalog="ctl", + Tmag=[10.5, 11], + POSflag="2mass") + check_result(result, {'ID': '291067184'}) + + result = Catalogs.query_criteria(catalog="DiskDetective", + state=["inactive", "disabled"], + oval=[8, 10], + multi=[3, 7]) + check_result(result, {'designation': 'J003920.04-300132.4'}) # with position - result = mast.Catalogs.query_criteria(catalog="Tic", - objectname="M10", objType="EXTENDED") - assert isinstance(result, Table) - assert '10000732589' in result['ID'] - - result = mast.Catalogs.query_criteria(objectname='TIC 291067184', - catalog="ctl", - Tmag=[10.5, 11], - POSflag="2mass") - assert isinstance(result, Table) - assert 10.893 in result['Tmag'] - - result = mast.Catalogs.query_criteria(catalog="DiskDetective", - objectname="M10", - radius=2, - state="complete") - assert isinstance(result, Table) - assert 'J165628.40-054630.8' in result['designation'] - - result = mast.Catalogs.query_criteria(catalog="panstarrs", - objectname="M10", - radius=.01, - qualityFlag=32, - zoneID=10306) - assert isinstance(result, Table) - assert 'PSO J254.2861-04.1091' in result['objName'] - - result = mast.Catalogs.query_criteria(coordinates="158.47924 -7.30962", - radius=0.01, - catalog="PANSTARRS", - table="mean", - data_release="dr2", - nStackDetections=[("gte", "1")], - columns=["objName", "distance"], - sort_by=[("asc", "distance")]) + result = Catalogs.query_criteria(catalog="Tic", + objectname="M10", objType="EXTENDED") + check_result(result, {'ID': '10000732589'}) + + result = Catalogs.query_criteria(objectname='TIC 291067184', + catalog="ctl", + Tmag=[10.5, 11], + POSflag="2mass") + check_result(result, {'Tmag': 10.893}) + + result = Catalogs.query_criteria(catalog="DiskDetective", + objectname="M10", + radius=2, + state="complete") + check_result(result, {'designation': 'J165628.40-054630.8'}) + + result = Catalogs.query_criteria(catalog="panstarrs", + objectname="M10", + radius=.01, + qualityFlag=32, + zoneID=10306) + check_result(result, {'objName': 'PSO J254.2861-04.1091'}) + + result = Catalogs.query_criteria(coordinates="158.47924 -7.30962", + radius=0.01, + catalog="PANSTARRS", + table="mean", + data_release="dr2", + nStackDetections=[("gte", "1")], + columns=["objName", "distance"], + sort_by=[("asc", "distance")]) assert isinstance(result, Table) assert result['distance'][0] <= result['distance'][1] def test_catalogs_query_hsc_matchid_async(self): - catalogData = mast.Catalogs.query_object("M10", - radius=.001, - catalog="HSC", - magtype=1) + catalogData = Catalogs.query_object("M10", + radius=.001, + catalog="HSC", + magtype=1) - responses = mast.Catalogs.query_hsc_matchid_async(catalogData[0]) + responses = Catalogs.query_hsc_matchid_async(catalogData[0]) assert isinstance(responses, list) - responses = mast.Catalogs.query_hsc_matchid_async(catalogData[0]["MatchID"]) + responses = Catalogs.query_hsc_matchid_async(catalogData[0]["MatchID"]) assert isinstance(responses, list) def test_catalogs_query_hsc_matchid(self): # clear columns config - mast.Catalogs._column_configs = dict() + Catalogs._column_configs = dict() - catalogData = mast.Catalogs.query_object("M10", - radius=.001, - catalog="HSC", - magtype=1) + catalogData = Catalogs.query_object("M10", + radius=.001, + catalog="HSC", + magtype=1) matchid = catalogData[0]["MatchID"] - result = mast.Catalogs.query_hsc_matchid(catalogData[0]) + result = Catalogs.query_hsc_matchid(catalogData[0]) assert isinstance(result, Table) assert (result['MatchID'] == matchid).all() - result2 = mast.Catalogs.query_hsc_matchid(matchid) + result2 = Catalogs.query_hsc_matchid(matchid) assert isinstance(result2, Table) assert len(result2) == len(result) assert (result2['MatchID'] == matchid).all() def test_catalogs_get_hsc_spectra_async(self): - responses = mast.Catalogs.get_hsc_spectra_async() + responses = Catalogs.get_hsc_spectra_async() assert isinstance(responses, list) def test_catalogs_get_hsc_spectra(self): # clear columns config - mast.Catalogs._column_configs = dict() + Catalogs._column_configs = dict() - result = mast.Catalogs.get_hsc_spectra() + result = Catalogs.get_hsc_spectra() assert isinstance(result, Table) assert result[np.where(result['MatchID'] == '19657846')] assert result[np.where(result['DatasetName'] == 'HAG_J072657.06+691415.5_J8HPAXAEQ_V01.SPEC1D')] def test_catalogs_download_hsc_spectra(self, tmpdir): - allSpectra = mast.Catalogs.get_hsc_spectra() + allSpectra = Catalogs.get_hsc_spectra() # actually download the products - result = mast.Catalogs.download_hsc_spectra(allSpectra[10], - download_dir=str(tmpdir)) + result = Catalogs.download_hsc_spectra(allSpectra[10], + download_dir=str(tmpdir)) assert isinstance(result, Table) for row in result: @@ -796,8 +787,8 @@ def test_catalogs_download_hsc_spectra(self, tmpdir): assert os.path.isfile(row['Local Path']) # just get the curl script - result = mast.Catalogs.download_hsc_spectra(allSpectra[20:24], - download_dir=str(tmpdir), curl_flag=True) + result = Catalogs.download_hsc_spectra(allSpectra[20:24], + download_dir=str(tmpdir), curl_flag=True) assert isinstance(result, Table) assert os.path.isfile(result['Local Path'][0]) @@ -807,29 +798,21 @@ def test_catalogs_download_hsc_spectra(self, tmpdir): @pytest.mark.parametrize("product", ["tica", "spoc"]) def test_tesscut_get_sectors(self, product): + def check_sector_table(sector_table): + assert isinstance(sector_table, Table) + assert len(sector_table) >= 1 + assert f"{name}-s00" in sector_table['sectorName'][0] + assert sector_table['sector'][0] > 0 + assert sector_table['camera'][0] > 0 + assert sector_table['ccd'][0] > 0 coord = SkyCoord(349.62609, -47.12424, unit="deg") - sector_table = mast.Tesscut.get_sectors(coordinates=coord, product=product) - assert isinstance(sector_table, Table) - assert len(sector_table) >= 1 - - name = "tess" if product == "spoc" else product - assert f"{name}-s00" in sector_table['sectorName'][0] - - assert sector_table['sector'][0] > 0 - assert sector_table['camera'][0] > 0 - assert sector_table['ccd'][0] > 0 - - sector_table = mast.Tesscut.get_sectors(objectname="M104", product=product) - assert isinstance(sector_table, Table) - assert len(sector_table) >= 1 - name = "tess" if product == "spoc" else product - assert f"{name}-s00" in sector_table['sectorName'][0] + sector_table = Tesscut.get_sectors(coordinates=coord, product=product) + check_sector_table(sector_table) - assert sector_table['sector'][0] > 0 - assert sector_table['camera'][0] > 0 - assert sector_table['ccd'][0] > 0 + sector_table = Tesscut.get_sectors(objectname="M104", product=product) + check_sector_table(sector_table) def test_tesscut_get_sectors_mt(self): @@ -838,8 +821,8 @@ def test_tesscut_get_sectors_mt(self): coord = SkyCoord(349.62609, -47.12424, unit="deg") moving_target_name = 'Eleonora' - sector_table = mast.Tesscut.get_sectors(objectname=moving_target_name, - moving_target=True) + sector_table = Tesscut.get_sectors(objectname=moving_target_name, + moving_target=True) assert isinstance(sector_table, Table) assert len(sector_table) >= 1 assert sector_table['sectorName'][0] == "tess-s0006-1-1" @@ -856,77 +839,62 @@ def test_tesscut_get_sectors_mt(self): error_tica_mt = "Only SPOC is available for moving targets queries." with pytest.raises(InvalidQueryError) as error_msg: - mast.Tesscut.get_sectors(moving_target=True) + Tesscut.get_sectors(moving_target=True) assert error_noname in str(error_msg.value) with pytest.raises(ResolverError) as error_msg: - mast.Tesscut.get_sectors(objectname=moving_target_name) + Tesscut.get_sectors(objectname=moving_target_name) assert error_nameresolve in str(error_msg.value) with pytest.raises(InvalidQueryError) as error_msg: - mast.Tesscut.get_sectors(coordinates=coord, moving_target=True) + Tesscut.get_sectors(coordinates=coord, moving_target=True) assert error_mt_coord in str(error_msg.value) with pytest.raises(InvalidQueryError) as error_msg: - mast.Tesscut.get_sectors(objectname=moving_target_name, - coordinates=coord) + Tesscut.get_sectors(objectname=moving_target_name, coordinates=coord) assert error_name_coord in str(error_msg.value) with pytest.raises(InvalidQueryError) as error_msg: - mast.Tesscut.get_sectors(objectname=moving_target_name, - coordinates=coord, - moving_target=True) + Tesscut.get_sectors(objectname=moving_target_name, + coordinates=coord, + moving_target=True) assert error_mt_coord in str(error_msg.value) # The TICA product option is not available for moving targets - with pytest.raises(InvalidQueryError) as error_msg: - sector_table = mast.Tesscut.get_sectors(objectname=moving_target_name, product='tica', - moving_target=True) + sector_table = Tesscut.get_sectors(objectname=moving_target_name, product='tica', + moving_target=True) assert error_tica_mt in str(error_msg.value) @pytest.mark.parametrize("product", ["tica", "spoc"]) def test_tesscut_download_cutouts(self, tmpdir, product): - coord = SkyCoord(349.62609, -47.12424, unit="deg") + def check_manifest(manifest, ext="fits"): + assert isinstance(manifest, Table) + assert len(manifest) >= 1 + assert manifest["Local Path"][0][-4:] == ext + for row in manifest: + assert os.path.isfile(row['Local Path']) - manifest = mast.Tesscut.download_cutouts(product=product, coordinates=coord, size=5, path=str(tmpdir)) - assert isinstance(manifest, Table) - assert len(manifest) >= 1 - assert manifest["Local Path"][0][-4:] == "fits" - for row in manifest: - assert os.path.isfile(row['Local Path']) + coord = SkyCoord(349.62609, -47.12424, unit="deg") + manifest = Tesscut.download_cutouts(product=product, coordinates=coord, size=1, path=str(tmpdir)) + check_manifest(manifest) coord = SkyCoord(107.18696, -70.50919, unit="deg") + manifest = Tesscut.download_cutouts(product=product, coordinates=coord, size=1, sector=27, + path=str(tmpdir)) + check_manifest(manifest) - manifest = mast.Tesscut.download_cutouts(product=product, coordinates=coord, size=5, sector=27, - path=str(tmpdir)) - assert isinstance(manifest, Table) - assert len(manifest) == 1 - assert manifest["Local Path"][0][-4:] == "fits" - assert os.path.isfile(manifest[0]['Local Path']) - - manifest = mast.Tesscut.download_cutouts(product=product, coordinates=coord, size=[5, 7]*u.pix, sector=33, - path=str(tmpdir)) - assert isinstance(manifest, Table) - assert len(manifest) >= 1 - assert manifest["Local Path"][0][-4:] == "fits" - for row in manifest: - assert os.path.isfile(row['Local Path']) + manifest = Tesscut.download_cutouts(product=product, coordinates=coord, size=[1, 1]*u.pix, sector=33, + path=str(tmpdir)) + check_manifest(manifest) - manifest = mast.Tesscut.download_cutouts(product=product, coordinates=coord, size=5, sector=33, - path=str(tmpdir), inflate=False) - assert isinstance(manifest, Table) - assert len(manifest) == 1 - assert manifest["Local Path"][0][-3:] == "zip" - assert os.path.isfile(manifest[0]['Local Path']) + manifest = Tesscut.download_cutouts(product=product, coordinates=coord, size=1, sector=33, + path=str(tmpdir), inflate=False) + check_manifest(manifest, ".zip") - manifest = mast.Tesscut.download_cutouts(product=product, objectname="TIC 32449963", size=5, path=str(tmpdir)) - assert isinstance(manifest, Table) - assert len(manifest) >= 1 - assert manifest["Local Path"][0][-4:] == "fits" - for row in manifest: - assert os.path.isfile(row['Local Path']) + manifest = Tesscut.download_cutouts(product=product, objectname="TIC 32449963", size=1, path=str(tmpdir)) + check_manifest(manifest, "fits") def test_tesscut_download_cutouts_mt(self, tmpdir): @@ -934,11 +902,11 @@ def test_tesscut_download_cutouts_mt(self, tmpdir): coord = SkyCoord(349.62609, -47.12424, unit="deg") moving_target_name = 'Eleonora' - manifest = mast.Tesscut.download_cutouts(objectname=moving_target_name, - moving_target=True, - sector=6, - size=5, - path=str(tmpdir)) + manifest = Tesscut.download_cutouts(objectname=moving_target_name, + moving_target=True, + sector=6, + size=1, + path=str(tmpdir)) assert isinstance(manifest, Table) assert len(manifest) == 1 assert manifest["Local Path"][0][-4:] == "fits" @@ -954,61 +922,63 @@ def test_tesscut_download_cutouts_mt(self, tmpdir): error_tica_mt = "Only SPOC is available for moving targets queries." with pytest.raises(InvalidQueryError) as error_msg: - mast.Tesscut.download_cutouts(moving_target=True) + Tesscut.download_cutouts(moving_target=True) assert error_noname in str(error_msg.value) with pytest.raises(ResolverError) as error_msg: - mast.Tesscut.download_cutouts(objectname=moving_target_name) + Tesscut.download_cutouts(objectname=moving_target_name) assert error_nameresolve in str(error_msg.value) with pytest.raises(InvalidQueryError) as error_msg: - mast.Tesscut.download_cutouts(coordinates=coord, moving_target=True) + Tesscut.download_cutouts(coordinates=coord, moving_target=True) assert error_mt_coord in str(error_msg.value) with pytest.raises(InvalidQueryError) as error_msg: - mast.Tesscut.download_cutouts(objectname=moving_target_name, - coordinates=coord) + Tesscut.download_cutouts(objectname=moving_target_name, coordinates=coord) assert error_name_coord in str(error_msg.value) with pytest.raises(InvalidQueryError) as error_msg: - mast.Tesscut.download_cutouts(objectname=moving_target_name, - coordinates=coord, - moving_target=True) + Tesscut.download_cutouts(objectname=moving_target_name, + coordinates=coord, + moving_target=True) assert error_mt_coord in str(error_msg.value) # The TICA product option is not available for moving targets with pytest.raises(InvalidQueryError) as error_msg: - mast.Tesscut.download_cutouts(objectname=moving_target_name, product='tica', - moving_target=True) + Tesscut.download_cutouts(objectname=moving_target_name, product='tica', + moving_target=True) assert error_tica_mt in str(error_msg.value) @pytest.mark.parametrize("product", ["tica", "spoc"]) def test_tesscut_get_cutouts(self, product): - coord = SkyCoord(107.18696, -70.50919, unit="deg") + def check_cutout_hdu(cutout_hdus_list): + assert isinstance(cutout_hdus_list, list) + assert len(cutout_hdus_list) >= 1 + assert isinstance(cutout_hdus_list[0], fits.HDUList) - cutout_hdus_list = mast.Tesscut.get_cutouts(product=product, coordinates=coord, size=5, sector=33) - assert isinstance(cutout_hdus_list, list) - assert len(cutout_hdus_list) >= 1 - assert isinstance(cutout_hdus_list[0], fits.HDUList) + coord = SkyCoord(107.18696, -70.50919, unit="deg") - cutout_hdus_list = mast.Tesscut.get_cutouts(product=product, coordinates=coord, size=5, sector=27) - assert isinstance(cutout_hdus_list, list) - assert len(cutout_hdus_list) == 1 - assert isinstance(cutout_hdus_list[0], fits.HDUList) + cutout_hdus_list = Tesscut.get_cutouts(product=product, + coordinates=coord, + size=1, + sector=33) + check_cutout_hdu(cutout_hdus_list) coord = SkyCoord(349.62609, -47.12424, unit="deg") - cutout_hdus_list = mast.Tesscut.get_cutouts(product=product, coordinates=coord, size=[2, 4]*u.arcmin) - assert isinstance(cutout_hdus_list, list) - assert len(cutout_hdus_list) >= 1 - assert isinstance(cutout_hdus_list[0], fits.HDUList) + cutout_hdus_list = Tesscut.get_cutouts(product=product, + coordinates=coord, + size=[1, 1]*u.arcmin, + sector=[28, 68]) + check_cutout_hdu(cutout_hdus_list) - cutout_hdus_list = mast.Tesscut.get_cutouts(product=product, objectname="TIC 32449963", size=5) - assert isinstance(cutout_hdus_list, list) - assert len(cutout_hdus_list) >= 1 - assert isinstance(cutout_hdus_list[0], fits.HDUList) + cutout_hdus_list = Tesscut.get_cutouts(product=product, + objectname="TIC 32449963", + size=1, + sector=37) + check_cutout_hdu(cutout_hdus_list) def test_tesscut_get_cutouts_mt(self): @@ -1016,10 +986,10 @@ def test_tesscut_get_cutouts_mt(self): coord = SkyCoord(349.62609, -47.12424, unit="deg") moving_target_name = 'Eleonora' - cutout_hdus_list = mast.Tesscut.get_cutouts(objectname=moving_target_name, - moving_target=True, - sector=6, - size=5) + cutout_hdus_list = Tesscut.get_cutouts(objectname=moving_target_name, + moving_target=True, + sector=6, + size=1) assert isinstance(cutout_hdus_list, list) assert len(cutout_hdus_list) == 1 assert isinstance(cutout_hdus_list[0], fits.HDUList) @@ -1033,216 +1003,183 @@ def test_tesscut_get_cutouts_mt(self): error_tica_mt = "Only SPOC is available for moving targets queries." with pytest.raises(InvalidQueryError) as error_msg: - mast.Tesscut.get_cutouts(moving_target=True) + Tesscut.get_cutouts(moving_target=True) assert error_noname in str(error_msg.value) with pytest.raises(ResolverError) as error_msg: - mast.Tesscut.get_cutouts(objectname=moving_target_name) + Tesscut.get_cutouts(objectname=moving_target_name) assert error_nameresolve in str(error_msg.value) with pytest.raises(InvalidQueryError) as error_msg: - mast.Tesscut.get_cutouts(coordinates=coord, moving_target=True) + Tesscut.get_cutouts(coordinates=coord, moving_target=True) assert error_mt_coord in str(error_msg.value) with pytest.raises(InvalidQueryError) as error_msg: - mast.Tesscut.get_cutouts(objectname=moving_target_name, - coordinates=coord) + Tesscut.get_cutouts(objectname=moving_target_name, + coordinates=coord) assert error_name_coord in str(error_msg.value) with pytest.raises(InvalidQueryError) as error_msg: - mast.Tesscut.get_cutouts(objectname=moving_target_name, - coordinates=coord, - moving_target=True) + Tesscut.get_cutouts(objectname=moving_target_name, + coordinates=coord, + moving_target=True) assert error_mt_coord in str(error_msg.value) # The TICA product option is not available for moving targets with pytest.raises(InvalidQueryError) as error_msg: - mast.Tesscut.get_cutouts(objectname=moving_target_name, product='tica', - moving_target=True) + Tesscut.get_cutouts(objectname=moving_target_name, product='tica', + moving_target=True) assert error_tica_mt in str(error_msg.value) ################### # ZcutClass tests # ################### def test_zcut_get_surveys(self): + def check_survey_list(survery_list, no_results=True): + assert isinstance(survery_list, list) + assert len(survery_list) == 0 if no_results else len(survery_list) >= 1 coord = SkyCoord(189.49206, 62.20615, unit="deg") - survey_list = mast.Zcut.get_surveys(coordinates=coord) - assert isinstance(survey_list, list) - assert len(survey_list) >= 1 + survey_list = Zcut.get_surveys(coordinates=coord) + check_survey_list(survey_list, False) assert survey_list[0] == 'candels_gn_60mas' assert survey_list[1] == 'candels_gn_30mas' assert survey_list[2] == 'goods_north' # This should always return no results + coord = SkyCoord(57.10523, -30.08085, unit="deg") with pytest.warns(NoResultsWarning): - coord = SkyCoord(57.10523, -30.08085, unit="deg") - survey_list = mast.Zcut.get_surveys(coordinates=coord, radius=0) - assert isinstance(survey_list, list) - assert len(survey_list) == 0 + survey_list = Zcut.get_surveys(coordinates=coord, radius=0) + check_survey_list(survey_list) coord = SkyCoord(57.10523, -30.08085, unit="deg") with pytest.warns(NoResultsWarning): - survey_list = mast.Zcut.get_surveys(coordinates=coord, radius=0) - assert isinstance(survey_list, list) - assert len(survey_list) == 0 + survey_list = Zcut.get_surveys(coordinates=coord, radius=0) + check_survey_list(survey_list) def test_zcut_download_cutouts(self, tmpdir): - coord = SkyCoord(34.47320, -5.24271, unit="deg") - - cutout_table = mast.Zcut.download_cutouts(coordinates=coord, size=5, path=str(tmpdir)) - assert isinstance(cutout_table, Table) - assert len(cutout_table) >= 1 - assert cutout_table["Local Path"][0][-4:] == "fits" - for row in cutout_table: + def check_cutout(cutout_table, ext): + assert isinstance(cutout_table, Table) + assert len(cutout_table) >= 1 + assert cutout_table["Local Path"][0][-4:] == ext assert os.path.isfile(cutout_table[0]['Local Path']) - coord = SkyCoord(189.28065571, 62.17415175, unit="deg") - - cutout_table = mast.Zcut.download_cutouts(coordinates=coord, size=[200, 300], path=str(tmpdir)) - assert isinstance(cutout_table, Table) - assert len(cutout_table) >= 1 - assert cutout_table["Local Path"][0][-4:] == "fits" - for row in cutout_table: - assert os.path.isfile(cutout_table[0]['Local Path']) + coord = SkyCoord(34.47320, -5.24271, unit="deg") + cutout_table = Zcut.download_cutouts(coordinates=coord, size=1, path=str(tmpdir)) + check_cutout(cutout_table, "fits") - cutout_table = mast.Zcut.download_cutouts(coordinates=coord, size=5, cutout_format="jpg", path=str(tmpdir)) - assert isinstance(cutout_table, Table) - assert len(cutout_table) >= 1 - assert cutout_table["Local Path"][0][-4:] == ".jpg" - for row in cutout_table: - assert os.path.isfile(cutout_table[0]['Local Path']) + coord = SkyCoord(189.28065571, 62.17415175, unit="deg") + cutout_table = Zcut.download_cutouts(coordinates=coord, size=[1, 1], cutout_format="jpg", path=str(tmpdir)) + check_cutout(cutout_table, ".jpg") - cutout_table = mast.Zcut.download_cutouts( - coordinates=coord, size=5, units='5*u.arcsec', cutout_format="png", path=str(tmpdir)) - assert isinstance(cutout_table, Table) - assert len(cutout_table) >= 1 - assert cutout_table["Local Path"][0][-4:] == ".png" - for row in cutout_table: - assert os.path.isfile(cutout_table[0]['Local Path']) + cutout_table = Zcut.download_cutouts( + coordinates=coord, size=1, units='1*u.arcsec', cutout_format="png", path=str(tmpdir)) + check_cutout(cutout_table, ".png") - # Intetionally returns no results + # Intentionally returns no results with pytest.warns(NoResultsWarning): - cutout_table = mast.Zcut.download_cutouts(coordinates=coord, - survey='candels_gn_30mas', - cutout_format="jpg", - path=str(tmpdir)) + cutout_table = Zcut.download_cutouts(coordinates=coord, + survey='candels_gn_30mas', + cutout_format="jpg", + path=str(tmpdir)) assert isinstance(cutout_table, Table) assert len(cutout_table) == 0 - cutout_table = mast.Zcut.download_cutouts( - coordinates=coord, survey='goods_north', cutout_format="jpg", path=str(tmpdir)) - assert isinstance(cutout_table, Table) - assert len(cutout_table) == 4 - assert cutout_table["Local Path"][0][-4:] == ".jpg" - for row in cutout_table: - assert os.path.isfile(cutout_table[0]['Local Path']) + cutout_table = Zcut.download_cutouts( + coordinates=coord, size=1, survey='goods_north', cutout_format="jpg", path=str(tmpdir)) + check_cutout(cutout_table, ".jpg") - cutout_table = mast.Zcut.download_cutouts( - coordinates=coord, cutout_format="jpg", path=str(tmpdir), stretch='asinh', invert=True) - assert isinstance(cutout_table, Table) - assert len(cutout_table) >= 1 - assert cutout_table["Local Path"][0][-4:] == ".jpg" - for row in cutout_table: - assert os.path.isfile(cutout_table[0]['Local Path']) + cutout_table = Zcut.download_cutouts( + coordinates=coord, size=1, cutout_format="jpg", path=str(tmpdir), stretch='asinh', invert=True) + check_cutout(cutout_table, ".jpg") def test_zcut_get_cutouts(self): + def check_cutout_list(cutout_list, multi=True): + assert isinstance(cutout_list, list) + assert isinstance(cutout_list[0], fits.HDUList) + assert len(cutout_list) >= 1 if multi else len(cutout_list) == 1 coord = SkyCoord(189.28065571, 62.17415175, unit="deg") - cutout_list = mast.Zcut.get_cutouts(coordinates=coord) - assert isinstance(cutout_list, list) - assert len(cutout_list) >= 1 - assert isinstance(cutout_list[0], fits.HDUList) + cutout_list = Zcut.get_cutouts(coordinates=coord, size=1) + check_cutout_list(cutout_list) - cutout_list = mast.Zcut.get_cutouts(coordinates=coord, size=[200, 300]) - assert isinstance(cutout_list, list) - assert len(cutout_list) >= 1 - assert isinstance(cutout_list[0], fits.HDUList) + cutout_list = Zcut.get_cutouts(coordinates=coord, size=[1, 1]) + check_cutout_list(cutout_list) # Intentionally returns no results with pytest.warns(NoResultsWarning): - cutout_list = mast.Zcut.get_cutouts(coordinates=coord, - survey='candels_gn_30mas') + cutout_list = Zcut.get_cutouts(coordinates=coord, + survey='candels_gn_30mas') assert isinstance(cutout_list, list) assert len(cutout_list) == 0 - cutout_list = mast.Zcut.get_cutouts(coordinates=coord, survey='3dhst_goods-n') - assert isinstance(cutout_list, list) - assert len(cutout_list) == 1 - assert isinstance(cutout_list[0], fits.HDUList) + cutout_list = Zcut.get_cutouts(coordinates=coord, + survey='3dhst_goods-n', + size=1) + check_cutout_list(cutout_list, multi=False) ################### # HapcutClass tests # ################### def test_hapcut_download_cutouts(self, tmpdir): + def check_cutout_table(cutout_table, check_shape=True, data_shape=None): + assert isinstance(cutout_table, Table) + assert len(cutout_table) >= 1 + for row in cutout_table: + assert os.path.isfile(row['Local Path']) + if check_shape and 'fits' in os.path.basename(row['Local Path']): + assert fits.getdata(row['Local Path']).shape == data_shape # Test 1: Simple API call with expected results coord = SkyCoord(351.347812, 28.497808, unit="deg") - cutout_table = mast.Hapcut.download_cutouts(coordinates=coord, size=5, path=str(tmpdir)) - assert isinstance(cutout_table, Table) - assert len(cutout_table) >= 1 - for row in cutout_table: - assert os.path.isfile(row['Local Path']) - if 'fits' in os.path.basename(row['Local Path']): - assert fits.getdata(row['Local Path']).shape == (5, 5) + cutout_table = Hapcut.download_cutouts(coordinates=coord, size=5, path=str(tmpdir)) + check_cutout_table(cutout_table, data_shape=(5, 5)) # Test 2: Make input size a list - cutout_table = mast.Hapcut.download_cutouts(coordinates=coord, size=[2, 3], path=str(tmpdir)) - assert isinstance(cutout_table, Table) - assert len(cutout_table) >= 1 - for row in cutout_table: - assert os.path.isfile(row['Local Path']) - if 'fits' in os.path.basename(row['Local Path']): - assert fits.getdata(row['Local Path']).shape == (3, 2) + cutout_table = Hapcut.download_cutouts(coordinates=coord, size=[2, 3], path=str(tmpdir)) + check_cutout_table(cutout_table, data_shape=(3, 2)) # Test 3: Specify unit for input size - cutout_table = mast.Hapcut.download_cutouts(coordinates=coord, size=5*u.arcsec, path=str(tmpdir)) - assert isinstance(cutout_table, Table) - assert len(cutout_table) >= 1 - for row in cutout_table: - assert os.path.isfile(row['Local Path']) + cutout_table = Hapcut.download_cutouts(coordinates=coord, size=5*u.arcsec, path=str(tmpdir)) + check_cutout_table(cutout_table, check_shape=False) # Test 4: Intentional API call with no results bad_coord = SkyCoord(102.7, 70.50, unit="deg") with pytest.warns(NoResultsWarning, match='Missing HAP files for input target. Cutout not performed.'): - cutout_table = mast.Hapcut.download_cutouts(coordinates=bad_coord, size=5, path=str(tmpdir)) + cutout_table = Hapcut.download_cutouts(coordinates=bad_coord, size=5, path=str(tmpdir)) assert isinstance(cutout_table, Table) assert len(cutout_table) == 0 def test_hapcut_get_cutouts(self): + def check_cutout_list(cutout_list, data_shape): + assert isinstance(cutout_list, list) + assert len(cutout_list) >= 1 + assert isinstance(cutout_list[0], fits.HDUList) + assert cutout_list[0][1].data.shape == data_shape # Test 1: Simple API call with expected results coord = SkyCoord(351.347812, 28.497808, unit="deg") - cutout_list = mast.Hapcut.get_cutouts(coordinates=coord) - assert isinstance(cutout_list, list) - assert len(cutout_list) >= 1 - assert isinstance(cutout_list[0], fits.HDUList) - assert cutout_list[0][1].data.shape == (5, 5) + cutout_list = Hapcut.get_cutouts(coordinates=coord) + check_cutout_list(cutout_list, (5, 5)) # Test 2: Make input size a list - cutout_list = mast.Hapcut.get_cutouts(coordinates=coord, size=[2, 3]) - assert isinstance(cutout_list, list) - assert len(cutout_list) >= 1 - assert isinstance(cutout_list[0], fits.HDUList) - assert cutout_list[0][1].data.shape == (3, 2) + cutout_list = Hapcut.get_cutouts(coordinates=coord, size=[2, 3]) + check_cutout_list(cutout_list, (3, 2)) # Test 3: Specify unit for input size - cutout_list = mast.Hapcut.get_cutouts(coordinates=coord, size=5*u.arcsec) - assert isinstance(cutout_list, list) - assert len(cutout_list) >= 1 - assert isinstance(cutout_list[0], fits.HDUList) - assert cutout_list[0][1].data.shape == (42, 42) + cutout_list = Hapcut.get_cutouts(coordinates=coord, size=5*u.arcsec) + check_cutout_list(cutout_list, (42, 42)) # Test 4: Intentional API call with no results bad_coord = SkyCoord(102.7, 70.50, unit="deg") with pytest.warns(NoResultsWarning, match='Missing HAP files for input target. Cutout not performed.'): - cutout_list = mast.Hapcut.get_cutouts(coordinates=bad_coord) + cutout_list = Hapcut.get_cutouts(coordinates=bad_coord) assert isinstance(cutout_list, list) assert len(cutout_list) == 0 From ac59149e671516650435b7ca092908f8485e49d4 Mon Sep 17 00:00:00 2001 From: Adrian Damian Date: Thu, 13 Jun 2024 14:38:35 -0700 Subject: [PATCH 12/35] Added support for spectral_resolution in ALMA --- CHANGES.rst | 1 + astroquery/alma/core.py | 15 +++++----- astroquery/alma/tapsql.py | 32 ---------------------- astroquery/alma/tests/data/alma-onerow.txt | 4 +-- astroquery/alma/tests/test_alma.py | 24 ++++++++-------- 5 files changed, 23 insertions(+), 53 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 0818cdbfd6..cedc68e8e8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -12,6 +12,7 @@ alma ^^^^ - Added method to return quantities instead of values and regions footprint in alma [#2855] +- Added support for frequency_resolution in KHz [#3035] mpc ^^^ diff --git a/astroquery/alma/core.py b/astroquery/alma/core.py index 99e3aea501..947d24d53b 100644 --- a/astroquery/alma/core.py +++ b/astroquery/alma/core.py @@ -29,7 +29,7 @@ from ..query import BaseQuery, QueryWithLogin, BaseVOQuery from .tapsql import (_gen_pos_sql, _gen_str_sql, _gen_numeric_sql, _gen_band_list_sql, _gen_datetime_sql, _gen_pol_sql, _gen_pub_sql, - _gen_science_sql, _gen_spec_res_sql, ALMA_DATE_FORMAT) + _gen_science_sql, ALMA_DATE_FORMAT) from . import conf, auth_urls from astroquery.exceptions import CorruptDataWarning @@ -51,7 +51,8 @@ 'gal_latitude': 'Galactic latitude', 'band_list': 'Band', 's_region': 'Footprint', - 'em_resolution': 'Frequency resolution', + 'em_resolution': 'Frequency resolution (m)', + 'spectral_resolution': 'Frequency resolution (Hz)', 'antenna_arrays': 'Array', 'is_mosaic': 'Mosaic', 't_exptime': 'Integration', @@ -111,7 +112,9 @@ 'Frequency (GHz)': ['frequency', 'frequency', _gen_numeric_sql], 'Bandwidth (Hz)': ['bandwidth', 'bandwidth', _gen_numeric_sql], 'Spectral resolution (KHz)': ['spectral_resolution', - 'em_resolution', _gen_spec_res_sql], + 'spectral_resolution', _gen_numeric_sql], + 'Spectral resolution (m)': ['em_resolution', + 'em_resolution', _gen_numeric_sql], 'Band': ['band_list', 'band_list', _gen_band_list_sql] }, 'Time': { @@ -182,11 +185,7 @@ def _gen_sql(payload): # use the value and the second entry in attrib which # is the new name of the column val = payload[constraint] - if constraint == 'em_resolution': - # em_resolution does not require any transformation - attrib_where = _gen_numeric_sql(constraint, val) - else: - attrib_where = attrib[2](attrib[1], val) + attrib_where = attrib[2](attrib[1], val) if attrib_where: if where: where += ' AND ' diff --git a/astroquery/alma/tapsql.py b/astroquery/alma/tapsql.py index a000f0660e..b6357baf12 100644 --- a/astroquery/alma/tapsql.py +++ b/astroquery/alma/tapsql.py @@ -167,38 +167,6 @@ def _gen_datetime_sql(field, value): return result -def _gen_spec_res_sql(field, value): - # This needs special treatment because spectral_resolution in AQ is in - # KHz while corresponding em_resolution is in m - result = '' - for interval in _val_parse(value): - if result: - result += ' OR ' - if isinstance(interval, tuple): - min_val, max_val = interval - if max_val is None: - result += "{}<={}".format( - field, - min_val*u.kHz.to(u.m, equivalencies=u.spectral())) - elif min_val is None: - result += "{}>={}".format( - field, - max_val*u.kHz.to(u.m, equivalencies=u.spectral())) - else: - result += "({1}<={0} AND {0}<={2})".format( - field, - max_val*u.kHz.to(u.m, equivalencies=u.spectral()), - min_val*u.kHz.to(u.m, equivalencies=u.spectral())) - else: - result += "{}={}".format( - field, interval*u.kHz.to(u.m, equivalencies=u.spectral())) - if ' OR ' in result: - # use brackets for multiple ORs - return '(' + result + ')' - else: - return result - - def _gen_pub_sql(field, value): if value is True: return "{}='Public'".format(field) diff --git a/astroquery/alma/tests/data/alma-onerow.txt b/astroquery/alma/tests/data/alma-onerow.txt index b1f928d77d..db539020bf 100644 --- a/astroquery/alma/tests/data/alma-onerow.txt +++ b/astroquery/alma/tests/data/alma-onerow.txt @@ -1,2 +1,2 @@ -obs_publisher_did obs_collection facility_name instrument_name obs_id dataproduct_type calib_level target_name s_ra s_dec s_fov s_region s_resolution t_min t_max t_exptime t_resolution em_min em_max em_res_power pol_states o_ucd access_url access_format proposal_id data_rights gal_longitude gal_latitude band_list em_resolution bandwidth antenna_arrays is_mosaic obs_release_date spatial_resolution frequency_support frequency velocity_resolution obs_creator_name pub_title first_author authors pub_abstract publication_year proposal_abstract schedblock_name proposal_authors sensitivity_10kms cont_sensitivity_bandwidth pwv group_ous_uid member_ous_uid asdm_uid obs_title type scan_intent science_observation spatial_scale_max qa2_passed bib_reference science_keyword scientific_category lastModified -ADS/JAO.ALMA#2017.1.00079.S ALMA JAO ALMA uid://A001/X1295/X35 cube 2 M83 204.24712522989094 -29.868853678162907 0.1650122764060855 "b'Polygon ICRS 204.319476 -29.894283 204.314307 -29.893360 204.174614 -29.893832 204.159606 -29.886181 204.153471 -29.877399 204.157057 -29.856075 204.152987 -29.849020 204.155142 -29.845057 204.159570 -29.842949 204.165241 -29.843807 204.183416 -29.843187 204.297720 -29.843831 204.302372 -29.842863 204.307818 -29.845083 204.310143 -29.850046 204.314283 -29.849063 204.334321 -29.849727 204.337744 -29.852341 204.343021 -29.861027 204.339032 -29.868522 204.342934 -29.888997 204.337052 -29.894104'" 1.5779219110458824 58480.431262 58488.516277 156.52 156.52 0.002624500730920478 0.002668272377703091 58490.401574578646 /XX/YY/ phot.flux.density;phys.polarization http://almascience.org/aq?member_ous_id=uid://A001/X1295/X35 text/html 2017.1.00079.S Public 314.57611565717997 31.970814026685794 3 36592333492997.875 1875000000.0 "A001:DA44 A002:DA51 A003:DV12 A004:DV18 A005:DA58 A006:DV25 A007:DV05 A008:DV07 A010:DV13 A011:DV21 A015:DA64 A016:DV11 A017:DV14 A018:DA54 A019:DA61 A022:DV23 A023:DA42 A024:DA49 A025:DA46 A026:DV19 A027:DV06 A033:DA53 A034:DA50 A035:DA43 A036:DA45 A039:DA56 A040:DA55 A041:DA57 A042:DV08 A043:DA60 A044:DV03 A045:DV01 A047:DV15 A048:DV02 A049:DA47 A050:DA62 A058:DA41 A060:DV17 A062:DV10 A066:DV04 A068:DA65 A069:DV16 A070:DA48 A072:DV20 A073:DA54 A074:DV09 A075:DV18 A076:DA59 A082:DA52 A083:DV22 A085:DA63 A088:DV24" T b'2020-03-03T03:20:58.000' 1.5779219110458824 "[112.35..114.23GHz,1952.95kHz,8mJy/beam@10km/s,360.2uJy/beam@native, XX YY] U [113.81..115.79GHz,31247.16kHz,9.9mJy/beam@10km/s,433.6uJy/beam@native, XX YY] U [114.84..115.31GHz,488.24kHz,8.2mJy/beam@10km/s,739.3uJy/beam@native, XX YY] U [114.95..115.19GHz,122.06kHz,8.2mJy/beam@10km/s,1mJy/beam@native, XX YY]" 114.07366301863082 317.6733782195699 "Koda, Jin" "" "" "" "" "" "We propose a full mapping of the cold molecular gas over the whole disk of M83 in CO(1-0) using ALMA 12m+7m+TP. This closest (d=4.5Mpc) face-on (i<30deg) archetypical barred spiral galaxy closely resembles our own Milky Way, and has been the important showcase for multi-wavelength studies. The proposed ALMA CO(1-0) map will not only detect the smallest clouds at the lowest end of the cloud mass function (10^4Msun), but also reveal the more extended, ambient molecular gas outside GMCs. This full census of the molecular ISM will be correlated with galactic environments and star formation, from the molecule-dominated galactic center to the atom-dominated outskirts, across the bar, spiral arms, and interarm regions. In combination with the ALMA CO(2-1) map (Cycle 4), we will use the CO 2-1/1-0 ratios of all GMCs to diagnose their physical states and variations as a function of the environments. We will also test the paradigm of GMCs as physical entities by measuring the partition between GMCs and the ambient molecular gas. The ALMA CO(1-0) map will become the foundation for future ALMA studies at higher resolution and in other molecular lines." M83_e_03_TM1 "Oka, Tomoharu; Wu, Yu-Ting; Muraoka, Kazuyuki; Madore, Barry; Watanabe, Yoshimasa; Nakanishi, Kouichiro; Sakamoto, Kazushi; Boissier, Samuel; Seibert, Mark; Martin, Sergio; Sawada, Tsuyoshi; Kuno, Nario; Vlahakis, Catherine; Donovan Meyer, Jennifer; Elmegreen, Bruce; Harada, Nanase; Heyer, Mark; Keto, Eric; Hirota, Akihiko; Kobayashi, Masato; Ohyama, Youichi; Scoville, Nick; Ho, Luis; Tosaki, Tomoka; Egusa, Fumi; Onodera, Sachiko; Gil de Paz, Armando;" 8.023585259543626 0.25181849902410236 5.474677085876465 uid://A001/X1295/X34 uid://A001/X1295/X35 uid://A002/Xd74c3f/Xcf20 "Mapping Molecular ISM in the Whole Disk of M83" S TARGET T 21.61875828636442 T "" "Spiral galaxies, Giant Molecular Clouds (GMC) properties" "Local Universe" b'2019-12-11T20:57:17.290' +obs_publisher_did obs_collection facility_name instrument_name obs_id dataproduct_type calib_level target_name s_ra s_dec s_fov s_region s_resolution t_min t_max t_exptime t_resolution em_min em_max em_res_power pol_states o_ucd access_url access_format proposal_id data_rights gal_longitude gal_latitude band_list em_resolution bandwidth antenna_arrays is_mosaic obs_release_date spatial_resolution frequency_support frequency velocity_resolution obs_creator_name pub_title first_author authors pub_abstract publication_year proposal_abstract schedblock_name proposal_authors sensitivity_10kms cont_sensitivity_bandwidth pwv group_ous_uid member_ous_uid asdm_uid obs_title type scan_intent science_observation spatial_scale_max qa2_passed bib_reference science_keyword scientific_category spectral_resolution lastModified +ADS/JAO.ALMA#2017.1.00079.S ALMA JAO ALMA uid://A001/X1295/X35 cube 2 M83 204.24712522989094 -29.868853678162907 0.1650122764060855 "b'Polygon ICRS 204.319476 -29.894283 204.314307 -29.893360 204.174614 -29.893832 204.159606 -29.886181 204.153471 -29.877399 204.157057 -29.856075 204.152987 -29.849020 204.155142 -29.845057 204.159570 -29.842949 204.165241 -29.843807 204.183416 -29.843187 204.297720 -29.843831 204.302372 -29.842863 204.307818 -29.845083 204.310143 -29.850046 204.314283 -29.849063 204.334321 -29.849727 204.337744 -29.852341 204.343021 -29.861027 204.339032 -29.868522 204.342934 -29.888997 204.337052 -29.894104'" 1.5779219110458824 58480.431262 58488.516277 156.52 156.52 0.002624500730920478 0.002668272377703091 58490.401574578646 /XX/YY/ phot.flux.density;phys.polarization http://almascience.org/aq?member_ous_id=uid://A001/X1295/X35 text/html 2017.1.00079.S Public 314.57611565717997 31.970814026685794 3 36592333492997.875 1875000000.0 "A001:DA44 A002:DA51 A003:DV12 A004:DV18 A005:DA58 A006:DV25 A007:DV05 A008:DV07 A010:DV13 A011:DV21 A015:DA64 A016:DV11 A017:DV14 A018:DA54 A019:DA61 A022:DV23 A023:DA42 A024:DA49 A025:DA46 A026:DV19 A027:DV06 A033:DA53 A034:DA50 A035:DA43 A036:DA45 A039:DA56 A040:DA55 A041:DA57 A042:DV08 A043:DA60 A044:DV03 A045:DV01 A047:DV15 A048:DV02 A049:DA47 A050:DA62 A058:DA41 A060:DV17 A062:DV10 A066:DV04 A068:DA65 A069:DV16 A070:DA48 A072:DV20 A073:DA54 A074:DV09 A075:DV18 A076:DA59 A082:DA52 A083:DV22 A085:DA63 A088:DV24" T b'2020-03-03T03:20:58.000' 1.5779219110458824 "[112.35..114.23GHz,1952.95kHz,8mJy/beam@10km/s,360.2uJy/beam@native, XX YY] U [113.81..115.79GHz,31247.16kHz,9.9mJy/beam@10km/s,433.6uJy/beam@native, XX YY] U [114.84..115.31GHz,488.24kHz,8.2mJy/beam@10km/s,739.3uJy/beam@native, XX YY] U [114.95..115.19GHz,122.06kHz,8.2mJy/beam@10km/s,1mJy/beam@native, XX YY]" 114.07366301863082 317.6733782195699 "Koda, Jin" "" "" "" "" "" "We propose a full mapping of the cold molecular gas over the whole disk of M83 in CO(1-0) using ALMA 12m+7m+TP. This closest (d=4.5Mpc) face-on (i<30deg) archetypical barred spiral galaxy closely resembles our own Milky Way, and has been the important showcase for multi-wavelength studies. The proposed ALMA CO(1-0) map will not only detect the smallest clouds at the lowest end of the cloud mass function (10^4Msun), but also reveal the more extended, ambient molecular gas outside GMCs. This full census of the molecular ISM will be correlated with galactic environments and star formation, from the molecule-dominated galactic center to the atom-dominated outskirts, across the bar, spiral arms, and interarm regions. In combination with the ALMA CO(2-1) map (Cycle 4), we will use the CO 2-1/1-0 ratios of all GMCs to diagnose their physical states and variations as a function of the environments. We will also test the paradigm of GMCs as physical entities by measuring the partition between GMCs and the ambient molecular gas. The ALMA CO(1-0) map will become the foundation for future ALMA studies at higher resolution and in other molecular lines." M83_e_03_TM1 "Oka, Tomoharu; Wu, Yu-Ting; Muraoka, Kazuyuki; Madore, Barry; Watanabe, Yoshimasa; Nakanishi, Kouichiro; Sakamoto, Kazushi; Boissier, Samuel; Seibert, Mark; Martin, Sergio; Sawada, Tsuyoshi; Kuno, Nario; Vlahakis, Catherine; Donovan Meyer, Jennifer; Elmegreen, Bruce; Harada, Nanase; Heyer, Mark; Keto, Eric; Hirota, Akihiko; Kobayashi, Masato; Ohyama, Youichi; Scoville, Nick; Ho, Luis; Tosaki, Tomoka; Egusa, Fumi; Onodera, Sachiko; Gil de Paz, Armando;" 8.023585259543626 0.25181849902410236 5.474677085876465 uid://A001/X1295/X34 uid://A001/X1295/X35 uid://A002/Xd74c3f/Xcf20 "Mapping Molecular ISM in the Whole Disk of M83" S TARGET T 21.61875828636442 T "" "Spiral galaxies, Giant Molecular Clouds (GMC) properties" "Local Universe" 2000000 b'2019-12-11T20:57:17.290' diff --git a/astroquery/alma/tests/test_alma.py b/astroquery/alma/tests/test_alma.py index c6c1177ac8..19f3fd0170 100644 --- a/astroquery/alma/tests/test_alma.py +++ b/astroquery/alma/tests/test_alma.py @@ -199,17 +199,6 @@ def test_gen_datetime_sql(): common_select + "(58849.0<=t_min AND t_min<=58880.0)" -def test_gen_spec_res_sql(): - common_select = 'select * from ivoa.obscore WHERE ' - assert _gen_sql({'spectral_resolution': 70}) == common_select + "em_resolution=20985472.06" - assert _gen_sql({'spectral_resolution': '<70'}) == common_select + "em_resolution>=20985472.06" - assert _gen_sql({'spectral_resolution': '>70'}) == common_select + "em_resolution<=20985472.06" - assert _gen_sql({'spectral_resolution': '(70 .. 80)'}) == common_select + \ - "(23983396.64<=em_resolution AND em_resolution<=20985472.06)" - assert _gen_sql({'spectral_resolution': '(70|80)'}) == common_select + \ - "(em_resolution=20985472.06 OR em_resolution=23983396.64)" - - def test_gen_public_sql(): common_select = 'select * from ivoa.obscore' assert _gen_sql({'public_data': None}) == common_select @@ -356,6 +345,19 @@ def test_query(): language='ADQL', maxrec=None ) + tap_mock.reset() + result = alma.query_region('1 2', radius=1*u.deg, + payload={'em_resolution': 6.929551916151968e-05, + 'spectral_resolution': 2000000} + ) + assert len(result) == 0 + tap_mock.search.assert_called_with( + "select * from ivoa.obscore WHERE em_resolution=6.929551916151968e-05 " + "AND spectral_resolution=2000000 " + "AND (INTERSECTS(CIRCLE('ICRS',1.0,2.0,1.0), " + "s_region) = 1) AND science_observation='T' AND data_rights='Public'", + language='ADQL', maxrec=None) + @pytest.mark.filterwarnings("ignore::astropy.utils.exceptions.AstropyUserWarning") def test_enhanced_table(): From 50ba0894c04279b327cf277630371f672a8cb945 Mon Sep 17 00:00:00 2001 From: Sam Bianco Date: Tue, 11 Jun 2024 18:46:54 -0400 Subject: [PATCH 13/35] Modulate verbosity on download_products() function --- astroquery/mast/cloud.py | 39 +++++++++++++++++++-------------- astroquery/mast/observations.py | 32 ++++++++++++++++++--------- astroquery/query.py | 27 ++++++++++++++--------- 3 files changed, 61 insertions(+), 37 deletions(-) diff --git a/astroquery/mast/cloud.py b/astroquery/mast/cloud.py index 40135014dc..02dd4d6576 100644 --- a/astroquery/mast/cloud.py +++ b/astroquery/mast/cloud.py @@ -161,7 +161,7 @@ def get_cloud_uri_list(self, data_products, include_bucket=True, full_url=False) return [self.get_cloud_uri(product, include_bucket, full_url) for product in data_products] - def download_file(self, data_product, local_path, cache=True): + def download_file(self, data_product, local_path, cache=True, verbose=True): """ Takes a data product in the form of an `~astropy.table.Row` and downloads it from the cloud into the given directory. @@ -174,6 +174,8 @@ def download_file(self, data_product, local_path, cache=True): The local filename to which toe downloaded file will be saved. cache : bool Default is True. If file is found on disc it will not be downloaded again. + verbose : bool, optional + Default is True. Whether to show download progress in the console. """ s3 = self.boto3.resource('s3', config=self.config) @@ -203,24 +205,27 @@ def download_file(self, data_product, local_path, cache=True): .format(local_path, statinfo.st_size)) return - with ProgressBarOrSpinner(length, ('Downloading URL s3://{0}/{1} to {2} ...'.format( - self.pubdata_bucket, bucket_path, local_path))) as pb: + if verbose: + with ProgressBarOrSpinner(length, ('Downloading URL s3://{0}/{1} to {2} ...'.format( + self.pubdata_bucket, bucket_path, local_path))) as pb: - # Bytes read tracks how much data has been received so far - # This variable will be updated in multiple threads below - global bytes_read - bytes_read = 0 + # Bytes read tracks how much data has been received so far + # This variable will be updated in multiple threads below + global bytes_read + bytes_read = 0 - progress_lock = threading.Lock() + progress_lock = threading.Lock() - def progress_callback(numbytes): - # Boto3 calls this from multiple threads pulling the data from S3 - global bytes_read + def progress_callback(numbytes): + # Boto3 calls this from multiple threads pulling the data from S3 + global bytes_read - # This callback can be called in multiple threads - # Access to updating the console needs to be locked - with progress_lock: - bytes_read += numbytes - pb.update(bytes_read) + # This callback can be called in multiple threads + # Access to updating the console needs to be locked + with progress_lock: + bytes_read += numbytes + pb.update(bytes_read) - bkt.download_file(bucket_path, local_path, Callback=progress_callback) + bkt.download_file(bucket_path, local_path, Callback=progress_callback) + else: + bkt.download_file(bucket_path, local_path) diff --git a/astroquery/mast/observations.py b/astroquery/mast/observations.py index 7517f858ec..36353f782c 100644 --- a/astroquery/mast/observations.py +++ b/astroquery/mast/observations.py @@ -500,7 +500,7 @@ def filter_products(self, products, *, mrp_only=False, extension=None, **filters return products[np.where(filter_mask)] - def download_file(self, uri, *, local_path=None, base_url=None, cache=True, cloud_only=False): + def download_file(self, uri, *, local_path=None, base_url=None, cache=True, cloud_only=False, verbose=True): """ Downloads a single file based on the data URI @@ -518,6 +518,8 @@ def download_file(self, uri, *, local_path=None, base_url=None, cache=True, clou Default False. If set to True and cloud data access is enabled (see `enable_cloud_dataset`) files that are not found in the cloud will be skipped rather than downloaded from MAST as is the default behavior. If cloud access is not enables this argument as no affect. + verbose : bool, optional + Default True. Whether to show download progress in the console. Returns ------- @@ -554,7 +556,7 @@ def download_file(self, uri, *, local_path=None, base_url=None, cache=True, clou try: if self._cloud_connection is not None and self._cloud_connection.is_supported(data_product): try: - self._cloud_connection.download_file(data_product, local_path, cache) + self._cloud_connection.download_file(data_product, local_path, cache, verbose) except Exception as ex: log.exception("Error pulling from S3 bucket: {}".format(ex)) if cloud_only: @@ -564,10 +566,12 @@ def download_file(self, uri, *, local_path=None, base_url=None, cache=True, clou else: log.warning("Falling back to mast download...") self._download_file(data_url, local_path, - cache=cache, head_safe=True, continuation=False) + cache=cache, head_safe=True, continuation=False, + verbose=verbose) else: self._download_file(data_url, local_path, - cache=cache, head_safe=True, continuation=False) + cache=cache, head_safe=True, continuation=False, + verbose=verbose) # check if file exists also this is where would perform md5, # and also check the filesize if the database reliably reported file sizes @@ -583,7 +587,7 @@ def download_file(self, uri, *, local_path=None, base_url=None, cache=True, clou return status, msg, url - def _download_files(self, products, base_dir, *, flat=False, cache=True, cloud_only=False,): + def _download_files(self, products, base_dir, *, flat=False, cache=True, cloud_only=False, verbose=True): """ Takes an `~astropy.table.Table` of data products and downloads them into the directory given by base_dir. @@ -602,6 +606,8 @@ def _download_files(self, products, base_dir, *, flat=False, cache=True, cloud_o Default False. If set to True and cloud data access is enabled (see `enable_cloud_dataset`) files that are not found in the cloud will be skipped rather than downloaded from MAST as is the default behavior. If cloud access is not enables this argument as no affect. + verbose : bool, optional + Default True. Whether to show download progress in the console. Returns ------- @@ -622,7 +628,7 @@ def _download_files(self, products, base_dir, *, flat=False, cache=True, cloud_o # download the files status, msg, url = self.download_file(data_product["dataURI"], local_path=local_path, - cache=cache, cloud_only=cloud_only) + cache=cache, cloud_only=cloud_only, verbose=verbose) manifest_array.append([local_path, status, msg, url]) @@ -630,7 +636,7 @@ def _download_files(self, products, base_dir, *, flat=False, cache=True, cloud_o return manifest - def _download_curl_script(self, products, out_dir): + def _download_curl_script(self, products, out_dir, verbose=True): """ Takes an `~astropy.table.Table` of data products and downloads a curl script to pull the datafiles. @@ -640,6 +646,8 @@ def _download_curl_script(self, products, out_dir): Table containing products to be included in the curl script. out_dir : str Directory in which the curl script will be saved. + verbose : bool, optional + Default True. Whether to show download progress in the console. Returns ------- @@ -651,7 +659,7 @@ def _download_curl_script(self, products, out_dir): local_path = os.path.join(out_dir, download_file) self._download_file(self._portal_api_connection.MAST_BUNDLE_URL + ".sh", - local_path, data=url_list, method="POST") + local_path, data=url_list, method="POST", verbose=verbose) status = "COMPLETE" msg = None @@ -666,7 +674,8 @@ def _download_curl_script(self, products, out_dir): return manifest def download_products(self, products, *, download_dir=None, flat=False, - cache=True, curl_flag=False, mrp_only=False, cloud_only=False, **filters): + cache=True, curl_flag=False, mrp_only=False, cloud_only=False, verbose=True, + **filters): """ Download data products. If cloud access is enabled, files will be downloaded from the cloud if possible. @@ -698,6 +707,8 @@ def download_products(self, products, *, download_dir=None, flat=False, Default False. If set to True and cloud data access is enabled (see `enable_cloud_dataset`) files that are not found in the cloud will be skipped rather than downloaded from MAST as is the default behavior. If cloud access is not enables this argument as no affect. + verbose : bool, optional + Default True. Whether to show download progress in the console. **filters : Filters to be applied. Valid filters are all products fields returned by ``get_metadata("products")`` and 'extension' which is the desired file extension. @@ -758,7 +769,8 @@ def download_products(self, products, *, download_dir=None, flat=False, manifest = self._download_files(products, base_dir=base_dir, flat=flat, cache=cache, - cloud_only=cloud_only) + cloud_only=cloud_only, + verbose=verbose) return manifest diff --git a/astroquery/query.py b/astroquery/query.py index 9358eca306..c41630412e 100644 --- a/astroquery/query.py +++ b/astroquery/query.py @@ -386,7 +386,7 @@ def _request(self, method, url, def _download_file(self, url, local_filepath, timeout=None, auth=None, continuation=True, cache=False, method="GET", - head_safe=False, **kwargs): + head_safe=False, verbose=True, **kwargs): """ Download a file. Resembles `astropy.utils.data.download_file` but uses the local ``_session`` @@ -405,6 +405,8 @@ def _download_file(self, url, local_filepath, timeout=None, auth=None, Cache downloaded file. Defaults to False. method : "GET" or "POST" head_safe : bool + verbose : bool + Whether to show download progress. Defaults to True. """ if head_safe: @@ -492,16 +494,21 @@ def _download_file(self, url, local_filepath, timeout=None, auth=None, else: progress_stream = io.StringIO() - with ProgressBarOrSpinner(length, f'Downloading URL {url} to {local_filepath} ...', - file=progress_stream) as pb: + if verbose: + with ProgressBarOrSpinner(length, f'Downloading URL {url} to {local_filepath} ...', + file=progress_stream) as pb: + with open(local_filepath, open_mode) as f: + for block in response.iter_content(blocksize): + f.write(block) + bytes_read += len(block) + if length is not None: + pb.update(bytes_read if bytes_read <= length else length) + else: + pb.update(bytes_read) + else: with open(local_filepath, open_mode) as f: - for block in response.iter_content(blocksize): - f.write(block) - bytes_read += len(block) - if length is not None: - pb.update(bytes_read if bytes_read <= length else length) - else: - pb.update(bytes_read) + f.write(response.content) + response.close() return response From 8bc95d255dbffe387ae8e9c9b69fd3860c95e903 Mon Sep 17 00:00:00 2001 From: Sam Bianco Date: Wed, 12 Jun 2024 12:18:13 -0400 Subject: [PATCH 14/35] style fixes --- astroquery/mast/observations.py | 2 +- astroquery/query.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/astroquery/mast/observations.py b/astroquery/mast/observations.py index 36353f782c..80bb18e84f 100644 --- a/astroquery/mast/observations.py +++ b/astroquery/mast/observations.py @@ -674,7 +674,7 @@ def _download_curl_script(self, products, out_dir, verbose=True): return manifest def download_products(self, products, *, download_dir=None, flat=False, - cache=True, curl_flag=False, mrp_only=False, cloud_only=False, verbose=True, + cache=True, curl_flag=False, mrp_only=False, cloud_only=False, verbose=True, **filters): """ Download data products. diff --git a/astroquery/query.py b/astroquery/query.py index c41630412e..ca24008efa 100644 --- a/astroquery/query.py +++ b/astroquery/query.py @@ -496,7 +496,7 @@ def _download_file(self, url, local_filepath, timeout=None, auth=None, if verbose: with ProgressBarOrSpinner(length, f'Downloading URL {url} to {local_filepath} ...', - file=progress_stream) as pb: + file=progress_stream) as pb: with open(local_filepath, open_mode) as f: for block in response.iter_content(blocksize): f.write(block) @@ -507,7 +507,7 @@ def _download_file(self, url, local_filepath, timeout=None, auth=None, pb.update(bytes_read) else: with open(local_filepath, open_mode) as f: - f.write(response.content) + f.write(response.content) response.close() return response From 8c0a3ff194b4af2aa5068253cc95eaf6cea72185 Mon Sep 17 00:00:00 2001 From: Sam Bianco Date: Wed, 12 Jun 2024 12:21:18 -0400 Subject: [PATCH 15/35] Update changes file --- CHANGES.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index d1e8866b7e..63bf0c0980 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -12,6 +12,7 @@ alma ^^^^ - Added method to return quantities instead of values and regions footprint in alma [#2855] + - Added support for frequency_resolution in KHz [#3035] mpc @@ -88,8 +89,12 @@ mast ^^^^ - Fix bug in which the ``local_path`` parameter for the ``mast.observations.download_file`` method does not accept a directory. [#3016] + - Optimize remote test suite to improve performance and reduce execution time. [#3036] +- Add ``verbose`` parameter to modulate output in ``mast.observations.download_products`` method. [#3031] + + 0.4.7 (2024-03-08) ================== From a5189c653f99d648b2f01d407388566ee4f455fa Mon Sep 17 00:00:00 2001 From: Sam Bianco Date: Wed, 12 Jun 2024 13:03:29 -0400 Subject: [PATCH 16/35] Add check to test case --- astroquery/mast/tests/test_mast.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/astroquery/mast/tests/test_mast.py b/astroquery/mast/tests/test_mast.py index 6baf3bc5c4..dd048d43a2 100644 --- a/astroquery/mast/tests/test_mast.py +++ b/astroquery/mast/tests/test_mast.py @@ -496,6 +496,13 @@ def test_observations_download_products(patch_post, tmpdir): mrp_only=False) assert isinstance(result, Table) + # without console output + result = mast.Observations.download_products('2003738726', + download_dir=str(tmpdir), + productType=["SCIENCE"], + verbose=False) + assert isinstance(result, Table) + # passing row product products = mast.Observations.get_product_list('2003738726') result1 = mast.Observations.download_products(products[0], From 458bbed9f10fe91eeecb0e6e9d7bd1612e0510f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Brigitta=20Sip=C5=91cz?= Date: Fri, 14 Jun 2024 14:01:44 -0700 Subject: [PATCH 17/35] MAINT: update mailmap --- .mailmap | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/.mailmap b/.mailmap index d6d3dd8d98..b2ed066c57 100644 --- a/.mailmap +++ b/.mailmap @@ -3,6 +3,9 @@ Adam Ginsburg Adam Ginsburg Adrian Damian Andrew O'Brien +Angel Ruiz +Antonio Ortega +Ari Pollak Austen Groener Austen Groener Ayush Yadav @@ -16,6 +19,7 @@ Clara Brasseur David Collom David Collom David Collom +Dino Bektesevic E. Madison Bray E. Madison Bray Edward Gomez @@ -23,12 +27,15 @@ Elena Colomo Eric Koch Erwan Pannier Fran Raga -Fred Moolekamp Magnus Persson +Fred Moolekamp +Hadrien Devillepoix Hans Moritz Guenter Henrik Norman Henrik Norman +Henrik Norman Jaladh Singhal James Dempsey +James Guillochon Javier Ballester Javier Duran Javier Duran @@ -43,33 +50,49 @@ Javier Espinosa <64952559+jespinosaar@users.noreply.gi Javier Espinosa Javier Espinosa Javier Espinosa +Javier Espinosa Jennifer Medina Jesus Juan Salgado +Jon Juaristi Campillo Jonathan Gagne Jordan Mirocha +Jorge Fernandez Hernandez Juan Carlos Segovia +Juanjo Bazán +Julien Milli Julien Woillez Julien Woillez +Kathleen Kiker <72056544+KatKiker@users.noreply.github.com> Kelvin Lee +Kyle Willett Larry Bradley Loïc Séguin-C Luis Valero Martín Madhura Parikh Magnus Persson Magnus Persson +Manon Marchand Maria H. Sarmiento Matthew Craig Matthieu Baumann Michael Mommert +Michael Mommert +Michael St. Clair +Michael St. Clair <64057573+m-stclair@users.noreply.github.com> Michele Costa Miguel de Val-Borro Natalie Korzh +Naveen Srinivasan <172697+naveensrinivasan@users.noreply.github.com> Nicholas Earl Oliver Oberdorf Oliver Oberdorf Pey Lian Lim <2090236+pllim@users.noreply.github.com> +Prajwel Joseph Raul Gutierrez Raul Gutierrez +Rounak Agarwal +Sam Lee +Sashank Mishra Simon Conseil Simon Conseil Simon Liedtke @@ -78,5 +101,6 @@ Syed Gilani Syed Gilani Tinuade Adeleke Tim Galvin +Tomas Alonso Albi Volodymyr Savchenko Volodymyr Savchenko From 40bc71b71846bb5991848a76a43a0f9d140c4420 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Brigitta=20Sip=C5=91cz?= Date: Fri, 14 Jun 2024 14:02:00 -0700 Subject: [PATCH 18/35] MAINT: update licence year --- LICENSE.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE.rst b/LICENSE.rst index 79cf6998d1..f4c35516b6 100644 --- a/LICENSE.rst +++ b/LICENSE.rst @@ -1,4 +1,4 @@ -Copyright (c) 2011-2023 Astroquery Developers +Copyright (c) 2011-2024 Astroquery Developers All rights reserved. Redistribution and use in source and binary forms, with or without modification, From c529bfdb32ad3ddcdd04e33aacc867442c4efb06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Brigitta=20Sip=C5=91cz?= Date: Fri, 14 Jun 2024 14:02:40 -0700 Subject: [PATCH 19/35] TST: skipping test when dependency is missing --- astroquery/alma/tests/test_alma_remote.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/astroquery/alma/tests/test_alma_remote.py b/astroquery/alma/tests/test_alma_remote.py index db8362a57e..b9258afcaf 100644 --- a/astroquery/alma/tests/test_alma_remote.py +++ b/astroquery/alma/tests/test_alma_remote.py @@ -16,6 +16,13 @@ from astroquery.exceptions import CorruptDataWarning from astroquery.alma import Alma, get_enhanced_table +try: + import regions + + HAS_REGIONS = True +except ImportError: + HAS_REGIONS = False + # ALMA tests involving staging take too long, leading to travis timeouts # TODO: make this a configuration item SKIP_SLOW = True @@ -62,11 +69,10 @@ def test_public(self, alma): for row in results: assert row['data_rights'] == 'Proprietary' + @pytest.mark.skipif(not HAS_REGIONS, reason="regions is required") @pytest.mark.filterwarnings( "ignore::astropy.utils.exceptions.AstropyUserWarning") def test_s_region(self, alma): - pytest.importorskip('regions') - import regions # to silence checkstyle alma.help_tap() result = alma.query_tap("select top 3 s_region from ivoa.obscore") enhanced_result = get_enhanced_table(result) @@ -75,6 +81,7 @@ def test_s_region(self, alma): regions.PolygonSkyRegion, regions.CompoundSkyRegion)) + @pytest.mark.skipif(not HAS_REGIONS, reason="regions is required") @pytest.mark.filterwarnings( "ignore::astropy.utils.exceptions.AstropyUserWarning") def test_SgrAstar(self, tmp_path, alma): From a682a6a7d6e6a354e7afe76c497c9f91a5dafa87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Brigitta=20Sip=C5=91cz?= Date: Fri, 14 Jun 2024 14:07:58 -0700 Subject: [PATCH 20/35] MAINT: raise exception for missing dependency --- astroquery/alma/core.py | 1 + 1 file changed, 1 insertion(+) diff --git a/astroquery/alma/core.py b/astroquery/alma/core.py index 947d24d53b..52adff39e0 100644 --- a/astroquery/alma/core.py +++ b/astroquery/alma/core.py @@ -218,6 +218,7 @@ def get_enhanced_table(result): print( "Could not import astropy-regions, which is a requirement for get_enhanced_table function in alma." "Please refer to http://astropy-regions.readthedocs.io/en/latest/installation.html for how to install it.") + raise def _parse_stcs_string(input): csys = 'icrs' From beecc98390d575747f88ca850877a29e869b3d56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Brigitta=20Sip=C5=91cz?= Date: Fri, 14 Jun 2024 16:43:54 -0700 Subject: [PATCH 21/35] DOC: fixing failing doctest examples --- docs/ipac/irsa/irsa.rst | 2 +- docs/ipac/irsa/irsa_dust/irsa_dust.rst | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/ipac/irsa/irsa.rst b/docs/ipac/irsa/irsa.rst index cab15dedb1..e231e9a89f 100644 --- a/docs/ipac/irsa/irsa.rst +++ b/docs/ipac/irsa/irsa.rst @@ -244,7 +244,7 @@ will return a `~astropy.table.Table`: >>> from astroquery.ipac.irsa import Irsa >>> Irsa.list_collections() - +
collection object --------------------- diff --git a/docs/ipac/irsa/irsa_dust/irsa_dust.rst b/docs/ipac/irsa/irsa_dust/irsa_dust.rst index a03a614843..a49f809b63 100644 --- a/docs/ipac/irsa/irsa_dust/irsa_dust.rst +++ b/docs/ipac/irsa/irsa_dust/irsa_dust.rst @@ -137,7 +137,7 @@ value: IRAC-4 7.68 0.122 0.005 0.151 0.006 WISE-1 3.32 0.189 0.008 0.234 0.01 WISE-2 4.57 0.146 0.006 0.18 0.008 - Length = 25 rows + Get other query details ----------------------- @@ -176,7 +176,7 @@ If you are repeatedly getting failed queries, or bad/out-of-date results, try cl >>> from astroquery.ipac.irsa.irsa_dust import IrsaDust >>> IrsaDust.clear_cache() -If this function is unavailable, upgrade your version of astroquery. +If this function is unavailable, upgrade your version of astroquery. The ``clear_cache`` function was introduced in version 0.4.7.dev8479. From e1231b08a28ec2fb9c3c740db39a0247235a9f50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Brigitta=20Sip=C5=91cz?= Date: Fri, 14 Jun 2024 16:44:14 -0700 Subject: [PATCH 22/35] TST: adding new datarelease --- astroquery/ipac/irsa/tests/test_most_remote.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/astroquery/ipac/irsa/tests/test_most_remote.py b/astroquery/ipac/irsa/tests/test_most_remote.py index 4648cb138b..296aabb35a 100644 --- a/astroquery/ipac/irsa/tests/test_most_remote.py +++ b/astroquery/ipac/irsa/tests/test_most_remote.py @@ -82,11 +82,11 @@ def test_list_catalogs(): 'wise_neowiser_yr1', 'wise_neowiser_yr2', 'wise_neowiser_yr3', 'wise_neowiser_yr4', 'wise_neowiser_yr5', 'wise_neowiser_yr6', 'wise_neowiser_yr7', 'wise_neowiser_yr8', 'wise_neowiser_yr9', 'ztf', - 'wise_merge_int', 'wise_neowiser_int', 'wise_neowiser_yr10' + 'wise_merge_int', 'wise_neowiser_int', 'wise_neowiser_yr10', 'wise_neowiser_yr11' ] cats = Most.list_catalogs() - assert expected == cats + assert set(expected) == set(cats) @pytest.mark.remote_data From bafbef83866ceccb37342691a5a8fe7de4067984 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Brigitta=20Sip=C5=91cz?= Date: Fri, 14 Jun 2024 18:41:38 -0700 Subject: [PATCH 23/35] MAINT: remove deprecated usage of BeautifulSoup --- astroquery/alma/core.py | 6 +++--- astroquery/cosmosim/core.py | 2 +- astroquery/eso/core.py | 6 +++--- astroquery/ipac/irsa/ibe/core.py | 6 +++--- astroquery/linelists/cdms/core.py | 2 +- astroquery/simbad/get_votable_fields.py | 2 +- astroquery/skyview/core.py | 4 ++-- astroquery/splatalogue/build_species_table.py | 2 +- astroquery/wfau/core.py | 2 +- 9 files changed, 16 insertions(+), 16 deletions(-) diff --git a/astroquery/alma/core.py b/astroquery/alma/core.py index 52adff39e0..99b060f9b5 100644 --- a/astroquery/alma/core.py +++ b/astroquery/alma/core.py @@ -1162,9 +1162,9 @@ def _cycle0_tarfile_content(self): # which the default parser does not pick up root = BeautifulSoup(response.content, 'html.parser') html_table = root.find('table', class_='grid listing') - data = list(zip(*[(x.findAll('td')[0].text, - x.findAll('td')[1].text) - for x in html_table.findAll('tr')])) + data = list(zip(*[(x.find_all('td')[0].text, + x.find_all('td')[1].text) + for x in html_table.find_all('tr')])) columns = [Column(data=data[0], name='ID'), Column(data=data[1], name='Files')] tbl = Table(columns) diff --git a/astroquery/cosmosim/core.py b/astroquery/cosmosim/core.py index c4c735a408..124ab8e7df 100644 --- a/astroquery/cosmosim/core.py +++ b/astroquery/cosmosim/core.py @@ -1109,7 +1109,7 @@ def download(self, jobid=None, filename=None, format=None, cache=True): auth=(self.username, self.password)) soup = BeautifulSoup(results.content, "lxml") urls = [i.get('xlink:href') - for i in soup.findAll({'uws:result'})] + for i in soup.find_all({'uws:result'})] formatlist = [urls[i].split('/')[-1].upper() for i in range(len(urls))] diff --git a/astroquery/eso/core.py b/astroquery/eso/core.py index 0ba9a33484..421e6fd81a 100644 --- a/astroquery/eso/core.py +++ b/astroquery/eso/core.py @@ -348,10 +348,10 @@ def list_surveys(self, *, cache=True): collections_table = root.find('table', id='collections_table') other_collections = root.find('select', id='collection_name_option') # it is possible to have empty collections or other collections... - collection_elts = (collections_table.findAll('input', type='checkbox') + collection_elts = (collections_table.find_all('input', type='checkbox') if collections_table is not None else []) - other_elts = (other_collections.findAll('option') + other_elts = (other_collections.find_all('option') if other_collections is not None else []) for element in (collection_elts + other_elts): @@ -969,7 +969,7 @@ def _print_surveys_help(self, url, *, cache=True): # hovertext from different labels are used to give more info on forms helptext_dict = {abbr['title'].split(":")[0].strip(): ":".join(abbr['title'].split(":")[1:]) - for abbr in form.findAll('abbr') + for abbr in form.find_all('abbr') if 'title' in abbr.attrs and ":" in abbr['title']} for fieldset in form.select('fieldset'): diff --git a/astroquery/ipac/irsa/ibe/core.py b/astroquery/ipac/irsa/ibe/core.py index 9f6a2fe881..3d46410cd7 100644 --- a/astroquery/ipac/irsa/ibe/core.py +++ b/astroquery/ipac/irsa/ibe/core.py @@ -271,7 +271,7 @@ def list_missions(self, *, cache=True): cache=cache) root = BeautifulSoup(response.text, 'html5lib') - links = root.findAll('a') + links = root.find_all('a') missions = [os.path.basename(a.attrs['href'].rstrip('/')) for a in links] self._missions = missions @@ -309,7 +309,7 @@ def list_datasets(self, *, mission=None, cache=True): cache=cache) root = BeautifulSoup(response.text, 'html5lib') - links = root.findAll('a') + links = root.find_all('a') datasets = [a.text for a in links if a.attrs['href'].count('/') >= 4 # shown as '..'; ignore ] @@ -364,7 +364,7 @@ def list_tables(self, *, mission=None, dataset=None, cache=True): cache=cache) root = BeautifulSoup(response.text, 'html5lib') - return [tr.find('td').string for tr in root.findAll('tr')[1:]] + return [tr.find('td').string for tr in root.find_all('tr')[1:]] # Unfortunately, the URL construction for each data set is different, and # they're not obviously accessible via API diff --git a/astroquery/linelists/cdms/core.py b/astroquery/linelists/cdms/core.py index 67919d96ba..a5bb2d68fa 100644 --- a/astroquery/linelists/cdms/core.py +++ b/astroquery/linelists/cdms/core.py @@ -156,7 +156,7 @@ def query_lines_async(self, min_frequency, max_frequency, *, soup = BeautifulSoup(response.text, 'html.parser') ok = False - urls = [x.attrs['src'] for x in soup.findAll('frame',)] + urls = [x.attrs['src'] for x in soup.find_all('frame',)] for url in urls: if 'tab' in url and 'head' not in url: ok = True diff --git a/astroquery/simbad/get_votable_fields.py b/astroquery/simbad/get_votable_fields.py index be817969cd..4c597212c1 100644 --- a/astroquery/simbad/get_votable_fields.py +++ b/astroquery/simbad/get_votable_fields.py @@ -17,7 +17,7 @@ def reload_votable_fields_json(): # Find the first
tag that follows it table = foundtext.findNext('table') outd = {} - for row in table.findAll('tr'): + for row in table.find_all('tr'): cols = row.findChildren('td') if len(cols) > 1: smallest_child = cols[0].find_all()[-1] diff --git a/astroquery/skyview/core.py b/astroquery/skyview/core.py index 5f1de6d6b1..c3a94b1293 100644 --- a/astroquery/skyview/core.py +++ b/astroquery/skyview/core.py @@ -283,10 +283,10 @@ def survey_dict(self): response = self._request('GET', self.URL, cache=False) response.raise_for_status() page = BeautifulSoup(response.content, "html.parser") - surveys = page.findAll('select', {'name': 'survey'}) + surveys = page.find_all('select', {'name': 'survey'}) self._survey_dict = { - sel['id']: [x.text for x in sel.findAll('option')] + sel['id']: [x.text for x in sel.find_all('option')] for sel in surveys if 'overlay' not in sel['id'] } diff --git a/astroquery/splatalogue/build_species_table.py b/astroquery/splatalogue/build_species_table.py index e0a11ed5b3..6fe0f787b2 100644 --- a/astroquery/splatalogue/build_species_table.py +++ b/astroquery/splatalogue/build_species_table.py @@ -53,7 +53,7 @@ def get_json_species_ids(*, outfile='splat-species.json', base_url=conf.base_url result = requests.get(f'{base_url}/b.php') page = bs4.BeautifulSoup(result.content, 'html5lib') # The ID needs to be checked periodically if Splatalogue is updated - sid = page.findAll('select', attrs={'id': 'speciesselectbox'})[0] + sid = page.find_all('select', attrs={'id': 'speciesselectbox'})[0] species_types = set() for kid in sid.children: diff --git a/astroquery/wfau/core.py b/astroquery/wfau/core.py index 7df464b142..fee25e5093 100644 --- a/astroquery/wfau/core.py +++ b/astroquery/wfau/core.py @@ -665,7 +665,7 @@ def _get_databases(self): root = BeautifulSoup(response.content, features='html5lib') databases = [xrf.attrs['value'] for xrf in - root.find('select').findAll('option')] + root.find('select').find_all('option')] return databases def list_databases(self): From 620351a26e43ebb961b4c937db9c46dd00e2cf2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Brigitta=20Sip=C5=91cz?= Date: Fri, 14 Jun 2024 18:50:04 -0700 Subject: [PATCH 24/35] CI: separate out online tests to their own workflow --- .github/workflows/ci_crontests.yml | 24 +++-------- .github/workflows/ci_online_crontests.yml | 51 +++++++++++++++++++++++ 2 files changed, 56 insertions(+), 19 deletions(-) create mode 100644 .github/workflows/ci_online_crontests.yml diff --git a/.github/workflows/ci_crontests.yml b/.github/workflows/ci_crontests.yml index 8cd9c50e20..f6905def91 100644 --- a/.github/workflows/ci_crontests.yml +++ b/.github/workflows/ci_crontests.yml @@ -6,8 +6,8 @@ on: tags: - '*' schedule: - # run every Friday at 23:00 UTC - - cron: '0 23 * * 5' + # run every Friday at 22:00 UTC + - cron: '0 22 * * 5' workflow_dispatch: permissions: @@ -22,24 +22,10 @@ jobs: fail-fast: false matrix: include: - - name: py3.11 all dev deps online + - name: py3.12 pre-release all deps os: ubuntu-latest - python: '3.11' - toxenv: py311-test-alldeps-devdeps-online - toxargs: -v - toxposargs: -v --durations=50 - - - name: Windows py3.9 all deps online - os: windows-latest - python: '3.9' - toxenv: py39-test-alldeps-online - toxargs: -v - toxposargs: -v --durations=50 - - - name: py3.11 pre-release all deps - os: ubuntu-latest - python: '3.11' - toxenv: py311-test-alldeps-predeps + python: '3.12' + toxenv: py312-test-alldeps-predeps toxargs: -v toxposargs: -v diff --git a/.github/workflows/ci_online_crontests.yml b/.github/workflows/ci_online_crontests.yml new file mode 100644 index 0000000000..c9527e8211 --- /dev/null +++ b/.github/workflows/ci_online_crontests.yml @@ -0,0 +1,51 @@ +name: CI-online-crontests + +on: + push: + # Run this job on release tags, but not on pushes to the main branch + tags: + - '*' + schedule: + # run every Friday at 23:00 UTC + - cron: '0 23 * * 5' + workflow_dispatch: + +permissions: + contents: read + +jobs: + tests: + name: ${{ matrix.name }} + runs-on: ${{ matrix.os }} + if: github.repository == 'astropy/astroquery' + strategy: + fail-fast: false + matrix: + include: + - name: py3.12 all dev deps online + os: ubuntu-latest + python: '3.12' + toxenv: py312-test-alldeps-devdeps-online + toxargs: -v + toxposargs: -v --durations=50 + + - name: Windows py3.9 all deps online + os: windows-latest + python: '3.9' + toxenv: py39-test-alldeps-online + toxargs: -v + toxposargs: -v --durations=50 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + - name: Install Python dependencies + run: python -m pip install --upgrade tox + - name: Run tests + run: tox ${{ matrix.toxargs }} -e ${{ matrix.toxenv }} -- ${{ matrix.toxposargs }} From 0a6502b02147d67904de48e9158d45f7dd994d00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Brigitta=20Sip=C5=91cz?= Date: Fri, 14 Jun 2024 21:47:50 -0700 Subject: [PATCH 25/35] MAINT: skipping bigdata marked tests in CI --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index fb65bd8974..16eab3e18a 100644 --- a/tox.ini +++ b/tox.ini @@ -22,7 +22,7 @@ description = run tests setenv = PYTEST_ARGS = '' - online: PYTEST_ARGS = --remote-data=any --reruns=1 --reruns-delay 10 + online: PYTEST_ARGS = --remote-data=any --reruns=1 --reruns-delay 10 -m "not bigdata" devdeps: PIP_EXTRA_INDEX_URL = https://pypi.anaconda.org/scientific-python-nightly-wheels/simple https://pypi.anaconda.org/astropy/simple https://pypi.anaconda.org/liberfa/simple deps = From 75d15f2f30aec0f43067bc5159ac1ad6cebb6606 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Brigitta=20Sip=C5=91cz?= Date: Fri, 14 Jun 2024 21:50:44 -0700 Subject: [PATCH 26/35] TST: marking a 800+Mb alma test bigdata --- astroquery/alma/tests/test_alma_remote.py | 1 + 1 file changed, 1 insertion(+) diff --git a/astroquery/alma/tests/test_alma_remote.py b/astroquery/alma/tests/test_alma_remote.py index b9258afcaf..46ce6f91a2 100644 --- a/astroquery/alma/tests/test_alma_remote.py +++ b/astroquery/alma/tests/test_alma_remote.py @@ -165,6 +165,7 @@ def test_data_proprietary(self, alma): with pytest.raises(AttributeError): alma.is_proprietary('uid://NON/EXI/STING') + @pytest.mark.bigdata def test_retrieve_data(self, tmp_path, alma): """ Regression test for issue 2490 (the retrieval step will simply fail if From 659ae85caea8ed6d089619e9aec831c1e1e6c7f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Brigitta=20Sip=C5=91cz?= Date: Mon, 17 Jun 2024 21:18:57 -0700 Subject: [PATCH 27/35] MAINT: adding doctesting dependency for cloud example --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index f30f7b8011..ac99c4eb69 100644 --- a/setup.cfg +++ b/setup.cfg @@ -148,6 +148,7 @@ test= matplotlib pytest-dependency pytest-rerunfailures + fsspec[http] docs= matplotlib sphinx-astropy>=1.5 From 9c155c9a8733d0e0b91ce2df3af4315f9ebbe6be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Brigitta=20Sip=C5=91cz?= Date: Mon, 17 Jun 2024 21:19:25 -0700 Subject: [PATCH 28/35] TST: don't test defunct module --- docs/ipac/irsa/sha/sha.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/ipac/irsa/sha/sha.rst b/docs/ipac/irsa/sha/sha.rst index 40074bff02..9bd923a3d1 100644 --- a/docs/ipac/irsa/sha/sha.rst +++ b/docs/ipac/irsa/sha/sha.rst @@ -1,5 +1,7 @@ :orphan: +.. doctest-skip-all + .. _astroquery.ipac.irsa.sha: From aec4422f5f9fc455a2446c9c65c005606a3ff3f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Brigitta=20Sip=C5=91cz?= Date: Mon, 17 Jun 2024 21:19:47 -0700 Subject: [PATCH 29/35] DOC: minor doc example fixes --- docs/ipac/irsa/irsa.rst | 2 +- docs/ipac/ned/ned.rst | 7 +++-- docs/ipac/nexsci/nasa_exoplanet_archive.rst | 29 +++++++++++++++++++-- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/docs/ipac/irsa/irsa.rst b/docs/ipac/irsa/irsa.rst index e231e9a89f..af5d70927b 100644 --- a/docs/ipac/irsa/irsa.rst +++ b/docs/ipac/irsa/irsa.rst @@ -244,7 +244,7 @@ will return a `~astropy.table.Table`: >>> from astroquery.ipac.irsa import Irsa >>> Irsa.list_collections() -
+
collection object --------------------- diff --git a/docs/ipac/ned/ned.rst b/docs/ipac/ned/ned.rst index 3049a73d71..bb1a4abee0 100644 --- a/docs/ipac/ned/ned.rst +++ b/docs/ipac/ned/ned.rst @@ -149,7 +149,6 @@ These queries can be used to retrieve all objects that appear in the specified 34 UGC 12149 340.28163 ... 8 0 35 MRK 0522 345.07954 ... 4 0 36 NGC 7674 351.98635 ... 8 0 - Length = 36 rows Image and Spectra Queries @@ -272,13 +271,13 @@ If you are repeatedly getting failed queries, or bad/out-of-date results, try cl .. code-block:: python - >>> from astroquery.ned import Ned + >>> from astroquery.ipac.ned import Ned >>> Ned.clear_cache() -If this function is unavailable, upgrade your version of astroquery. +If this function is unavailable, upgrade your version of astroquery. The ``clear_cache`` function was introduced in version 0.4.7.dev8479. - + Reference/API ============= diff --git a/docs/ipac/nexsci/nasa_exoplanet_archive.rst b/docs/ipac/nexsci/nasa_exoplanet_archive.rst index aff0f55fa8..e4e4977b84 100644 --- a/docs/ipac/nexsci/nasa_exoplanet_archive.rst +++ b/docs/ipac/nexsci/nasa_exoplanet_archive.rst @@ -94,19 +94,44 @@ A list of accessible tables can be found in the ``TAP_TABLES`` attribute: >>> from astroquery.ipac.nexsci.nasa_exoplanet_archive import NasaExoplanetArchive >>> NasaExoplanetArchive.TAP_TABLES ['spectra', + 'TD', + 'pscomppars', 'superwasptimeseries', 'kelttimeseries', 'DI_STARS_EXEP', + 'stellarhosts', 'transitspec', 'emissionspec', 'ps', - 'pscomppars', 'keplernames', 'k2names', + 'toi', + 'CUMULATIVE', + 'Q1_Q6_KOI', + 'Q1_Q8_KOI', + 'Q1_Q12_KOI', + 'Q1_Q16_KOI', + 'Q1_Q17_DR24_KOI', + 'Q1_Q17_DR25_KOI', + 'Q1_Q17_DR25_SUP_KOI', + 'Q1_Q12_TCE', + 'Q1_Q16_TCE', + 'Q1_Q17_DR24_TCE', + 'Q1_Q17_DR25_TCE', + 'stellarhosts', 'ukirttimeseries', 'ml', 'object_aliases', - 'k2pandc'] + 'k2pandc', + 'K2TARGETS', + 'KEPLERTIMESERIES', + 'KEPLERSTELLAR', + 'Q1_Q12_KS', + 'Q1_Q16_KS', + 'Q1_Q17_DR24_KS', + 'Q1_Q17_DR25_KS', + 'Q1_Q17_DR25_SUP_KS'] + Example queries From 499053d618bb6b6d9c80ec7ba294da7af5fdf71b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Brigitta=20Sip=C5=91cz?= Date: Mon, 17 Jun 2024 22:21:14 -0700 Subject: [PATCH 30/35] MAINT: fix OGLE url --- astroquery/ogle/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/astroquery/ogle/__init__.py b/astroquery/ogle/__init__.py index fcb002cd55..d1636fd53b 100644 --- a/astroquery/ogle/__init__.py +++ b/astroquery/ogle/__init__.py @@ -7,7 +7,7 @@ This package is for querying interstellar extinction toward the Galactic bulge from OGLE-III data -`hosted at. `_ +`hosted at. `_ Note: If you use the data from OGLE please refer to the publication by Nataf et al. @@ -21,7 +21,7 @@ class Conf(_config.ConfigNamespace): Configuration parameters for `astroquery.ogle`. """ server = _config.ConfigItem( - ['http://ogle.astrouw.edu.pl/cgi-ogle/getext.py'], + ['https://ogle.astrouw.edu.pl/cgi-ogle/getext.py'], 'Name of the OGLE mirror to use.') timeout = _config.ConfigItem( 60, From 9c45a4d21d94c17f293f5d160e2c40d2ed8f97dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Brigitta=20Sip=C5=91cz?= Date: Mon, 17 Jun 2024 22:24:36 -0700 Subject: [PATCH 31/35] DOC: add changelog --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 63bf0c0980..042102723f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -25,6 +25,12 @@ linelists.cdms - Fix result parsing incompatibility with astropy 6.1 on Windows systems. [#3008] +ogle +^^^^ + +- Change URL to https and thus making the module functional again. [#3048] + + splatalogue ^^^^^^^^^^^ From b82d57beeb24870d4a33fd912d394817fb0f6e0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Brigitta=20Sipo=CC=8Bcz?= Date: Mon, 17 Jun 2024 22:56:31 -0700 Subject: [PATCH 32/35] MAINT: fixing changed astropy API for test --- astroquery/vo_conesearch/tests/test_conesearch.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/astroquery/vo_conesearch/tests/test_conesearch.py b/astroquery/vo_conesearch/tests/test_conesearch.py index a1672f7580..8e572d5898 100644 --- a/astroquery/vo_conesearch/tests/test_conesearch.py +++ b/astroquery/vo_conesearch/tests/test_conesearch.py @@ -13,7 +13,6 @@ from astropy import units as u from astropy.coordinates import ICRS, SkyCoord from astropy.io.votable.exceptions import W25 -from astropy.io.votable.tree import Table as VOTable from astropy.table import Table from astropy.utils.data import get_pkg_data_filename from astropy.utils import data @@ -24,6 +23,14 @@ from ..exceptions import VOSError, ConeSearchError from ...exceptions import NoResultsWarning + +try: + # Workaround astropy deprecation, remove try/except once >=6.0 is required + from astropy.io.votable.tree import TableElement as VOTable +except ImportError: + from astropy.io.votable.tree import Table as VOTable + + __doctest_skip__ = ['*'] From 722ff43615733d9487764dae583784ad8dee65d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Brigitta=20Sip=C5=91cz?= Date: Mon, 17 Jun 2024 22:57:53 -0700 Subject: [PATCH 33/35] MAINT: fix python deprecation --- docs/esa/iso/iso.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/esa/iso/iso.rst b/docs/esa/iso/iso.rst index 93421565c2..99c805b69f 100644 --- a/docs/esa/iso/iso.rst +++ b/docs/esa/iso/iso.rst @@ -88,7 +88,7 @@ provided by this service, see section 'Getting Tables Details'. >>> from astroquery.esa.iso import ISO >>> import tarfile >>> - >>> ISO.download_data('80000203', retrieval_type="OBSERVATION", + >>> ISO.download_data("80000203", retrieval_type="OBSERVATION", ... product_level="DEFAULT_DATA_SET", ... filename="80000203", verbose=True) INFO: https://nida.esac.esa.int/nida-sl-tap/data?retrieval_type=OBSERVATION&DATA_RETRIEVAL_ORIGIN=astroquery&tdt=80000203&product_level=DEFAULT_DATA_SET [astroquery.esa.iso.core] @@ -102,7 +102,7 @@ provided by this service, see section 'Getting Tables Details'. -rw-r--r-- idaops/0 14400 2005-12-23 11:02:55 ././ISO1659972236/EXTRAKON//psph80000203.fits -rw-r--r-- idaops/0 5599 2005-12-23 11:02:55 ././ISO1659972236/EXTRAKON//ppch80000203.gif -rw-r--r-- idaops/0 266240 2005-12-23 11:02:54 ././ISO1659972236/EXTRAKON//C10180000203.tar - >>> tar.extractall() + >>> tar.extractall(filter="fully_trusted") >>> tar.close() 'download_data' method invokes the data download of files from the ISO Data Archive, using the @@ -409,7 +409,7 @@ If you are repeatedly getting failed queries, or bad/out-of-date results, try cl >>> from astroquery.esa.iso import ISO >>> ISO.clear_cache() -If this function is unavailable, upgrade your version of astroquery. +If this function is unavailable, upgrade your version of astroquery. The ``clear_cache`` function was introduced in version 0.4.7.dev8479. From c34d6c69aa329dc3bb3c29ea11b27f1889e4ff51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Brigitta=20Sip=C5=91cz?= Date: Mon, 17 Jun 2024 23:00:18 -0700 Subject: [PATCH 34/35] TST: Catch one more warning in test --- astroquery/vo_conesearch/tests/test_conesearch.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/astroquery/vo_conesearch/tests/test_conesearch.py b/astroquery/vo_conesearch/tests/test_conesearch.py index 8e572d5898..48f1bb51e1 100644 --- a/astroquery/vo_conesearch/tests/test_conesearch.py +++ b/astroquery/vo_conesearch/tests/test_conesearch.py @@ -124,10 +124,11 @@ def test_sky_coord_classic(self): def test_timeout_classic(self): """Test timed out query.""" with pytest.warns(W25, match='timed out'): - with conf.set_temp('timeout', 1e-6): - result = conesearch.conesearch( - SCS_CENTER, SCS_RADIUS, cache=False, - verbose=self.verbose, catalog_db=self.url) + with pytest.warns(NoResultsWarning): + with conf.set_temp('timeout', 1e-6): + result = conesearch.conesearch( + SCS_CENTER, SCS_RADIUS, cache=False, + verbose=self.verbose, catalog_db=self.url) assert result is None def test_searches_classic(self): From 02f9072e01821019fb17709e26e02b9e1bba0a80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Brigitta=20Sip=C5=91cz?= Date: Mon, 17 Jun 2024 23:11:43 -0700 Subject: [PATCH 35/35] TST: various minor doctest fixes --- docs/cadc/cadc.rst | 4 +-- docs/esa/xmm_newton/xmm_newton.rst | 6 ++-- docs/gaia/gaia.rst | 48 ++++++++++++++++-------------- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/docs/cadc/cadc.rst b/docs/cadc/cadc.rst index 511d165135..b04e5ac9d5 100644 --- a/docs/cadc/cadc.rst +++ b/docs/cadc/cadc.rst @@ -26,7 +26,7 @@ these collections: >>> from astroquery.cadc import Cadc >>> cadc = Cadc() >>> for collection, details in sorted(cadc.get_collections().items()): - ... print(f'{collection} : {details}') + ... print(f'{collection} : {details}') # doctest: +IGNORE_OUTPUT ... APASS : {'Description': 'The APASS collection at the CADC', 'Bands': ['Optical', 'Infrared|Optical', '']} BLAST : {'Description': 'The BLAST collection at the CADC', 'Bands': ['', 'Millimeter']} @@ -313,8 +313,6 @@ To get a list of table objects: caom2.HarvestState caom2.SIAv1 ivoa.ObsCore - ivoa.ObsFile - ivoa.ObsPart tap_schema.schemas tap_schema.tables tap_schema.columns diff --git a/docs/esa/xmm_newton/xmm_newton.rst b/docs/esa/xmm_newton/xmm_newton.rst index 781fa971c1..08ee3e1c49 100644 --- a/docs/esa/xmm_newton/xmm_newton.rst +++ b/docs/esa/xmm_newton/xmm_newton.rst @@ -122,9 +122,9 @@ stored in the file 'results10.csv'. The result of this query can be printed by d INFO: Retrieving tables... [astroquery.utils.tap.core] INFO: Parsing tables... [astroquery.utils.tap.core] INFO: Done. [astroquery.utils.tap.core] - ['tap_config.coord_sys', 'tap_config.properties', 'tap_schema.columns', + ['public.dual', 'tap_config.coord_sys', 'tap_config.properties', 'tap_schema.columns', 'tap_schema.key_columns', 'tap_schema.keys', 'tap_schema.schemas', - 'tap_schema.tables', 'xsa.dual', 'xsa.v_all_observations', 'xsa.v_epic_source', + 'tap_schema.tables', 'xsa.v_all_observations', 'xsa.v_epic_source', 'xsa.v_epic_source_cat', 'xsa.v_epic_xmm_stack_cat', 'xsa.v_exposure', 'xsa.v_instrument_mode', 'xsa.v_om_source', 'xsa.v_om_source_cat', 'xsa.v_proposal', 'xsa.v_proposal_observation_info', 'xsa.v_publication', @@ -205,7 +205,7 @@ If you are repeatedly getting failed queries, or bad/out-of-date results, try cl >>> from astroquery.esa.xmm_newton import XMMNewton >>> XMMNewton.clear_cache() -If this function is unavailable, upgrade your version of astroquery. +If this function is unavailable, upgrade your version of astroquery. The ``clear_cache`` function was introduced in version 0.4.7.dev8479. diff --git a/docs/gaia/gaia.rst b/docs/gaia/gaia.rst index d9fb4f996a..40d492eee8 100644 --- a/docs/gaia/gaia.rst +++ b/docs/gaia/gaia.rst @@ -105,7 +105,7 @@ degrees around an specific point in RA/Dec coordinates. The results are sorted b >>> r = Gaia.query_object_async(coordinate=coord, width=width, height=height) INFO: Query finished. [astroquery.utils.tap.core] >>> r.pprint(max_lines=12, max_width=130) - dist solution_id DESIGNATION ... ebpminrp_gspphot_upper libname_gspphot + dist solution_id designation ... ebpminrp_gspphot_upper libname_gspphot ... mag --------------------- ------------------- ---------------------------- ... ---------------------- --------------- 0.0026043272506261527 1636148068921376768 Gaia DR3 6636090334814214528 ... -- @@ -126,7 +126,7 @@ Queries return a limited number of rows controlled by ``Gaia.ROW_LIMIT``. To cha >>> r = Gaia.query_object_async(coordinate=coord, width=width, height=height) INFO: Query finished. [astroquery.utils.tap.core] >>> r.pprint(max_width=140) - dist solution_id DESIGNATION ... ebpminrp_gspphot_lower ebpminrp_gspphot_upper libname_gspphot + dist solution_id designation ... ebpminrp_gspphot_lower ebpminrp_gspphot_upper libname_gspphot ... mag mag --------------------- ------------------- ---------------------------- ... ---------------------- ---------------------- --------------- 0.0026043272506261527 1636148068921376768 Gaia DR3 6636090334814214528 ... -- -- @@ -146,7 +146,7 @@ To return an unlimited number of rows set ``Gaia.ROW_LIMIT`` to -1. >>> r = Gaia.query_object_async(coordinate=coord, width=width, height=height) INFO: Query finished. [astroquery.utils.tap.core] >>> r.pprint(max_lines=12, max_width=140) - dist solution_id DESIGNATION ... ebpminrp_gspphot_lower ebpminrp_gspphot_upper libname_gspphot + dist solution_id designation ... ebpminrp_gspphot_lower ebpminrp_gspphot_upper libname_gspphot ... mag mag --------------------- ------------------- ---------------------------- ... ---------------------- ---------------------- --------------- 0.0026043272506261527 1636148068921376768 Gaia DR3 6636090334814214528 ... -- -- @@ -176,7 +176,7 @@ radius argument. INFO: Query finished. [astroquery.utils.tap.core] >>> r = j.get_results() >>> r.pprint() - solution_id DESIGNATION ... dist + solution_id designation ... dist ... ------------------- ---------------------------- ... --------------------- 1636148068921376768 Gaia DR3 6636090334814214528 ... 0.0026043272506261527 @@ -207,15 +207,15 @@ To load only table names metadata (TAP+ capability): INFO: Done. [astroquery.utils.tap.core] >>> for table in tables: ... print(table.get_qualified_name()) - external.external.apassdr9 - external.external.catwise2020 - external.external.gaiadr2_astrophysical_parameters - external.external.gaiadr2_geometric_distance - external.external.gaiaedr3_distance + external.apassdr9 + external.catwise2020 + external.gaiadr2_astrophysical_parameters + external.gaiadr2_geometric_distance + external.gaiaedr3_distance ... - tap_schema.tap_schema.keys - tap_schema.tap_schema.schemas - tap_schema.tap_schema.tables + tap_schema.keys + tap_schema.schemas + tap_schema.tables To load all tables metadata (TAP compatible): @@ -227,11 +227,12 @@ To load all tables metadata (TAP compatible): INFO: Parsing tables... [astroquery.utils.tap.core] INFO: Done. [astroquery.utils.tap.core] >>> print(tables[0]) - TAP Table name: external.external.apassdr9 + TAP Table name: external.apassdr9 Description: The AAVSO Photometric All-Sky Survey - Data Release 9 This publication makes use of data products from the AAVSO Photometric All Sky Survey (APASS). Funded by the Robert Martin Ayers Sciences Fund and the National Science Foundation. Original catalogue released by Henden et al. 2015 AAS Meeting #225, id.336.16. Data retrieved using the VizieR catalogue access tool, CDS, Strasbourg, France. The original description of the VizieR service was published in A&AS 143, 23. VizieR catalogue II/336. + Size (bytes): 22474547200 Num. columns: 25 @@ -242,8 +243,9 @@ To load only a table (TAP+ capability): >>> from astroquery.gaia import Gaia >>> gaiadr3_table = Gaia.load_table('gaiadr3.gaia_source') >>> print(gaiadr3_table) - TAP Table name: gaiadr3.gaiadr3.gaia_source + TAP Table name: gaiadr3.gaia_source Description: This table has an entry for every Gaia observed source as published with this data release. It contains the basic source parameters, in their final state as processed by the Gaia Data Processing and Analysis Consortium from the raw data coming from the spacecraft. The table is complemented with others containing information specific to certain kinds of objects (e.g.~Solar--system objects, non--single stars, variables etc.) and value--added processing (e.g.~astrophysical parameters etc.). Further array data types (spectra, epoch measurements) are presented separately via Datalink resources. + Size (bytes): 3646930329600 Num. columns: 152 @@ -411,7 +413,7 @@ Query without saving results in a file: INFO: Query finished. [astroquery.utils.tap.core] >>> r = job.get_results() >>> print(r) - DESIGNATION ra dec + designation ra dec deg deg ---------------------- ------------------ -------------------- Gaia DR3 4295806720 44.99615537864534 0.005615226341865997 @@ -524,15 +526,15 @@ To obtain a list of the tables shared to a user type the following:: INFO: Done. [astroquery.utils.tap.core] >>> for table in (tables): ... print(table.get_qualified_name()) - external.external.apassdr9 - external.external.gaiadr2_astrophysical_parameters - external.external.gaiadr2_geometric_distance - external.external.gaiaedr3_distance + external.apassdr9 + external.gaiadr2_astrophysical_parameters + external.gaiadr2_geometric_distance + external.gaiaedr3_distance ... ... ... - tap_schema.tap_schema.key_columns - tap_schema.tap_schema.keys - tap_schema.tap_schema.schemas - tap_schema.tap_schema.tables + tap_schema.key_columns + tap_schema.keys + tap_schema.schemas + tap_schema.tables 2.3. Uploading table to user space ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^