From 42c76e03a91a42a78945d528f9316fea7cc9c7c3 Mon Sep 17 00:00:00 2001 From: Stelios Voutsinas Date: Fri, 24 Jan 2025 10:26:41 -0700 Subject: [PATCH] Update AsyncTAPJob.result to strictly follow TAP spec result ID requirement --- CHANGES.rst | 3 ++ pyvo/dal/tap.py | 15 ++++-- pyvo/dal/tests/test_tap.py | 104 +++++++++++++++++++++++++++++++++++++ 3 files changed, 119 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index db02e8af..6232c8aa 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,6 +6,9 @@ Enhancements and Fixes - Make deletion of TAP jobs optional via a new ``delete`` kwarg. [#640] +- Change AsyncTAPJob.result to return None if no result is found explicitly [#644] + + Deprecations and Removals ------------------------- diff --git a/pyvo/dal/tap.py b/pyvo/dal/tap.py index 5e4a2d38..d4547007 100644 --- a/pyvo/dal/tap.py +++ b/pyvo/dal/tap.py @@ -861,14 +861,14 @@ def results(self): @property def result(self): """ - The job result if exists + Returns the UWS result with id='result' if it exists, otherwise None. """ try: for r in self._job.results: if r.id_ == 'result': return r - return self._job.results[0] + return None except IndexError: return None @@ -885,7 +885,10 @@ def result_uri(self): the uri of the result """ try: - uri = self.result.href + result = self.result + if result is None: + return None + uri = result.href if not urlparse(uri).netloc: uri = urljoin(self.url, uri) return uri @@ -1007,6 +1010,12 @@ def fetch_result(self): """ returns the result votable if query is finished """ + result_uri = self.result_uri + if result_uri is None: + self._update() + self.raise_if_error() + raise DALServiceError("No result URI available", self.url) + try: response = self._session.get(self.result_uri, stream=True) response.raise_for_status() diff --git a/pyvo/dal/tests/test_tap.py b/pyvo/dal/tests/test_tap.py index 2e69b7bb..e5e1adbe 100644 --- a/pyvo/dal/tests/test_tap.py +++ b/pyvo/dal/tests/test_tap.py @@ -11,6 +11,7 @@ import tempfile import pytest +import requests import requests_mock from pyvo.dal.tap import escape, search, AsyncTAPJob, TAPService @@ -355,6 +356,12 @@ def async_fixture(mocker): yield from mock_server.use(mocker) +@pytest.fixture() +def async_fixture_with_timeout(mocker): + mock_server = MockAsyncTAPServer() + yield from mock_server.use(mocker) + + @pytest.fixture() def tables(mocker): def callback_tables(request, context): @@ -737,6 +744,103 @@ def match_request_text(request): finally: prototype.deactivate_features('cadc-tb-upload') + @pytest.mark.usefixtures('async_fixture') + def test_job_no_result(self): + service = TAPService('http://example.com/tap') + job = service.submit_job("SELECT * FROM ivoa.obscore") + with pytest.raises(DALServiceError) as excinfo: + job.fetch_result() + + assert "No result URI available" in str(excinfo.value) + job.delete() + + @pytest.mark.usefixtures('async_fixture') + def test_fetch_result_network_error(self): + service = TAPService('http://example.com/tap') + job = service.submit_job("SELECT * FROM ivoa.obscore") + job.run() + job.wait() + status_response = ''' + + 1 + COMPLETED + + + + ''' + + with requests_mock.Mocker() as rm: + rm.get(f'http://example.com/tap/async/{job.job_id}', + text=status_response) + rm.get( + f'http://example.com/tap/async/{job.job_id}/results/result', + exc=requests.exceptions.ConnectTimeout + ) + + with pytest.raises(DALServiceError) as excinfo: + job.fetch_result() + + assert "Unknown service error" in str(excinfo.value) + + job.delete() + + @pytest.mark.usefixtures('async_fixture') + def test_job_no_result_uri(self): + status_response = ''' + + 1 + COMPLETED + + + + ''' + + service = TAPService('http://example.com/tap') + job = service.submit_job("SELECT * FROM ivoa.obscore") + job.run() + job.wait() + + with requests_mock.Mocker() as rm: + rm.get(f'http://example.com/tap/async/{job.job_id}', + text=status_response) + job._update() + with pytest.raises(DALServiceError) as excinfo: + job.fetch_result() + + assert "No result URI available" in str(excinfo.value) + + job.delete() + + @pytest.mark.usefixtures('async_fixture') + def test_job_with_empty_error(self): + error_response = ''' + + 1 + ERROR + + + + + ''' + + service = TAPService('http://example.com/tap') + job = service.submit_job("SELECT * FROM ivoa.obscore") + job.run() + job.wait() + + with requests_mock.Mocker() as rm: + rm.get(f'http://example.com/tap/async/{job.job_id}', + text=error_response) + job._update() + with pytest.raises(DALQueryError) as excinfo: + job.fetch_result() + + assert "" in str(excinfo.value) + @pytest.mark.usefixtures("tapservice") class TestTAPCapabilities: