Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci_devtests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ on:
push:
branches:
- main
- develop
tags:
- '*'
pull_request:
branches:
- main
- develop
Comment on lines +8 to +14
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The addition of a 'develop' branch to CI workflow triggers is not mentioned in the PR description, which only describes ASM query functionality. This appears to be an infrastructure change unrelated to the main purpose of this PR and should either be in a separate PR or explained in the PR description.

Copilot uses AI. Check for mistakes.
schedule:
# run every Monday at 5am UTC
- cron: '0 5 * * 1'
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/ci_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ on:
push:
branches:
- main
- develop
tags:
- '*'
pull_request:
branches:
- main
- develop
Comment on lines +8 to +14
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The addition of a 'develop' branch to CI workflow triggers is not mentioned in the PR description, which only describes ASM query functionality. This appears to be an infrastructure change unrelated to the main purpose of this PR and should either be in a separate PR or explained in the PR description.

Copilot uses AI. Check for mistakes.
schedule:
# run every Monday at 5am UTC
- cron: '0 5 * * 1'
Expand Down
45 changes: 45 additions & 0 deletions .github/workflows/sync_fork.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Sync Fork
run-name: Sync Fork
on:
schedule:
- cron: '58 23 * * *' # run every day - two minutes to midnight
workflow_dispatch: # to enable manual runs of the workflow

jobs:
Get-Timestamp:
runs-on: ubuntu-latest
steps:
- run: date

Sync-With-Upstream:
runs-on: ubuntu-latest
steps:
- run: echo "The job was automatically triggered by a ${{ github.event_name }} event."
- run: echo "This job is now running on a ${{ runner.os }} server hosted by GitHub"
- run: echo "Running on branch ${{ github.ref }}, repository ${{ github.repository }}."
- name: Check out repository code
uses: actions/checkout@v4
with:
fetch-depth: 0
- run: echo "The ${{ github.repository }} repository has been cloned to the runner."
- name: List files in the repository
run: |
ls ${{ github.workspace }}
- name: Sync repository with upstream
run: |
cd ${{ github.workspace }}
git config --global user.email "jcarmona@eso.org"
Comment on lines +29 to +31
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The workflow file includes hardcoded personal email 'jcarmona@eso.org' in the git configuration. This should either use a generic project email or be configured as a repository secret/variable to avoid exposing personal information and to allow for easier maintenance when ownership changes.

Suggested change
run: |
cd ${{ github.workspace }}
git config --global user.email "jcarmona@eso.org"
env:
GIT_USER_EMAIL: ${{ secrets.GIT_USER_EMAIL }}
run: |
cd ${{ github.workspace }}
git config --global user.email "${GIT_USER_EMAIL}"

Copilot uses AI. Check for mistakes.
git config --global user.name "Nightly Sync"
git remote add upstream https://github.com/astropy/astroquery.git
git remote -v
git fetch upstream main
echo "--- upstream log: "
git log upstream/main --oneline -10
echo "--- current branch log before merge: "
git log --oneline -10
git merge upstream/main
echo "--- current branch log after merge: "
git log --oneline -10
echo "--- push force with lease"
git push --force-with-lease
- run: echo "The job finished with status ${{ job.status }}."
Comment on lines +1 to +45
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This entire workflow file appears to be specific to maintaining a fork synchronization with the upstream astropy/astroquery repository. This is not mentioned in the PR description and is not related to the ASM query functionality. This should either be in a separate PR or the PR description should be updated to explain why fork synchronization infrastructure is being added.

Copilot uses AI. Check for mistakes.
201 changes: 192 additions & 9 deletions astroquery/eso/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
from ..utils import schema
from .utils import _UserParams, raise_if_coords_not_valid, _reorder_columns, \
_raise_if_has_deprecated_keys, _build_adql_string, \
DEFAULT_LEAD_COLS_PHASE3, DEFAULT_LEAD_COLS_RAW
_split_str_as_list_of_str, DEFAULT_LEAD_COLS_PHASE3, DEFAULT_LEAD_COLS_RAW


__all__ = ['Eso', 'EsoClass']
Expand Down Expand Up @@ -71,6 +71,7 @@ class _EsoNames:
phase3_table = "ivoa.ObsCore"
raw_instruments_column = "instrument"
phase3_surveys_column = "obs_collection"
asm_schema = "asm"

