Skip to content

Commit 796085e

Browse files
authored
Merge pull request #2374 from andamian/cadc_datalink
A number of CADC authentication and doc fixes
2 parents 1f4e665 + 65627ee commit 796085e

File tree

4 files changed

+143
-62
lines changed

4 files changed

+143
-62
lines changed

astroquery/cadc/core.py

Lines changed: 71 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import requests
1313
from numpy import ma
1414
from urllib.parse import urlencode
15+
from urllib.error import HTTPError
1516

1617
from ..utils.class_or_instance import class_or_instance
1718
from ..utils import async_to_sync, commons
@@ -104,15 +105,15 @@ def __init__(self, url=None, tap_plus_handler=None, verbose=None,
104105

105106
super(CadcClass, self).__init__()
106107
self.baseurl = url
108+
# _auth_session contains the credentials that are used by both
109+
# the cadc tap and cadc datalink services
107110
if auth_session:
108111
self._auth_session = auth_session
109112
else:
110-
self._auth_session = None
113+
self._auth_session = authsession.AuthSession()
111114

112115
@property
113116
def cadctap(self):
114-
if not self._auth_session:
115-
self._auth_session = authsession.AuthSession()
116117
if not hasattr(self, '_cadctap'):
117118
if self.baseurl is None:
118119
self.baseurl = get_access_url(self.CADCTAP_SERVICE_URI)
@@ -125,6 +126,13 @@ def cadctap(self):
125126
self.baseurl, session=self._auth_session)
126127
return self._cadctap
127128

129+
@property
130+
def cadcdatalink(self):
131+
if not hasattr(self, '_datalink'):
132+
self._datalink = pyvo.dal.adhoc.DatalinkService(
133+
self.data_link_url, session=self._auth_session)
134+
return self._datalink
135+
128136
@property
129137
def data_link_url(self):
130138
if not hasattr(self, '_data_link_url'):
@@ -154,9 +162,9 @@ def login(self, user=None, password=None, certificate_file=None):
154162
# start with a new session
155163
if not isinstance(self.cadctap._session, (requests.Session,
156164
authsession.AuthSession)):
157-
raise TypeError('Cannot login with user provided session that is '
158-
'not an pyvo.authsession.AuthSession or '
159-
'requests.Session')
165+
raise AttributeError('Cannot login with user provided session that is '
166+
'not an pyvo.authsession.AuthSession or '
167+
'requests.Session')
160168
if not certificate_file and not (user and password):
161169
raise AttributeError('login credentials missing (user/password '
162170
'or certificate)')
@@ -216,13 +224,25 @@ def logout(self, verbose=None):
216224
if verbose is not None:
217225
warnings.warn('verbose deprecated since 0.4.0')
218226

219-
# the only way to ensure complete logout is to start with a new
220-
# session. This is mainly because of certificates. Adding cert
221-
# argument to a session already in use does not force it to
222-
# re-do the HTTPS hand shake
223-
self.cadctap._session = authsession.AuthSession()
224-
self.cadctap._session.update_from_capabilities(
225-
self.cadctap.capabilities)
227+
if isinstance(self._auth_session, pyvo.auth.AuthSession):
228+
# Remove the existing credentials (if any)
229+
# PyVO should provide this reset credentials functionality
230+
# TODO - this should be implemented in PyVO to avoid this deep
231+
# intrusion into that package
232+
self._auth_session.credentials.credentials = \
233+
{key: value for (key, value) in self._auth_session.credentials.credentials.items()
234+
if key == pyvo.auth.securitymethods.ANONYMOUS}
235+
elif isinstance(self._auth_session, requests.Session):
236+
# the only way to ensure complete logout is to start with a new
237+
# session. This is mainly because of certificates. Removing cert
238+
# argument to a session already in use does not force it to
239+
# re-do the HTTPS hand shake
240+
self._auth_session = requests.Session()
241+
self.cadctap._session = self._auth_session
242+
self.cadcdatalink._session = self._auth_session
243+
else:
244+
raise RuntimeError(
245+
'Do not know how to log out from custom session')
226246

