From 038e11e61cabb53b6a1099d3540bdbeec4be26dd Mon Sep 17 00:00:00 2001 From: Adam Thornton Date: Fri, 17 Jan 2025 12:01:25 -0700 Subject: [PATCH] Add get_query_history --- .../20250117_120000_athornton_DM_48465.md | 6 +++ pyproject.toml | 1 + src/lsst/rsp/__init__.py | 2 + src/lsst/rsp/catalog.py | 22 +++++++++ tests/catalog_test.py | 48 +++++++++++++++++++ tests/client_test.py | 25 ---------- 6 files changed, 79 insertions(+), 25 deletions(-) create mode 100644 changelog.d/20250117_120000_athornton_DM_48465.md create mode 100644 tests/catalog_test.py delete mode 100644 tests/client_test.py diff --git a/changelog.d/20250117_120000_athornton_DM_48465.md b/changelog.d/20250117_120000_athornton_DM_48465.md new file mode 100644 index 0000000..21b2c01 --- /dev/null +++ b/changelog.d/20250117_120000_athornton_DM_48465.md @@ -0,0 +1,6 @@ + + +### New features + +- added `get_query_history()` function + diff --git a/pyproject.toml b/pyproject.toml index 5b0c5e0..c34ff52 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ dependencies = [ "httpx<0.28", "structlog", # Uses CalVer, not SemVer "symbolicmode<3", + "xmltodict" ] dynamic = ["version"] diff --git a/src/lsst/rsp/__init__.py b/src/lsst/rsp/__init__.py index eb080f3..429f12c 100644 --- a/src/lsst/rsp/__init__.py +++ b/src/lsst/rsp/__init__.py @@ -5,6 +5,7 @@ from .catalog import ( get_catalog, get_obstap_service, + get_query_history, get_tap_service, retrieve_query, ) @@ -41,6 +42,7 @@ "get_datalink_result", "get_digest", "get_node", + "get_query_history", "get_pod", "get_tap_service", "get_siav2_service", diff --git a/src/lsst/rsp/catalog.py b/src/lsst/rsp/catalog.py index 0139546..cdccce8 100644 --- a/src/lsst/rsp/catalog.py +++ b/src/lsst/rsp/catalog.py @@ -3,8 +3,10 @@ import warnings import pyvo +import xmltodict from deprecated import deprecated +from .client import RSPClient from .utils import get_pyvo_auth, get_service_url @@ -66,3 +68,23 @@ def retrieve_query(query_url: str) -> pyvo.dal.AsyncTAPJob: with warnings.catch_warnings(): warnings.simplefilter("ignore", category=UserWarning) return pyvo.dal.AsyncTAPJob(query_url, get_pyvo_auth()) + + +async def get_query_history(n: int | None = None) -> list[str]: + """Retrieve last n query jobref ids. If n is not specified, or n<1, + retrieve all query jobref ids. + """ + client = RSPClient("/api/tap") + params = {} + if n and n > 0: + params = {"last": f"{n}"} + full_history_xml = await client.get("async", params=params) + history_dict = xmltodict.parse(full_history_xml.text) + joblist = history_dict["uws:jobs"]["uws:jobref"] + jobs: list[str] = [] + for job in joblist: + for k, v in job.items(): + if k == "@id": + jobs.append(v) + break + return jobs diff --git a/tests/catalog_test.py b/tests/catalog_test.py new file mode 100644 index 0000000..a1af2a3 --- /dev/null +++ b/tests/catalog_test.py @@ -0,0 +1,48 @@ +"""Test the RSPClient.""" + +import pytest +from pytest_httpx import HTTPXMock + +from lsst.rsp import get_query_history + + +@pytest.mark.usefixtures("_rsp_env") +@pytest.mark.asyncio +async def test_get_query_history(httpx_mock: HTTPXMock) -> None: + """Ensure that get_query_history() works, which in turn will ensure + that the RSPClient has the right headers and assembles its URL correctly. + """ + httpx_mock.add_response( + url="https://rsp.example.com/api/tap/async", + match_headers={ + "Authorization": "Bearer gf-dummytoken", + "Content-Type": "application/json", + }, + text=( + """ + + + COMPLETED + dp02_dc2_catalogs.Object - data-dev + adam + 2025-01-15T23:36:17.931Z + + + COMPLETED + adam + 2024-12-05T17:49:27.518Z + + + COMPLETED + ivoa.ObsCore - data-dev + adam + 2025-01-15T23:37:03.089Z + +""" + ), + ) + jobs = await get_query_history() + assert jobs == ["phdl67i3tmklfdbz", "r4qyb04xesh7mbz3", "yk16agxjefl6gly6"] + # The httpx mock will throw an error at teardown if we did not exercise + # the mock, so we know the request matched both the URL and the headers. diff --git a/tests/client_test.py b/tests/client_test.py deleted file mode 100644 index d02e23f..0000000 --- a/tests/client_test.py +++ /dev/null @@ -1,25 +0,0 @@ -"""Test the RSPClient.""" - -import pytest -from pytest_httpx import HTTPXMock - -from lsst.rsp import RSPClient - - -@pytest.mark.usefixtures("_rsp_env") -@pytest.mark.asyncio -async def test_client(httpx_mock: HTTPXMock) -> None: - """Ensure that the RSPClient has the right headers and assembles its - URL correctly. - """ - httpx_mock.add_response( - url="https://rsp.example.com/test-service/foo", - match_headers={ - "Authorization": "Bearer gf-dummytoken", - "Content-Type": "application/json", - }, - ) - client = RSPClient("/test-service") - await client.get("/foo") - # The httpx mock will throw an error at teardown if we did not exercise - # the mock, so we know the request matched both the URL and the headers.