Skip to content

Commit 9366c3c

Browse files
committed
move column_filter to query_region and remove query_constraints. Update tests and docs accordingly
1 parent 74eaa15 commit 9366c3c

File tree

3 files changed

+195
-131
lines changed

3 files changed

+195
-131
lines changed

astroquery/heasarc/core.py

Lines changed: 83 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)