Source code for astroquery.esa.emds.core

# Licensed under a 3-clause BSD style license - see LICENSE.rst
"""
==========================================
Multi-Mission Data Services (EMDS)
==========================================

European Space Astronomy Centre (ESAC)
European Space Agency (ESA)

"""

from . import conf
from astroquery.utils import commons
from html import unescape
import os
import warnings
import astroquery.esa.utils.utils as esautils
from astroquery.esa.utils import EsaTap, download_file

__all__ = ['Emds', 'EmdsClass']

from ...exceptions import NoResultsWarning


[docs] class EmdsClass(EsaTap): """ EMDS TAP client (multi-mission / multi-schema). EMDS provides access to multiple missions through a single TAP service. Mission data are organised under different TAP schemas. This client offers a small convenience layer to work with mission-specific tables while reusing the standard TAP utilities. """ ESA_ARCHIVE_NAME = "ESA Multi-Mission Data Services (EMDS)" TAP_URL = conf.EMDS_TAP_SERVER LOGIN_URL = conf.EMDS_LOGIN_SERVER LOGOUT_URL = conf.EMDS_LOGOUT_SERVER def __init__(self, auth_session=None, tap_url=None): super().__init__(auth_session=auth_session, tap_url=tap_url) # IMPORTANT: ensure every instance has a config namespace. # Subclasses (missions) can overwrite this with their own module conf. self.conf = conf def _get_obscore_table(self) -> str: """ Return the fully-qualified ObsCore table/view used by this client. Sub-clients override this by providing `conf.OBSCORE_TABLE`. """ table = getattr(self.conf, "OBSCORE_TABLE", None) if not (isinstance(table, str) and table.strip()): raise ValueError( "OBSCORE_TABLE is not configured for this client. " "Please set conf.OBSCORE_TABLE to a fully-qualified name like 'schema.table'." ) return table
[docs] def get_tables(self, *, only_names: bool = False): """ Return the tables available for this mission. By default, only tables belonging to the mission-specific schema(s) are returned. Set ``only_names=True`` to return table names instead of table objects. Parameters ---------- only_names : bool, optional If True, return table names as strings. If False, return table objects. Returns ------- list Table names (str) if ``only_names=True``, otherwise table objects. """ tables = super().get_tables(only_names=only_names) schemas = getattr(self.conf, "DEFAULT_SCHEMAS", "") if not isinstance(schemas, str) or not schemas.strip(): # No schema filtering configured: return all tables return tables # Split and normalize schema names schemas_list = [s.strip() for s in schemas.split(",") if s.strip()] # Build lowercase schema prefixes schema_prefixes = tuple(s.lower() + "." for s in schemas_list) # Check whether a table belongs to one of the schemas def belongs(name: str) -> bool: n = (name or "").lower() return n.startswith(schema_prefixes) if only_names: # Filter table names (strings) return [t for t in tables if belongs(t)] else: # Filter table objects using their 'name' attribute return [ t for t in tables if belongs(getattr(t, "name", "")) ]
[docs] def list_missions(self): """ Retrieve the list of missions available in the EMDS ObsCore view. This method returns the distinct values of the ``obs_collection`` field from the ``ivoa.ObsCore`` view, where ``obs_collection`` typically identifies the mission or data collection associated with each observation. Returns ------- astropy.table that contains the distinct mission identifiers present in ObsCore. """ query = "SELECT DISTINCT obs_collection FROM ivoa.ObsCore WHERE obs_collection IS NOT NULL" return self.query_tap(query=query)
[docs] def get_observations(self, *, target_name=None, coordinates=None, radius=1.0, columns=None, get_metadata=False, output_file=None, **filters): """ Query the observation catalogue for this mission. This method queries the mission-specific observation catalogue configured for this client and returns observation-level metadata as an Astropy table. Queries can be restricted using a cone search (by target name or coordinates) and additional column-based filters. Parameters ---------- target_name: str, optional Name of the target to be resolved against SIMBAD/NED/VIZIER coordinates: str or SkyCoord, optional coordinates of the center in the cone search radius: float or quantity, optional, default value 1 degree radius in degrees (int, float) or quantity of the cone_search columns : str or list of str, optional, default None Columns from the table to be retrieved. They can be checked using get_metadata=True get_metadata : bool, optional, default False Get the table metadata to verify the columns that can be filtered output_file : str, optional, default None file name where the results are saved. If this parameter is not provided, the jobid is used instead **filters : str, optional, default None Filters to be applied to the search. The column name is the keyword and the value is any value accepted by the column datatype. They will be used to generate the SQL filters for the query. Some examples are described below, where the left side is the parameter defined for this method and the right side the SQL filter generated: obs_collection="EPSA" -> obs_collection = 'EPSA' target_name="AT 2023%" -> target_name ILIKE 'AT 2023%' dataproduct_type=["img", "pha"] -> dataproduct_type = 'img' OR dataproduct_type = 'pha' dataproduct_type=["img", "pha"] -> dataproduct_type IN ('img', 'pha') t_min=(">", 60000) -> t_min > 60000 s_ra=(80, 82) -> s_ra >= 80 AND s_ra <= 82 Returns ------- An astropy.table containing the query results, or the metadata table when ``get_metadata=True`` """ cone_search_filter = None if radius is not None: radius = esautils.get_degree_radius(radius) if target_name and coordinates: raise TypeError("Please use only target or coordinates as " "parameter.") elif target_name: coordinates = esautils.resolve_target(conf.EMDS_TARGET_RESOLVER, self.tap._session, target_name, 'ALL') cone_search_filter = self.create_cone_search_query(coordinates.ra.deg, coordinates.dec.deg, "s_ra", "s_dec", radius) elif coordinates: coord = commons.parse_coordinates(coordinates=coordinates) ra = coord.ra.degree dec = coord.dec.degree cone_search_filter = self.create_cone_search_query(ra, dec, "s_ra", "s_dec", radius) obscore_table = self._get_obscore_table() return self.query_table(table_name=obscore_table, columns=columns, custom_filters=cone_search_filter, get_metadata=get_metadata, async_job=True, output_file=output_file, **filters)
[docs] def get_products(self, *, target_name=None, coordinates=None, radius=1.0, get_metadata=False, **filters): """ Retrieve data products given a Taget Name, coordinates and/or some filters This method queries the mission product catalogue and returns product-level information. It ensures that the ``obs_publisher_did`` and ``access_url`` columns required for downloading products are included in the results. Parameters ---------- target_name: str, optional Name of the target to be resolved against SIMBAD/NED/VIZIER coordinates: str or SkyCoord, optional coordinates of the center in the cone search radius: float or quantity, optional, default value 1 degree radius in degrees (int, float) or quantity of the cone_search get_metadata : bool, optional, default False Get the table metadata to verify the columns that can be filtered **filters : str, optional, default None Filters to be applied to the search. The column name is the keyword and the value is any value accepted by the column datatype. They will be used to generate the SQL filters for the query. Some examples are described below, where the left side is the parameter defined for this method and the right side the SQL filter generated: obs_collection="EPSA" -> obs_collection = 'EPSA' target_name="AT 2023%" -> target_name ILIKE 'AT 2023%' dataproduct_type=["img", "pha"] -> dataproduct_type = 'img' OR dataproduct_type = 'pha' dataproduct_type=["img", "pha"] -> dataproduct_type IN ('img', 'pha') t_min=(">", 60000) -> t_min > 60000 s_ra=(80, 82) -> s_ra >= 80 AND s_ra <= 82 Returns ------- astropy.table.Table """ return self.get_observations(target_name=target_name, coordinates=coordinates, radius=radius, columns=['obs_id', 'obs_publisher_did', 'access_url'], get_metadata=get_metadata, **filters)
[docs] def download_products(self, products, *, path="", cache=False, cache_folder=None, verbose=False, params=None): """ Download all products from a table returned by `get_products()`. Parameters ---------- products : `~astropy.table.Table` Table returned by `get_products()`. The table must contain the ``access_url`` and ``obs_publisher_did`` columns. path : str, optional Local directory where the downloaded files will be stored. If not provided, files are downloaded to the current working directory. Ignored if ``cache=True``. cache : bool, optional If True, store the downloaded files in the Astroquery cache. Default is False. cache_folder : str, optional Subdirectory within the Astroquery cache where files will be stored. Only used if ``cache=True``. verbose : bool, optional If True, print progress messages during download. Default is False. params : dict, optional Additional parameters passed to the HTTP request. Returns ------- list of str List of local file paths for the downloaded products. """ if products is None or len(products) == 0: warnings.warn('There are no products available', NoResultsWarning) return [] if "access_url" not in products.colnames: raise ValueError("Products table must contain an 'access_url' column.") if "obs_publisher_did" not in products.colnames: raise ValueError("Products table must contain an 'obs_publisher_did' column.") if path and not cache: os.makedirs(path, exist_ok=True) downloaded = [] for row in products: url = unescape(row["access_url"]) session = self.tap._session file_path = download_file( url, session, params=params, path=path, cache=cache, cache_folder=cache_folder, verbose=verbose, ) downloaded.append(file_path) return downloaded
Emds = EmdsClass()