@staticmethod
def ist_table(instrument_name):
Expand All @@ -79,6 +80,13 @@ def ist_table(instrument_name):
"""
return f"ist.{instrument_name}"

@staticmethod
def asm_table(asm_name):
"""
Returns the name of the ASM table
"""
return f"{_EsoNames.asm_schema}.{asm_name}"

apex_quicklooks_table = ist_table.__func__("apex_quicklooks")
apex_quicklooks_pid_column = "project_id"

Expand Down Expand Up @@ -348,6 +356,28 @@ def list_instruments(self, cache=True) -> List[str]:

return l_res

@unlimited_maxrec
@deprecated_renamed_argument('cache', None, since='0.4.12')
def list_asm(self, cache=True) -> List[str]:
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The list_surveys method uses keyword-only arguments (*, cache=True) on line 383, but the newly added list_asm method (line 361) does not follow this pattern and uses positional-or-keyword argument (cache=True). For consistency with list_surveys, consider using keyword-only arguments. Note that list_instruments also doesn't use keyword-only, suggesting this might be an area where the codebase itself is inconsistent.

Suggested change
def list_asm(self, cache=True) -> List[str]:
def list_asm(self, *, cache=True) -> List[str]:

Copilot uses AI. Check for mistakes.
"""
List all the available ASM tables offered by the ESO archive.

Returns
-------
asm_list : list of strings
cache : bool
Deprecated - unused.
"""
Comment on lines +365 to +370
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring has incorrect structure. The 'cache' parameter should be documented in a 'Parameters' section, not mixed with 'Returns'. The current format will not render properly in documentation tools like Sphinx.

Copilot uses AI. Check for mistakes.
_ = cache # We're aware about disregarding the argument
query_str = ("select table_name from TAP_SCHEMA.tables "
f"where schema_name='{_EsoNames.asm_schema}' order by table_name")
res = self.query_tap(query_str)["table_name"].data

l_res = list(res)
l_res = list(map(lambda x: x.split(".", 1)[1], l_res))

return l_res
Comment on lines +376 to +379

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could be probably rewritten as:
return [x.split(".", 1)[1] for x in res]
assuming that res is an iterable.


@unlimited_maxrec
@deprecated_renamed_argument('cache', None, since='0.4.12')
def list_surveys(self, *, cache=True) -> List[str]:
Expand All @@ -368,17 +398,30 @@ def list_surveys(self, *, cache=True) -> List[str]:
return res

@unlimited_maxrec
def list_column(self, table_name: str) -> None:
def _get_table_columns(self, table_name: str, *, include_description: bool = False) -> Table:
columns = "column_name, datatype, xtype, unit"
if include_description:
columns = f"{columns}, description"
query_str = (
f"select {columns} "
f"from TAP_SCHEMA.columns "
f"where table_name = '{table_name}'")
return self.query_tap(query_str)

@unlimited_maxrec
def list_column(self, table_name: str, *, include_description: bool = False) -> None:
"""
Prints the columns contained in a given table

Parameters
----------
table_name : str
Name of the table to inspect.
include_description : bool, optional
If ``True``, include column descriptions when available.
"""
help_query = (
f"select column_name, datatype, xtype, unit "
# TODO: The column description renders output unmanageable
# f", description "
f"from TAP_SCHEMA.columns "
f"where table_name = '{table_name}'")
available_cols = self.query_tap(help_query)
available_cols = self._get_table_columns(
table_name, include_description=include_description)

count_query = f"select count(*) from {table_name}"
num_records = list(self.query_tap(count_query)[0].values())[0]
Expand Down Expand Up @@ -590,6 +633,146 @@ def query_main(
t = _reorder_columns(t, DEFAULT_LEAD_COLS_RAW)
return t

def query_asm(
self,
asm_table: str, *,
help: bool = False,
columns: Union[List, str] = None,
column_filters: Optional[dict] = None,
maxrec: int = None,
**kwargs,
Comment on lines +636 to +643
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The query_asm method lacks several parameters present in query_instrument (cone_ra, cone_dec, cone_radius, order_by, order_by_desc) but documents order_by and order_by_desc as being available through **kwargs. While cone search might not be applicable for ASM data, the inconsistency in how parameters are handled (some explicit vs some in **kwargs) makes the API less clear. Consider making order_by and order_by_desc explicit parameters like in query_instrument for consistency and better API clarity.

Copilot uses AI. Check for mistakes.
) -> Union[Table, int, str, None]:
"""
Query ASM (Astronomical Site Monitor) data contained in the ESO archive.

