-
-
Notifications
You must be signed in to change notification settings - Fork 441
MAST: Adding read_product function to mast.observations #3610
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 8 commits
f00645a
69f719b
8e4240d
ee3d10a
6c1c2d8
7cf4e22
6925301
aa5026e
1afbc52
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,6 +14,7 @@ | |
|
|
||
| import astropy.coordinates as coord | ||
| import astropy.units as u | ||
| from astropy.io import fits | ||
| import numpy as np | ||
| from astropy.table import Row, Table, vstack | ||
| from astropy.utils.decorators import deprecated_renamed_argument | ||
|
|
@@ -41,6 +42,16 @@ | |
| except ImportError: | ||
| pass | ||
|
|
||
| try: | ||
| import fsspec | ||
| except ImportError: | ||
| fsspec = None | ||
|
|
||
| try: | ||
| import asdf | ||
| except ImportError: | ||
| asdf = None | ||
|
|
||
| __all__ = ['Observations', 'ObservationsClass', 'MastClass', 'Mast'] | ||
|
|
||
| CLOUD_DISABLED_MESSAGE = ( | ||
|
|
@@ -1235,6 +1246,72 @@ def get_unique_product_list(self, observations, *, batch_size=500): | |
| log.info("To return all products, use `Observations.get_product_list`") | ||
| return unique_products | ||
|
|
||
| # TODO: Need to inlcude way to parse if it is a MAST on prem URL and handle the streaming of that | ||
| def read_product(self, product_path, ignore_unrecognized=True, **kwargs): | ||
| """ | ||
| Read a product from Open S3 bucket to memory. Currently supports FITS and ASDF product types only. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| product_path: str | ||
| URI to the product in the STScI S3 open data bucket. | ||
| ignore_unrecognized: bool | ||
| Tells asdf.open() to include or ignore warnings from unrecognized asdf tags. Defaults to True | ||
| **kwargs | ||
| Additional keyword arguments passed to the underlying file reader: | ||
| - For FITS files: forwarded to ``astropy.io.fits.open``. | ||
| Common options include ``memmap``, ``mode``, etc. | ||
| - Ignored for ASDF files (except for future extension if needed). | ||
|
|
||
| Returns | ||
| ------- | ||
| object | ||
| FITS or ASDF object for the given data product. | ||
| """ | ||
| # Checks if a path is empty or None. | ||
| if not product_path or not str(product_path).strip(): | ||
| raise ValueError("No product path provided") | ||
|
|
||
| # Forces the path to be lowercase for the extension checks. This is only used for the checks | ||
| path = str(product_path).lower() | ||
|
|
||
| # Checks users enviornment for fsspec, required for both fits and asdf | ||
| if fsspec is None: | ||
| raise ImportError('The "fsspec" package is required to read products directly from a URI. ' | ||
| 'Please install it with `pip install fsspec`.') | ||
|
|
||
| # Logic for reading FITS files | ||
| if path.endswith((".fits", ".fits.gz")): | ||
| try: | ||
| data_product = fits.open(product_path, fsspec_kwargs={"anon": True}, **kwargs) | ||
| log.info(f"Loaded: {product_path}") | ||
| return data_product | ||
| except Exception as e: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you be more specific about the exception here? Or if you want to catch generally, add a comment explaining why. |
||
| raise RuntimeError(f"Failed to open FITS File: {product_path} {e}") | ||
|
|
||
| # Logic for reading ASDF files | ||
| elif path.endswith(".asdf"): | ||
| if fsspec is None: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since this package is needed for both FITS and ASDF, can you check that it exists in the environment outside of this block so that it's not there twice? |
||
| raise ImportError('The "fsspec" package is required to read products directly from a URI. ' | ||
| 'Please install it with `pip install fsspec` or install astroquery with ' | ||
| 'optional dependencies using `pip install astroquery[all]`.') | ||
| if asdf is None: | ||
| raise ImportError('The "asdf" package is required to read ASDF files. Please install it with ' | ||
| '`pip install asdf` or install astroquery with optional dependencies using ' | ||
| '`pip install astroquery[all]`.') | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd also like to raise warnings if the optional packages are not found. Something like this: |
||
| # Attempts to open the asdf files | ||
| try: | ||
| f = fsspec.open(product_path, "rb", anon=True).open() | ||
| data_product = asdf.open(f, ignore_unrecognized_tag=ignore_unrecognized) | ||
| log.info(f"Loaded: {product_path}") | ||
| return data_product | ||
| except Exception as e: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use a more specific exception here if you can |
||
| raise RuntimeError(f"Failed to open ASDF File: {product_path} {e}") | ||
|
|
||
| else: | ||
| raise ValueError(f"Unsupported product type: {product_path}") | ||
|
|
||
|
|
||
| @async_to_sync | ||
| class MastClass(MastQueryWithLogin): | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1372,6 +1372,72 @@ def test_observations_disable_cloud_dataset(patch_boto3): | |
| assert Observations._cloud_enabled_explicitly is False | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def mock_fits_open(mocker): | ||
| """Mock fits.open to return a valid HDUList without network access.""" | ||
| return mocker.patch("astropy.io.fits.open", return_value=fits.HDUList([fits.PrimaryHDU()])) | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def mock_asdf_open(mocker): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you add: tests that use this fixture will automatically skip if |
||
| return mocker.patch( | ||
| "asdf.open", | ||
| return_value=MagicMock(name="AsdfFile"), | ||
| ) | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def mock_fsspec_open(mocker): | ||
| fake = mocker.Mock() | ||
| fake.open.return_value = "mock_asdf_file_object" | ||
| return mocker.patch("fsspec.open", return_value=fake) | ||
|
|
||
|
|
||
| def test_observations_read_product_fits(mock_fits_open): | ||
| s3_fits_path = "s3://mock_fits_path.fits" | ||
| result = Observations.read_product(s3_fits_path) | ||
|
|
||
| mock_fits_open(s3_fits_path, fsspec_kwargs={"anon": True}) | ||
| assert result is mock_fits_open.return_value | ||
|
|
||
|
|
||
| def test_observations_read_product_asdf(mock_asdf_open, mock_fsspec_open): | ||
| s3_asdf_path = "s3://fake_asdf_path.asdf" | ||
| result = Observations.read_product(s3_asdf_path) | ||
|
|
||
| mock_asdf_open("mock_asdf_file_object") | ||
| assert result is mock_asdf_open.return_value | ||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| "product_path, expected_exception, match", | ||
| [ | ||
| ("", ValueError, "No product path provided"), | ||
| (" ", ValueError, "No product path provided"), | ||
| (None, ValueError, "No product path provided"), | ||
| ("unsupported_ex.txt", ValueError, "Unsupported product type"), | ||
| ], | ||
| ) | ||
| def test_observations_read_product_invalid_inputs(product_path, expected_exception, match): | ||
| with pytest.raises(expected_exception, match=match): | ||
| Observations.read_product(product_path) | ||
|
|
||
|
|
||
| def test_observations_read_product_fsspec_missing(monkeypatch): | ||
| # Forces fsspec to be None | ||
| monkeypatch.setitem(Observations.read_product.__globals__, "fsspec", None) | ||
|
|
||
| with pytest.raises(ImportError, match="fsspec"): | ||
| Observations.read_product("file.fits") | ||
|
|
||
|
|
||
| def test_observations_read_product_asdf_missing(monkeypatch): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you combine this test with the one before it using parametrization? |
||
| # Forces asdf to be None | ||
| monkeypatch.setitem(Observations.read_product.__globals__, "asdf", None) | ||
|
|
||
| with pytest.raises(ImportError, match="asdf"): | ||
| Observations.read_product("file.asdf") | ||
|
|
||
| ###################### | ||
| # CatalogClass tests # | ||
| ###################### | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -629,3 +629,15 @@ remain fully cloud-based. | |
| COMPLETE | ||
| COMPLETE | ||
| COMPLETE | ||
|
|
||
| Streaming Data Products from S3 to memory | ||
| ----------------------------------------- | ||
| If instead of downloading you would like to load an S3 URI directly to memory, you can use the `~astroquery.mast.ObservationsClass.read_product` method. | ||
| This function supports FITS and ASDF data products and will automatically parse the file for the suffix and load it to memory using `~astropy.io.fits.open` or `~asdf.open`. | ||
| For ASDF data products, additional packages may be required such as ``roman_datamodels``, ``gwcs`` and ``lz4``. To install astroquery with all these, and all other optional dependencies, use ``pip install astroquery[all]``. | ||
|
|
||
| .. doctest-remote-data:: | ||
|
|
||
| >>> from astroquery.mast import Observations | ||
| >>> fits_product = Observations.read_product(product_path="s3://stpubdata/hst/public/u9o4/u9o40504m/u9o40504m_c3m.fits") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you add a |
||
| >>> asdf_product = Observations.read_product(product_path="s3://stpubdata/roman/nexus/soc_simulations/tutorial_data/r0003201001001001004_0001_wfi01_f106_cal.asdf", ignore_unrecognized=True) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.