Skip to content

Commit 045469f

Browse files
NXSAPCR-1091: Added functionality to download proprietary data
- added corresponding tests
1 parent 46739aa commit 045469f

File tree

5 files changed

+294
-92
lines changed

5 files changed

+294
-92
lines changed

astroquery/esa/xmm_newton/core.py

Lines changed: 89 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
1313
"""
1414
import re
15+
from getpass import getpass
1516
from ...utils.tap.core import TapPlus
1617
from ...query import BaseQuery
1718
import shutil
@@ -21,7 +22,7 @@
2122
import os
2223

2324
from astropy.io import fits
24-
from . import conf
25+
from . import conf, config
2526
from astroquery import log
2627
from astropy.coordinates import SkyCoord
2728
from ...exceptions import LoginError
@@ -41,14 +42,13 @@ def __init__(self, tap_handler=None):
4142
super(XMMNewtonClass, self).__init__()
4243

4344
if tap_handler is None:
44-
self._tap = TapPlus(url="https://nxsa.esac.esa.int"
45-
"/tap-server/tap")
45+
self._tap = TapPlus(url="https://nxsa.esac.esa.int/tap-server/tap")
4646
else:
4747
self._tap = tap_handler
4848
self._rmf_ftp = str("http://sasdev-xmm.esac.esa.int/pub/ccf/constituents/extras/responses/")
4949

5050
def download_data(self, observation_id, *, filename=None, verbose=False,
51-
cache=True, **kwargs):
51+
cache=True, prop=False, username=None, password=None, **kwargs):
5252
"""
5353
Download data from XMM-Newton
5454
@@ -100,42 +100,38 @@ def download_data(self, observation_id, *, filename=None, verbose=False,
100100
None if not verbose. It downloads the observation indicated
101101
If verbose returns the filename
102102
"""
103-
if filename is not None:
104-
filename = os.path.splitext(filename)[0]
103+
"""
104+
Here we change the log level so that it is above 20, this is to stop a log.debug in query.py. this debug
105+
reveals the url being sent which in turn reveals the users username and password
106+
"""
107+
previouslevel = log.getEffectiveLevel()
108+
log.setLevel(50)
105109

106-
link = self.data_aio_url + "obsno=" + observation_id
110+
# create url to access the aio
111+
link = self._create_link(observation_id, **kwargs)
107112

108-
link = link + "".join("&{0}={1}".format(key, val)
109-
for key, val in kwargs.items())
113+
# If the user wants to access proprietary data, ask them for there credentials
114+
if prop:
115+
username, password = self._get_username_and_password(credentials_file)
116+
link = f"{link}&AIOUSER={username}&AIOPWD={password}"
110117

111118
if verbose:
112119
log.info(link)
113120

114-
# we can cache this HEAD request - the _download_file one will check
115-
# the file size and will never cache
116-
response = self._request('HEAD', link, save=False, cache=cache)
117-
118-
# Get original extension
119-
if 'Content-Type' in response.headers and 'text' not in response.headers['Content-Type']:
120-
_, params = cgi.parse_header(response.headers['Content-Disposition'])
121-
else:
122-
if response.status_code == 401:
123-
error = "Data protected by proprietary rights. Please check your credentials"
124-
raise LoginError(error)
125-
response.raise_for_status()
126-
121+
# get response of created url
122+
params = self._request_link(link, cache)
127123
r_filename = params["filename"]
128124
suffixes = Path(r_filename).suffixes
125+
print(suffixes)
129126

130-
if filename is None:
131-
filename = observation_id
132-
133-
filename += "".join(suffixes)
127+
# get desired filename
128+
filename = self._create_filename(filename, observation_id, suffixes)
134129

135130
self._download_file(link, filename, head_safe=True, cache=cache)
136131

137132
if verbose:
138-
log.info("Wrote {0} to {1}".format(link, filename))
133+
log.info(f"Wrote {link} to {filename}")
134+
log.setLevel(previouslevel)
139135

140136
def get_postcard(self, observation_id, *, image_type="OBS_EPIC",
141137
filename=None, verbose=False):
@@ -186,12 +182,12 @@ def get_postcard(self, observation_id, *, image_type="OBS_EPIC",
186182
else:
187183
filename = observation_id + ".png"
188184

189-
log.info("Copying file to {0}...".format(filename))
185+
log.info(f"Copying file to {filename}...")
190186

191187
shutil.move(local_filepath, filename)
192188

193189
if verbose:
194-
log.info("Wrote {0} to {1}".format(link, filename))
190+
log.info(f"Wrote {link} to {filename}")
195191

196192
return filename
197193

@@ -273,14 +269,53 @@ def get_columns(self, table_name, *, only_names=True, verbose=False):
273269
break
274270

275271
if columns is None:
276-
raise ValueError("table name specified is not found in "
277-
"XSA TAP service")
272+
raise ValueError("table name specified is not found in XSA TAP service")
278273

279274
if only_names:
280275
return [c.name for c in columns]
281276
else:
282277
return columns
283278

279+
def _create_link(self, observation_id, **kwargs):
280+
link = f"{self.data_aio_url}obsno={observation_id}"
281+
link = link + "".join("&{0}={1}".format(key, val)
282+
for key, val in kwargs.items())
283+
return link
284+
285+
def _request_link(self, link, cache):
286+
# we can cache this HEAD request - the _download_file one will check
287+
# the file size and will never cache
288+
response = self._request('HEAD', link, save=False, cache=cache)
289+
# Get original extension
290+
if 'Content-Type' in response.headers and 'text' not in response.headers['Content-Type']:
291+
_, params = cgi.parse_header(response.headers['Content-Disposition'])
292+
elif response.status_code == 401:
293+
error = "Data protected by proprietary rights. Please check your credentials"
294+
raise LoginError(error)
295+
elif 'Content-Type' not in response.headers:
296+
error = "Incorrect credentials"
297+
raise LoginError(error)
298+
response.raise_for_status()
299+
return params
300+
301+
def _get_username_and_password(self, credentials_file):
302+
if credentials_file != None:
303+
self.configuration.read(credentials_file)
304+
username = self.configuration.get('user', 'username')
305+
password = self.configuration.get('user', 'password')
306+
else:
307+
username = input("Username: ")
308+
password = getpass("Password: ")
309+
return username, password
310+
311+
def _create_filename(self, filename, observation_id, suffixes):
312+
if filename is not None:
313+
filename = os.path.splitext(filename)[0]
314+
else:
315+
filename = observation_id
316+
filename += "".join(suffixes)
317+
return filename
318+
284319
def _parse_filename(self, filename):
285320
"""Parses the file's name of a product
286321
@@ -572,9 +607,9 @@ def get_epic_metadata(self, *, target_name=None,
572607
Tables containing the metadata of the target
573608
"""
574609
if not target_name and not coordinates:
575-
raise Exception("Input parameters needed, "
576-
"please provide the name "
577-
"or the coordinates of the target")
610+
raise Exception("Input parameters needed, "
611+
"please provide the name "
612+
"or the coordinates of the target")
578613

579614
epic_source = {"table": "xsa.v_epic_source",
580615
"column": "epic_source_equatorial_spoint"}
@@ -600,29 +635,29 @@ def get_epic_metadata(self, *, target_name=None,
600635
query_fmt = ("select {} from {} "
601636
"where 1=contains({}, circle('ICRS', {}, {}, {}));")
602637
epic_source_table = self.query_xsa_tap(query_fmt.format(cols,
603-
epic_source["table"],
604-
epic_source["column"],
605-
c.ra.degree,
606-
c.dec.degree,
607-
radius))
638+
epic_source["table"],
639+
epic_source["column"],
640+
c.ra.degree,
641+
c.dec.degree,
642+
radius))
608643
cat_4xmm_table = self.query_xsa_tap(query_fmt.format(cols,
609-
cat_4xmm["table"],
610-
cat_4xmm["column"],
611-
c.ra.degree,
612-
c.dec.degree,
613-
radius))
644+
cat_4xmm["table"],
645+
cat_4xmm["column"],
646+
c.ra.degree,
647+
c.dec.degree,
648+
radius))
614649
stack_4xmm_table = self.query_xsa_tap(query_fmt.format(cols,
615-
stack_4xmm["table"],
616-
stack_4xmm["column"],
617-
c.ra.degree,
618-
c.dec.degree,
619-
radius))
650+
stack_4xmm["table"],
651+
stack_4xmm["column"],
652+
c.ra.degree,
653+
c.dec.degree,
654+
radius))
620655
slew_source_table = self.query_xsa_tap(query_fmt.format(cols,
621-
slew_source["table"],
622-
slew_source["column"],
623-
c.ra.degree,
624-
c.dec.degree,
625-
radius))
656+
slew_source["table"],
657+
slew_source["column"],
658+
c.ra.degree,
659+
c.dec.degree,
660+
radius))
626661
return epic_source_table, cat_4xmm_table, stack_4xmm_table, slew_source_table
627662

628663
def get_epic_lightcurve(self, filename, source_number, *,
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[user]
2+
username=test
3+
password=test

astroquery/esa/xmm_newton/tests/setup_package.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
def get_package_data():
2020
paths = [os.path.join('data', '*.tar'),
2121
os.path.join('data', '*.xml'),
22+
os.path.join('my_config.ini')
2223
] # etc, add other extensions
2324
# you can also enlist files individually by names
2425
# finally construct and return a dict for the sub module

astroquery/esa/xmm_newton/tests/test_xmm_newton.py

Lines changed: 70 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,33 +9,40 @@
99
1010
Created on 4 Sept. 2019
1111
"""
12+
from unittest.mock import patch
1213

1314
import pytest
14-
15-
import sys
1615
import tarfile
1716
import os
1817
import errno
1918
import shutil
20-
from astropy.coordinates import SkyCoord
21-
from astropy.utils.diff import report_diff_values
22-
from astroquery.utils.tap.core import TapPlus
2319

2420
from ..core import XMMNewtonClass
2521
from ..tests.dummy_tap_handler import DummyXMMNewtonTapHandler
2622
from ..tests.dummy_handler import DummyHandler
27-
from fileinput import filename
28-
from tarfile import is_tarfile
23+
from astroquery.exceptions import LoginError
2924

3025

31-
class TestXMMNewton():
26+
class mockResponse():
27+
headers = {'Date': 'Wed, 24 Nov 2021 13:43:50 GMT',
28+
'Server': 'Apache/2.4.6 (Red Hat Enterprise Linux) OpenSSL/1.0.2k-fips',
29+
'Content-Disposition': 'inline; filename="0560181401.tar.gz"',
30+
'Content-Type': 'application/x-gzip',
31+
'Content-Length': '6590874', 'Connection': 'close'}
32+
status_code = 400
33+
34+
@staticmethod
35+
def raise_for_status():
36+
pass
3237

38+
39+
class TestXMMNewton():
3340
def get_dummy_tap_handler(self):
34-
parameterst = {'query': "select top 10 * from v_public_observations",
35-
'output_file': "test2.vot",
36-
'output_format': "votable",
37-
'verbose': False}
38-
dummyTapHandler = DummyXMMNewtonTapHandler("launch_job", parameterst)
41+
parameters = {'query': "select top 10 * from v_public_observations",
42+
'output_file': "test2.vot",
43+
'output_format': "votable",
44+
'verbose': False}
45+
dummyTapHandler = DummyXMMNewtonTapHandler("launch_job", parameters)
3946
return dummyTapHandler
4047

4148
def test_query_xsa_tap(self):
@@ -235,7 +242,7 @@ def _create_tar(self, tarname, files):
235242
os.makedirs(os.path.join(ob_name, ftype))
236243
except OSError as exc:
237244
if exc.errno == errno.EEXIST and \
238-
os.path.isdir(os.path.join(ob_name, ftype)):
245+
os.path.isdir(os.path.join(ob_name, ftype)):
239246
pass
240247
else:
241248
raise
@@ -255,7 +262,7 @@ def _create_tar_lightcurves(self, tarname, files):
255262
os.makedirs(os.path.join(ob_name, ftype))
256263
except OSError as exc:
257264
if exc.errno == errno.EEXIST and \
258-
os.path.isdir(os.path.join(ob_name, ftype)):
265+
os.path.isdir(os.path.join(ob_name, ftype)):
259266
pass
260267
else:
261268
raise
@@ -377,7 +384,7 @@ def test_get_epic_images(self):
377384
xsa = XMMNewtonClass(self.get_dummy_tap_handler())
378385
res = xsa.get_epic_images(_tarname, band=[], instrument=[],
379386
get_detmask=True, get_exposure_map=True)
380-
assert len(res) == 6 # Number of different bands
387+
assert len(res) == 6 # Number of different bands
381388
assert len(res[1]) == 9 # Number of different inst within band 1
382389
assert len(res[2]) == 9 # Number of different inst within band 2
383390
assert len(res[3]) == 9 # Number of different inst within band 3
@@ -510,3 +517,50 @@ def test_get_epic_lightcurve_invalid_source_number(self, capsys):
510517
% (_tarname, _invalid_source_number,
511518
_default_instrument))
512519
os.remove(_tarname)
520+
521+
def test_create_link(self):
522+
xsa = XMMNewtonClass(self.get_dummy_tap_handler())
523+
link = xsa._create_link("0560181401")
524+
assert link == "https://nxsa.esac.esa.int/nxsa-sl/servlet/data-action-aio?obsno=0560181401"
525+
526+
@patch('astroquery.query.BaseQuery._request')
527+
def test_request_link(self, mock_request):
528+
xsa = XMMNewtonClass(self.get_dummy_tap_handler())
529+
mock_request.return_value = mockResponse
530+
params = xsa._request_link("https://nxsa.esac.esa.int/nxsa-sl/servlet/data-action-aio?obsno=0560181401", None)
531+
assert params == {'filename': '0560181401.tar.gz'}
532+
533+
@pytest.mark.xfail(raises=LoginError)
534+
@patch('astroquery.query.BaseQuery._request')
535+
def test_request_link_protected(self, mock_request):
536+
xsa = XMMNewtonClass(self.get_dummy_tap_handler())
537+
dummyclass = mockResponse
538+
dummyclass.headers = {}
539+
mock_request.return_value = dummyclass
540+
xsa._request_link("https://nxsa.esac.esa.int/nxsa-sl/servlet/data-action-aio?obsno=0560181401", None)
541+
542+
@pytest.mark.xfail(raises=LoginError)
543+
@patch('astroquery.query.BaseQuery._request')
544+
def test_request_link_incorrect_credentials(self, mock_request):
545+
xsa = XMMNewtonClass(self.get_dummy_tap_handler())
546+
dummyclass = mockResponse
547+
dummyclass.headers = {}
548+
dummyclass.status_code = 10
549+
mock_request.return_value = dummyclass
550+
xsa._request_link("https://nxsa.esac.esa.int/nxsa-sl/servlet/data-action-aio?obsno=0560181401", None)
551+
552+
def test_get_username_and_password(self):
553+
xsa = XMMNewtonClass(self.get_dummy_tap_handler())
554+
username, password = xsa._get_username_and_password("astroquery/esa/xmm_newton/tests/my_config.ini")
555+
assert username == "test"
556+
assert password == "test"
557+
558+
def test_create_filename_None(self):
559+
xsa = XMMNewtonClass(self.get_dummy_tap_handler())
560+
filename = xsa._create_filename(None, "0560181401", ['.tar', '.gz'])
561+
assert filename == "0560181401.tar.gz"
562+
563+
def test_create_filename_Not_None(self):
564+
xsa = XMMNewtonClass(self.get_dummy_tap_handler())
565+
filename = xsa._create_filename("Test", "0560181401", ['.tar', '.gz'])
566+
assert filename == "Test.tar.gz"

0 commit comments

Comments
 (0)