Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

access_url in RegTAP backward compatibility hacks #341

Merged
merged 2 commits into from
Sep 18, 2022
Merged
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
65 changes: 52 additions & 13 deletions pyvo/registry/regtap.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@
This module provides basic, low-level access to the RegTAP Registries using
standardized TAP-based services.
"""

import functools
import os
import warnings

from astropy import table
from astropy.utils.exceptions import AstropyDeprecationWarning

from . import rtcons
from ..dal import scs, sia, ssa, sla, tap, query as dalq
Expand Down Expand Up @@ -374,13 +377,13 @@ def __init__(self, results, index, session=None):
dalq.Record.__init__(self, results, index, session)

self._mapping["access_urls"
] = self._mapping["access_urls"].split(TOKEN_SEP)
] = self._parse_pseudo_array(self._mapping["access_urls"])
self._mapping["standard_ids"
] = self._mapping["standard_ids"].split(TOKEN_SEP)
] = self._parse_pseudo_array(self._mapping["standard_ids"])
self._mapping["intf_types"
] = self._mapping["intf_types"].split(TOKEN_SEP)
] = self._parse_pseudo_array(self._mapping["intf_types"])
self._mapping["intf_roles"
] = self._mapping["intf_roles"].split(TOKEN_SEP)
] = self._parse_pseudo_array(self._mapping["intf_roles"])

self.interfaces = [Interface(*props)
for props in zip(
Expand All @@ -389,6 +392,26 @@ def __init__(self, results, index, session=None):
self["intf_types"],
self["intf_roles"])]

@staticmethod
def _parse_pseudo_array(literal):
"""
parses RegTAP pseudo-arrays into lists.

Parameters
----------
literal : str
the result of an ivo_string_agg call with TOKEN_SEP

Returns
-------
A list of strings corresponding to the orginal, database-side
aggregate.
"""
if not literal:
# As VOTable, we don't distinguish between None and ""
return []
return literal.split(TOKEN_SEP)

@property
def ivoid(self):
"""
Expand Down Expand Up @@ -496,13 +519,20 @@ def access_url(self):
"""
# some services declare some data models using multiple
# identifiers; in this case, we'll get the same access URL
# multiple times in here. Don't be alarmed when that happens:
access_urls = list(set(self["access_urls"]))
if len(access_urls)==1:
return access_urls[0]
else:
# multiple times in here. Be cool about that situation:
access_urls = list(sorted(set(self["access_urls"])))

if len(access_urls)==0:
raise dalq.DALQueryError(
"No unique access URL. Use get_service.")
f"The resource {self.ivoid} has no queriable interfaces.")

elif len(access_urls)>1:
warnings.warn(AstropyDeprecationWarning(
f"The resource {self.ivoid} has multiple capabilities. "
" You should explicitly pick one using get_service. "
" Returning some access_url now, but this behaviour "
" may change in the future."))
return access_urls[0]

@property
def standard_id(self):
Expand Down Expand Up @@ -615,6 +645,15 @@ def get_service(self,
If there are multiple capabilities for service_type, the
function choose the first matching capability by default
Pass lax=False to instead raise a DALQueryError.

Returns
-------
`pyvo.dal.DALService`
For standard service types, a specific DAL service instance
(e.g., a `pyvo.dal.tap.TAPService` when requesting ``tap``
services) is returned. For ``web`` services, what is returned is
an opaque service object that has a ``search()`` method simply
opening a web browser on the access URL.
"""
return self.get_interface(service_type, lax, std_only=True
).to_service()
Expand Down Expand Up @@ -691,10 +730,10 @@ def describe(self, verbose=False, width=78, file=None):
print("Access modes: " + ", ".join(sorted(self.access_modes())),
file=file)

try:
if len(self._mapping["access_urls"])==1:
print("Base URL: " + self.access_url, file=file)
except dalq.DALQueryError:
print("Multi-capabilty service -- use get_service()")
else:
print("Multi-capabilty service -- use get_service()", file=file)

if self.res_description:
print(file=file)
Expand Down
35 changes: 35 additions & 0 deletions pyvo/registry/tests/test_regtap.py
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,7 @@ def _makeRegistryRecord(overrides):
"standard_ids": "",
"intf_types": "",
"intf_roles": "",
"ivoid": "ivo://pyvo/test_regtap.py"
}
defaults.update(overrides)
return regtap.RegistryResource(_FakeResult(defaults), 0)
Expand Down Expand Up @@ -584,9 +585,43 @@ def test_describe_multi(self, flash_service):
assert "Flash/Heros SSAP" in output
assert ("Access modes: datalink#links-1.0, soda#sync-1.0,"
" ssa, tap#aux, web" in output)
assert "Multi-capabilty service" in output

assert "More info: http://dc.zah" in output

def test_no_access_url(self):
rsc = _makeRegistryRecord({
"access_urls": [],
"standard_ids": [],
"intf_types": [],
"intf_roles": []
})
with pytest.raises(dalq.DALQueryError) as excinfo:
rsc.access_url

assert str(excinfo.value) == ("The resource"
" ivo://pyvo/test_regtap.py has no queriable interfaces.")

def test_unique_access_url(self):
rsc = _makeRegistryRecord({
"access_urls": ["http://a"],
"standard_ids": ["ivo://ivoa.net/std/tap"],
"intf_types": ["vs:paramhttp"],
"intf_roles": [""]
})
assert rsc.access_url == "http://a"

def test_ambiguous_access_url_warns(self, recwarn):
rsc = _makeRegistryRecord({
"access_urls": ["http://a", "http://b"],
"standard_ids": ["ivo://ivoa.net/std/tap"]*2,
"intf_types": ["vs:paramhttp"]*2,
"intf_roles": ["std"]*2,
})
assert rsc.access_url == "http://a"
assert [str(w.message)[:50] for w in recwarn
] == ['The resource ivo://pyvo/test_regtap.py has multipl']


# TODO: While I suppose the contact test should keep requiring network,
# I think we should can the network responses involved in the following;
Expand Down