@@ -364,6 +364,55 @@ def _query_execute(self, catalog=None, where=None, *,
364364 table .remove_column ('__row' )
365365 return table
366366
367+ def _parse_constraints (self , column_filters ):
368+ """Convert constraints dictionary to ADQL WHERE clause
369+
370+ Parameters
371+ ----------
372+ column_filters : dict
373+ A dictionary of column constraint filters to include in the query.
374+ Each key-value pair will be translated into an ADQL condition.
375+ See `query_region` for details.
376+
377+ Returns
378+ -------
379+ conditions : list
380+ a list of ADQL conditions as str
381+
382+ """
383+ conditions = []
384+ if column_filters is None :
385+ return conditions
386+ for key , value in column_filters .items ():
387+ if isinstance (value , tuple ):
388+ if (
389+ len (value ) == 2
390+ and all (isinstance (v , (int , float )) for v in value )
391+ ):
392+ conditions .append (
393+ f"{ key } BETWEEN { value [0 ]} AND { value [1 ]} "
394+ )
395+ elif (
396+ len (value ) == 2
397+ and value [0 ] in (">" , "<" , ">=" , "<=" )
398+ ):
399+ conditions .append (f"{ key } { value [0 ]} { value [1 ]} " )
400+ elif isinstance (value , list ):
401+ # handle list values: key IN (...)
402+ formatted = []
403+ for v in value :
404+ if isinstance (v , str ):
405+ formatted .append (f"'{ v } '" )
406+ else :
407+ formatted .append (str (v ))
408+ conditions .append (f"{ key } IN ({ ', ' .join (formatted )} )" )
409+ else :
410+ conditions .append (
411+ f"{ key } = '{ value } '"
412+ if isinstance (value , str ) else f"{ key } = { value } "
413+ )
414+ return conditions
415+
367416 @deprecated_renamed_argument (
368417 ('mission' , 'fields' , 'resultmax' , 'entry' , 'coordsys' , 'equinox' ,
369418 'displaymode' , 'action' , 'sortvar' , 'cache' ),
@@ -374,8 +423,8 @@ def _query_execute(self, catalog=None, where=None, *,
374423 True , True , True , False )
375424 )
376425 def query_region (self , position = None , catalog = None , radius = None , * ,
377- spatial = 'cone' , width = None , polygon = None , add_offset = False ,
378- get_query_payload = False , columns = None , cache = False ,
426+ spatial = 'cone' , width = None , polygon = None , column_filters = None ,
427+ add_offset = False , get_query_payload = False , columns = None , cache = False ,
379428 verbose = False , maxrec = None ,
380429 ** kwargs ):
381430 """Queries the HEASARC TAP server around a coordinate and returns a
@@ -411,6 +460,23 @@ def query_region(self, position=None, catalog=None, radius=None, *,
411460 outlining the polygon to search in. It can also be a list of
412461 `astropy.coordinates` object or strings that can be parsed by
413462 `astropy.coordinates.ICRS`.
463+ column_filters : dict
464+ A dictionary of column constraint filters to include in the query.
465+ Each key-value pair will be translated into an ADQL condition.
466+ - For a range query, use a tuple of two values (min, max).
467+ e.g. ``{'flux': (1e-12, 1e-10)}`` translates to
468+ ``flux BETWEEN 1e-12 AND 1e-10``.
469+ - For list values, use a list of values.
470+ e.g. ``{'object_type': ['QSO', 'GALAXY']}`` translates to
471+ ``object_type IN ('QSO', 'GALAXY')``.
472+ - For comparison queries, use a tuple of (operator, value),
473+ where operator is one of '=', '!=', '<', '>', '<=', '>='.
474+ e.g. ``{'magnitude': ('<', 15)}`` translates to ``magnitude < 15``.
475+ - For exact matches, use a single value (str, int, float).
476+ e.g. ``{'object_type': 'QSO'}`` translates to
477+ ``object_type = 'QSO'``.
478+ The keys should correspond to valid column names in the catalog.
479+ Use `list_columns` to see the available columns.
414480 add_offset: bool
415481 If True and spatial=='cone', add a search_offset column that
416482 indicates the separation (in arcmin) between the requested
@@ -457,6 +523,11 @@ def query_region(self, position=None, catalog=None, radius=None, *,
457523 where = ("WHERE CONTAINS(POINT('ICRS',ra,dec),"
458524 f"POLYGON('ICRS',{ ',' .join (coords_str )} ))=1" )
459525 else :
526+ if position is None :
527+ raise InvalidQueryError (
528+ "position is required to for spatial='cone' (default). "
529+ "Use spatial='all-sky' For all-sky searches."
530+ )
460531 coords_icrs = parse_coordinates (position ).icrs
461532 ra , dec = coords_icrs .ra .deg , coords_icrs .dec .deg
462533
@@ -481,6 +552,16 @@ def query_region(self, position=None, catalog=None, radius=None, *,
481552 raise ValueError ("Unrecognized spatial query type. Must be one"
482553 " of 'cone', 'box', 'polygon', or 'all-sky'." )
483554
555+ # handle column filters
556+ if column_filters is not None :
557+ conditions = self ._parse_constraints (column_filters )
558+ if len (conditions ) > 0 :
559+ constraints_str = ' AND ' .join (conditions )
560+ if where == '' :
561+ where = 'WHERE ' + constraints_str
562+ else :
563+ where += ' AND ' + constraints_str
564+
484565 table_or_query = self ._query_execute (
485566 catalog = catalog , where = where ,
486567 get_query_payload = get_query_payload ,
@@ -527,96 +608,6 @@ def query_object(self, object_name, mission, *,
527608 return self .query_region (pos , catalog = mission , spatial = 'cone' ,
528609 get_query_payload = get_query_payload )
529610
530- def query_constraints (self , catalog , column_filters , * ,
531- get_query_payload = False , columns = None ,
532- verbose = False , maxrec = None ):
533- """Query the HEASARC TAP server using a constraints on the columns.
534-
535- This is a simple wrapper around
536- `~astroquery.heasarc.HeasarcClass.query_tap`
537- that constructs an ADQL query from a dictionary of filters.
538-
539- Parameters
540- ----------
541- catalog : str
542- The catalog to query. To list the available catalogs, use
543- :meth:`~astroquery.heasarc.HeasarcClass.list_catalogs`.
544- column_filters : dict
545- A dictionary of column constraint filters to include in the query.
546- Each key-value pair will be translated into an ADQL condition.
547- - For a range query, use a tuple of two values (min, max).
548- e.g. ``{'flux': (1e-12, 1e-10)}`` translates to
549- ``flux BETWEEN 1e-12 AND 1e-10``.
550- - For list values, use a list of values.
551- e.g. ``{'object_type': ['QSO', 'GALAXY']}`` translates to
552- ``object_type IN ('QSO', 'GALAXY')``.
553- - For comparison queries, use a tuple of (operator, value),
554- where operator is one of '=', '!=', '<', '>', '<=', '>='.
555- e.g. ``{'magnitude': ('<', 15)}`` translates to ``magnitude < 15``.
556- - For exact matches, use a single value (str, int, float).
557- e.g. ``{'object_type': 'QSO'}`` translates to
558- ``object_type = 'QSO'``.
559- The keys should correspond to valid column names in the catalog.
560- Use `list_columns` to see the available columns.
561- get_query_payload : bool, optional
562- If `True` then returns the generated ADQL query as str.
563- Defaults to `False`.
564- columns : str, optional
565- Target column list with value separated by a comma(,).
566- Use * for all the columns. The default is to return a subset
567- of the columns that are generally the most useful.
568- verbose : bool, optional
569- If False, suppress vo warnings.
570- maxrec : int, optional
571- Maximum number of records
572-
573- """
574-
575- if not isinstance (column_filters , dict ):
576- raise ValueError ('params must be a dictionary of key-value pairs' )
577-
578- conditions = []
579- for key , value in column_filters .items ():
580- if isinstance (value , tuple ):
581- if (
582- len (value ) == 2
583- and all (isinstance (v , (int , float )) for v in value )
584- ):
585- conditions .append (
586- f"{ key } BETWEEN { value [0 ]} AND { value [1 ]} "
587- )
588- elif (
589- len (value ) == 2
590- and value [0 ] in (">" , "<" , ">=" , "<=" )
591- ):
592- conditions .append (f"{ key } { value [0 ]} { value [1 ]} " )
593- elif isinstance (value , list ):
594- # handle list values: key IN (...)
595- formatted = []
596- for v in value :
597- if isinstance (v , str ):
598- formatted .append (f"'{ v } '" )
599- else :
600- formatted .append (str (v ))
601- conditions .append (f"{ key } IN ({ ', ' .join (formatted )} )" )
602- else :
603- conditions .append (
604- f"{ key } = '{ value } '"
605- if isinstance (value , str ) else f"{ key } = { value } "
606- )
607- if len (conditions ) == 0 :
608- where = ""
609- else :
610- where = "WHERE " + (" AND " .join (conditions ))
611-
612- table_or_query = self ._query_execute (
613- catalog = catalog , where = where ,
614- get_query_payload = get_query_payload ,
615- columns = columns , verbose = verbose ,
616- maxrec = maxrec
617- )
618- return table_or_query
619-
620611 def locate_data (self , query_result = None , catalog_name = None ):
621612 """Get links to data products
622613 Use vo/datalinks to query the data products for some query_results.
0 commit comments