227247
@class_or_instance
228248
def query_region_async(self, coordinates, radius=0.016666666666667*u.deg,
@@ -271,7 +291,7 @@ def query_name_async(self, name):
271291
272292
Parameters
273293
----------
274-
name: str
294+
name : str
275295
name of object to query for
276296
277297
Returns
@@ -353,7 +373,7 @@ def get_images(self, coordinates, radius,
353373
for fn in filenames:
354374
try:
355375
images.append(fn.get_fits())
356-
except requests.exceptions.HTTPError as err:
376+
except (requests.exceptions.HTTPError, HTTPError) as err:
357377
# Catch HTTPError if user is unauthorized to access file
358378
log.debug(
359379
"{} - Problem retrieving the file: {}".
@@ -456,7 +476,8 @@ def get_image_list(self, query_result, coordinates, radius):
456476
range(0, len(publisher_ids), batch_size)):
457477
datalink = pyvo.dal.adhoc.DatalinkResults.from_result_url(
458478
'{}?{}'.format(self.data_link_url,
459-
urlencode({'ID': pid_sublist}, True)))
479+
urlencode({'ID': pid_sublist}, True),
480+
session=self.cadcdatalink._session))
460481
for service_def in datalink.bysemantics('#cutout'):
461482
access_url = service_def.access_url
462483
if isinstance(access_url, bytes): # ASTROPY_LT_4_1
@@ -523,7 +544,8 @@ def get_data_urls(self, query_result, include_auxiliaries=False):
523544
datalink = pyvo.dal.adhoc.DatalinkResults.from_result_url(
524545
'{}?{}'.format(self.data_link_url,
525546
urlencode({'ID': pid_sublist,
526-
'REQUEST': 'downloads-only'}, True)))
547+
'REQUEST': 'downloads-only'}, True)),
548+
session=self.cadcdatalink._session)
527549
for service_def in datalink:
528550
if service_def.semantics in ['http://www.opencadc.org/caom2#pkg', '#package']:
529551
# TODO http://www.openadc.org/caom2#pkg has been replaced
@@ -582,7 +604,8 @@ def get_table(self, table, verbose=None):
582604
if table == t.name:
583605
return t
584606

