# Licensed under a 3-clause BSD style license - see LICENSE.rst """Can `astropy.io.fits.open` access (remote) data using the fsspec package?""" import numpy as np import pytest from numpy.testing import assert_allclose, assert_array_equal from astropy.io import fits from astropy.nddata import Cutout2D from astropy.utils.compat.optional_deps import HAS_FSSPEC, HAS_S3FS from astropy.utils.data import get_pkg_data_filename if HAS_FSSPEC: import fsspec @pytest.mark.skipif(not HAS_FSSPEC, reason="requires fsspec") def test_fsspec_local(): """Can we use fsspec to read a local file?""" fn = get_pkg_data_filename("data/test0.fits") with fits.open(fn) as hdulist_classic: with fits.open(fn, use_fsspec=True) as hdulist_fsspec: assert_array_equal(hdulist_classic[2].data, hdulist_fsspec[2].data) assert_array_equal( hdulist_classic[2].section[3:5], hdulist_fsspec[2].section[3:5] ) @pytest.mark.skipif(not HAS_FSSPEC, reason="requires fsspec") def test_fsspec_local_write(tmp_path): """Can we write to a local file that was opened using fsspec?""" fn = get_pkg_data_filename("data/test0.fits") fn_tmp = tmp_path / "tmp.fits" with fits.open(fn, use_fsspec=True) as hdul: # writing to a section is never allowed with pytest.raises(TypeError): hdul[1].section[0, 0] = -999 # however writing to .data should work hdul[1].data[2, 3] = -999 assert hdul[1].data[2, 3] == -999 hdul.writeto(fn_tmp) # Is the new value present when we re-open the file? with fits.open(fn_tmp) as hdul: assert hdul[1].data[2, 3] == -999 @pytest.mark.skipif(not HAS_FSSPEC, reason="requires fsspec") def test_fsspec_cutout2d(): """Does Cutout2D work with data loaded lazily using fsspec and .section?""" fn = get_pkg_data_filename("data/test0.fits") with fits.open(fn, use_fsspec=True) as hdul: position = (10, 20) size = (2, 3) cutout1 = Cutout2D(hdul[1].data, position, size) cutout2 = Cutout2D(hdul[1].section, position, size) assert_allclose(cutout1.data, cutout2.data) @pytest.mark.skipif(not HAS_FSSPEC, reason="requires fsspec") def test_fsspec_compressed(): """Does fsspec support compressed data correctly?""" # comp.fits[1] is a compressed image with shape (440, 300) fn = get_pkg_data_filename("data/comp.fits") with fits.open(fn, use_fsspec=True) as hdul: # The .data attribute should work as normal assert hdul[1].data[0, 0] == 7 # And the .section attribute should work too assert hdul[1].section[0, 0] == 7 @pytest.mark.remote_data class TestFsspecRemote: """Test obtaining cutouts from FITS files via HTTP (from MAST) and S3 (from Amazon).""" def setup_class(self): # The test file (ibxl50020_jif.fits) is a Hubble jitter FITS file (*.jif) # rather than a real image, because jitter files are less likely to # change due to reprocessing. self.http_url = "https://mast.stsci.edu/api/v0.1/Download/file/?uri=mast:HST/product/ibxl50020_jif.fits" self.s3_uri = "s3://stpubdata/hst/public/ibxl/ibxl50020/ibxl50020_jif.fits" # Random slice was selected for testing: self.slice = (slice(31, 33), slice(27, 30)) # The expected cutout array below was obtained by downloading the URIs # listed above to a local path and and executing: # with fits.open(local_path) as hdul: # expected_cutout = hdul[1].data[31:33, 27:30] self.expected_cutout = np.array([[24, 88, 228], [35, 132, 305]], dtype=np.int32) @pytest.mark.skipif(not HAS_FSSPEC, reason="requires fsspec") def test_fsspec_http(self): """Can we use fsspec to open a remote FITS file via http?""" with fits.open(self.http_url, use_fsspec=True) as hdul: # Do we retrieve the expected array? assert_array_equal(hdul[1].section[self.slice], self.expected_cutout) # The file has multiple extensions which are not yet downloaded; # the repr and string representation should reflect this. assert "partially read" in repr(hdul) assert "partially read" in str(hdul) # Can the user also pass an fsspec file object directly to fits open? with fsspec.open(self.http_url) as fileobj: with fits.open(fileobj) as hdul2: assert_array_equal(hdul2[1].section[self.slice], self.expected_cutout) assert "partially read" in repr(hdul) assert "partially read" in str(hdul) @pytest.mark.skipif(not HAS_S3FS, reason="requires s3fs") def test_fsspec_s3(self): """Can we use fsspec to open a FITS file in a public Amazon S3 bucket?""" with fits.open( self.s3_uri, fsspec_kwargs={"anon": True} ) as hdul: # s3:// paths should default to use_fsspec=True # Do we retrieve the expected array? assert_array_equal(hdul[1].section[self.slice], self.expected_cutout) # The file has multiple extensions which are not yet downloaded; # the repr and string representation should reflect this. assert "partially read" in repr(hdul) assert "partially read" in str(hdul) # Can the user also pass an fsspec file object directly to fits open? with fsspec.open(self.s3_uri, anon=True) as fileobj: with fits.open(fileobj) as hdul2: assert_array_equal(hdul2[1].section[self.slice], self.expected_cutout) assert "partially read" in repr(hdul) assert "partially read" in str(hdul)