Parameters
----------
asm_table : str
Name of the ASM table to query. Should be ONLY ONE of the
names returned by :meth:`~astroquery.eso.EsoClass.list_asm`.
The ``asm.`` prefix is accepted.
help : bool, optional
If ``True``, prints all the parameters accepted in ``column_filters``
and ``columns``. Default is ``False``.
columns : str or list of str, optional
Name of the columns the query should return. If specified as a string,
it should be a comma-separated list of column names.
column_filters : dict or None, optional
Constraints applied to the query in ADQL syntax,
e.g., ``{"exp_start": "between '2024-12-31' and '2025-12-31'"}``.
Default is ``None``.
maxrec : int or None, optional
Overrides the configured row limit for this query only.
**kwargs
Additional optional parameters consistent with
:meth:`~astroquery.eso.EsoClass.query_instrument`, including:
``top``, ``count_only``, ``get_query_payload``, ``authenticated``,
``order_by``, and ``order_by_desc``.

Returns
-------
astropy.table.Table, str, int, or None
- By default, returns an :class:`~astropy.table.Table` containing records
based on the specified columns and constraints. Returns ``None`` if no results.
- When ``count_only`` is ``True``, returns an ``int`` representing the
record count for the specified filters.
- When ``get_query_payload`` is ``True``, returns the query string that
would be issued to the TAP service given the specified arguments.
"""
column_filters = column_filters if column_filters else {}

if not isinstance(asm_table, str) or not asm_table.strip():
raise ValueError("asm_table must be a non-empty string.")

asm_table = asm_table.strip()
if asm_table.lower().startswith(f"{_EsoNames.asm_schema}."):
asm_table = asm_table.split(".", 1)[1]

asm_names = self.list_asm()
asm_map = {name.lower(): name for name in asm_names}
asm_table_key = asm_table.lower()
if asm_table_key not in asm_map:
raise ValueError(
f"Unknown ASM table '{asm_table}'. "
"Use list_asm() to see available ASM tables."
)

asm_table = asm_map[asm_table_key]
table_name = _EsoNames.asm_table(asm_table)

if help:
self.list_column(table_name, include_description=True)
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When help=True is used in query_asm, the function calls list_column with include_description=True to show descriptions. However, in _query_on_allowed_values (line 441), when print_help is True, it calls list_column WITHOUT include_description, defaulting to False. This creates an inconsistency: query_asm(help=True) shows descriptions, but query_instrument(help=True) does not. For consistency across the API, both should behave the same way.

Suggested change
self.list_column(table_name, include_description=True)
self.list_column(table_name)

Copilot uses AI. Check for mistakes.
return

allowed_kwargs = {
"top", "count_only", "get_query_payload", "authenticated",
"order_by", "order_by_desc",
}
unknown_kwargs = set(kwargs) - allowed_kwargs
if unknown_kwargs:
unknown_str = ", ".join(sorted(unknown_kwargs))
raise TypeError(f"Unexpected keyword argument(s): {unknown_str}")

columns_list = None
if columns is not None:
if isinstance(columns, str):
columns_list = _split_str_as_list_of_str(columns)
else:
columns_list = list(columns)

available_cols = self._get_table_columns(table_name)["column_name"].data
available_cols_map = {c.lower(): c for c in available_cols}

if columns_list:
if not (len(columns_list) == 1 and columns_list[0] == '*'):
missing_cols = [
c for c in columns_list if c.lower() not in available_cols_map
]
if missing_cols:
missing_str = ", ".join(sorted(missing_cols))
raise ValueError(
f"Unknown column(s) in columns for table {table_name}: {missing_str}"
)
columns = [available_cols_map[c.lower()] for c in columns_list]

if column_filters:
missing_filters = [
k for k in column_filters.keys() if k.lower() not in available_cols_map
]
if missing_filters:
missing_str = ", ".join(sorted(missing_filters))
raise ValueError(
f"Unknown column(s) in column_filters for table {table_name}: {missing_str}"
)
column_filters = {
available_cols_map[k.lower()]: v for k, v in column_filters.items()
}

row_limit = None
if maxrec is not None:
row_limit = self.ROW_LIMIT
self.ROW_LIMIT = maxrec

try:
user_params = _UserParams(
table_name=table_name,
column_name=None,
allowed_values=None,
columns=columns,
column_filters=column_filters,
top=kwargs.get("top"),
count_only=kwargs.get("count_only", False),
get_query_payload=kwargs.get("get_query_payload", False),
print_help=False,
authenticated=kwargs.get("authenticated", False),
order_by=kwargs.get("order_by", ''),
order_by_desc=kwargs.get("order_by_desc", True),
)
return self._query_on_allowed_values(user_params)
finally:
if row_limit is not None:
self.ROW_LIMIT = row_limit

@deprecated_renamed_argument(('open_form', 'cache'), (None, None),
since=['0.4.12', '0.4.12'])
def query_instrument(
Expand Down
Loading