585-
def exec_sync(self, query, maxrec=None, uploads=None, output_file=None):
607+
def exec_sync(self, query, maxrec=None, uploads=None, output_file=None,
608+
output_format='votable'):
586609
"""
587610
Run a query and return the results or save them in an output_file
588611
@@ -592,10 +615,13 @@ def exec_sync(self, query, maxrec=None, uploads=None, output_file=None):
592615
SQL to execute
593616
maxrec : int
594617
the maximum records to return. defaults to the service default
595-
uploads:
618+
uploads :
596619
Temporary tables to upload and run with the queries
597-
output_file: str or file handler:
620+
output_file : str or file handler
598621
File to save the results to
622+
output_format :
623+
Format of the output (default is basic). Must be one
624+
of the formats supported by `astropy.table`
599625
600626
Returns
601627
-------
@@ -611,10 +637,12 @@ def exec_sync(self, query, maxrec=None, uploads=None, output_file=None):
611637
result = response.to_table()
612638
if output_file:
613639
if isinstance(output_file, str):
614-
with open(output_file, 'bw') as f:
615-
f.write(result)
640+
fname = output_file
641+
elif hasattr(output_file, 'name'):
642+
fname = output_file.name
616643
else:
617-
output_file.write(result)
644+
raise AttributeError('Not a valid file name or file handler')
645+
result.write(fname, format=output_format, overwrite=True)
618646
return result
619647

620648
def create_async(self, query, maxrec=None, uploads=None):
@@ -681,9 +709,9 @@ def run_query(self, query, operation, output_file=None,
681709
when the job is executed in asynchronous mode,
682710
this flag specifies whether the execution will wait until results
683711
are available
684-
upload_resource: str, optional, default None
712+
upload_resource : str, optional, default None
685713
resource to be uploaded to UPLOAD_SCHEMA
686-
upload_table_name: str, required if uploadResource is provided,
714+
upload_table_name : str, required if uploadResource is provided,
687715
default None
688716
resource temporary table name associated to the uploaded resource
689717
@@ -712,7 +740,7 @@ def load_async_job(self, jobid, verbose=None):
712740
warnings.warn('verbose deprecated since 0.4.0')
713741

714742
return pyvo.dal.AsyncTAPJob('{}/async/{}'.format(
715-
self.cadctap.baseurl, jobid))
743+
self.cadctap.baseurl, jobid), session=self._auth_session)
716744

717745
def list_async_jobs(self, phases=None, after=None, last=None,
718746
short_description=True, verbose=None):
@@ -721,13 +749,13 @@ def list_async_jobs(self, phases=None, after=None, last=None,
721749
722750
Parameters
723751
----------
724-
phases: list of str
752+
phases : list of str
725753
Union of job phases to filter the results by.
726-
after: datetime
754+
after : datetime
727755
Return only jobs created after this datetime
728-
last: int
756+
last : int
729757
Return only the most recent number of jobs
730-
short_description: flag - True or False
758+
short_description : flag - True or False
731759
If True, the jobs in the list will contain only the information
732760
corresponding to the TAP ShortJobDescription object (job ID, phase,
733761
run ID, owner ID and creation ID) whereas if False, a separate GET
@@ -783,12 +811,19 @@ def get_access_url(service, capability=None):
783811
"""
784812
Returns the URL corresponding to a service by doing a lookup in the cadc
785813
registry. It returns the access URL corresponding to cookie authentication.
786-
:param service: the service the capability belongs to. It can be identified
787-
by a CADC uri ('ivo://cadc.nrc.ca/) which is looked up in the CADC registry
788-
or by the URL where the service capabilities is found.
789-
:param capability: uri representing the capability for which the access
790-
url is sought
791-
:return: the access url
814+
815+
Parameters
816+
----------
817+
service : str
818+
the service the capability belongs to. It can be identified
819+
by a CADC uri ('ivo://cadc.nrc.ca/) which is looked up in the CADC registry
820+
or by the URL where the service capabilities is found.
821+
capability : str
822+
uri representing the capability for which the access url is sought
823+
824+
Returns
825+
-------
826+
The access url
792827
793828
Note
794829
------

astroquery/cadc/tests/test_cadctap.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
import requests
2323
try:
2424
pyvo_OK = True
25-
from pyvo.auth import authsession
25+
from pyvo.auth import authsession, securitymethods
2626
from astroquery.cadc import Cadc, conf
2727
import astroquery.cadc.core as cadc_core
2828
except ImportError:
@@ -134,13 +134,14 @@ def test_list_async_jobs():
134134

135135
@patch('astroquery.cadc.core.get_access_url',
136136
Mock(side_effect=lambda x, y=None: 'https://some.url'))
137+
@patch('astroquery.cadc.core.pyvo.dal.TAPService.capabilities', []) # TAP capabilities not needed
138+
@patch('astroquery.cadc.core.pyvo.dal.adhoc.DatalinkService.capabilities', []) # DL capabilities not needed
137139
@pytest.mark.skipif(not pyvo_OK, reason='not pyvo_OK')
138140
def test_auth():
139141
# the Cadc() will cause a remote data call to TAP service capabilities
140142
# To avoid this, use an anonymous session and replace it with an
141143
# auth session later
142-
cadc = Cadc(auth_session=requests.Session())
143-
cadc.cadctap._session = authsession.AuthSession()
144+
cadc = Cadc()
144145
user = 'user'
145146
password = 'password'
146147
cert = 'cert'
@@ -153,8 +154,13 @@ def test_auth():
153154
cadc.login(certificate_file=cert)
154155
assert cadc.cadctap._session.credentials.get(
155156
'ivo://ivoa.net/sso#tls-with-certificate').cert == cert
157+
assert cadc.cadcdatalink._session.credentials.get(
158+
'ivo://ivoa.net/sso#tls-with-certificate').cert == cert
156159
# reset and try with user password/cookies
157-
cadc.cadctap._session = authsession.AuthSession()
160+
cadc.logout()
161+
for service in (cadc.cadctap, cadc.cadcdatalink):
162+
assert len(service._session.credentials.credentials) == 1
163+
assert securitymethods.ANONYMOUS in service._session.credentials.credentials.keys()
158164
post_mock = Mock()
159165
cookie = 'ABC'
160166
mock_resp = Mock()
@@ -166,6 +172,13 @@ def test_auth():
166172
assert cadc.cadctap._session.credentials.get(
167173
'ivo://ivoa.net/sso#cookie').cookies[cadc_core.CADC_COOKIE_PREFIX] == \
168174
'"{}"'.format(cookie)
175+
assert cadc.cadcdatalink._session.credentials.get(
176+
'ivo://ivoa.net/sso#cookie').cookies[cadc_core.CADC_COOKIE_PREFIX] == \
177+
'"{}"'.format(cookie)
178+
cadc.logout()
179+
for service in (cadc.cadctap, cadc.cadcdatalink):
180+
assert len(service._session.credentials.credentials) == 1
181+
assert securitymethods.ANONYMOUS in service._session.credentials.credentials.keys()
169182

170183

171184
# make sure that caps is reset at the end of the test
@@ -204,6 +217,8 @@ def raise_for_status(self):
204217

205218
@patch('astroquery.cadc.core.get_access_url',
206219
Mock(side_effect=lambda x, y=None: 'https://some.url'))
220+
@patch('astroquery.cadc.core.pyvo.dal.adhoc.DatalinkService',
221+
Mock(return_value=Mock(capabilities=[]))) # DL capabilities not needed
207222
@pytest.mark.skipif(not pyvo_OK, reason='not pyvo_OK')
208223
def test_get_data_urls():
209224

@@ -283,6 +298,10 @@ def test_misc():
283298

284299
@patch('astroquery.cadc.core.get_access_url',
285300
Mock(side_effect=lambda x, y=None: 'https://some.url'))
301+
@patch('astroquery.cadc.core.pyvo.dal.TAPService',
302+
Mock(return_value=Mock(capabilities=[]))) # TAP capabilities not needed
303+
@patch('astroquery.cadc.core.pyvo.dal.adhoc.DatalinkService',
304+
Mock(return_value=Mock(capabilities=[]))) # DL capabilities not needed
286305
@pytest.mark.skipif(not pyvo_OK, reason='not pyvo_OK')
287306
def test_get_image_list():
288307
def get(*args, **kwargs):
@@ -371,7 +390,7 @@ def test_exec_sync():
371390
votable.to_xml(buffer)
372391
cadc = Cadc(auth_session=requests.Session())
373392
response = Mock()
374-
response.to_table.return_value = buffer.getvalue()
393+
response.to_table.return_value = table.to_table()
375394
cadc.cadctap.search = Mock(return_value=response)
376395
output_file = '{}/test_vooutput.xml'.format(tempfile.tempdir)
377396
cadc.exec_sync('some query', output_file=output_file)

astroquery/cadc/tests/test_cadctap_remote.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,12 @@ def test_login_with_user_password(self):
151151
format(now.strftime('%Y-%m-%dT%H:%M:%S.%f'))
152152
result = cadc.exec_sync(query)
153153
assert len(result) == 0
154+
# login in again
155+
cadc.login(os.environ['CADC_USER'], os.environ['CADC_PASSWD'])
156+
query = "select top 1 * from caom2.Plane where metaRelease>'{}'". \
157+
format(now.strftime('%Y-%m-%dT%H:%M:%S.%f'))
158+
result = cadc.exec_sync(query)
159+
assert len(result) == 1
154160

155161
@pytest.mark.skipif(one_test, reason='One test mode')
156162
@pytest.mark.skipif('CADC_CERT' not in os.environ,

0 commit comments

Comments
 (0)