Skip to content

Commit 5644067

Browse files
authored
Merge pull request #2251 from esdc-esac-esa-int/xmm_proprietary_data
Xmm proprietary data
2 parents a31ab86 + 0c365f3 commit 5644067

File tree

8 files changed

+370
-111
lines changed

8 files changed

+370
-111
lines changed

CHANGES.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ New Tools and Services
77

88
Service fixes and enhancements
99
------------------------------
10+
esa.xmm_newton
11+
^^^^^^^^^^^^^^
12+
13+
- Add option to download proprietary data [#2251]
1014

1115
esa.jwst
1216
^^^^^^^^^^

astroquery/esa/xmm_newton/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@ class Conf(_config.ConfigNamespace):
1717
"""
1818
Configuration parameters for `astroquery.esa.xmm_newton`.
1919
"""
20-
DATA_ACTION = _config.ConfigItem("http://nxsa.esac.esa.int/"
20+
DATA_ACTION = _config.ConfigItem("https://nxsa.esac.esa.int/"
2121
"nxsa-sl/servlet/data-action?",
2222
"Main url for retriving XSA files")
23-
DATA_ACTION_AIO = _config.ConfigItem("http://nxsa.esac.esa.int/"
23+
DATA_ACTION_AIO = _config.ConfigItem("https://nxsa.esac.esa.int/"
2424
"nxsa-sl/servlet/data-action-aio?",
2525
"Main url for retriving XSA files")
26-
METADATA_ACTION = _config.ConfigItem("http://nxsa.esac.esa.int/"
26+
METADATA_ACTION = _config.ConfigItem("https://nxsa.esac.esa.int/"
2727
"nxsa-sl/servlet/"
2828
"metadata-action?",
2929
"Main url for retriving XSA metadata")

astroquery/esa/xmm_newton/core.py

Lines changed: 103 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -12,43 +12,43 @@
1212
1313
"""
1414
import re
15+
from getpass import getpass
1516
from ...utils.tap.core import TapPlus
16-
from ...query import BaseQuery
17+
from ...query import BaseQuery, QueryWithLogin
1718
import shutil
1819
import cgi
1920
from pathlib import Path
2021
import tarfile
2122
import os
23+
import configparser
2224

2325
from astropy.io import fits
2426
from . import conf
2527
from astroquery import log
2628
from astropy.coordinates import SkyCoord
2729
from ...exceptions import LoginError
2830

29-
3031
__all__ = ['XMMNewton', 'XMMNewtonClass']
3132

3233

3334
class XMMNewtonClass(BaseQuery):
34-
3535
data_url = conf.DATA_ACTION
3636
data_aio_url = conf.DATA_ACTION_AIO
3737
metadata_url = conf.METADATA_ACTION
3838
TIMEOUT = conf.TIMEOUT
3939

4040
def __init__(self, tap_handler=None):
4141
super(XMMNewtonClass, self).__init__()
42+
self.configuration = configparser.ConfigParser()
4243

4344
if tap_handler is None:
44-
self._tap = TapPlus(url="http://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, credentials_file=None, **kwargs):
5252
"""
5353
Download data from XMM-Newton
5454
@@ -63,6 +63,13 @@ def download_data(self, observation_id, *, filename=None, verbose=False,
6363
verbose : bool
6464
optional, default 'False'
6565
flag to display information about the process
66+
prop: boolean
67+
optional, default 'False'
68+
flag to download proprietary data, the method will then ask the user to
69+
input their username and password either manually or using the credentials_file
70+
credentials_file: string
71+
optional, default None
72+
path to where the users config.ini file is stored with their username and password
6673
level : string
6774
level to download, optional, by default everything is downloaded
6875
values: ODF, PPS
@@ -94,48 +101,44 @@ def download_data(self, observation_id, *, filename=None, verbose=False,
94101
file format, optional, by default all formats
95102
values: ASC, ASZ, FTZ, HTM, IND, PDF, PNG
96103
97-
98104
Returns
99105
-------
100106
None if not verbose. It downloads the observation indicated
101107
If verbose returns the filename
102108
"""
103-
if filename is not None:
104-
filename = os.path.splitext(filename)[0]
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 their 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
129125

130-
if filename is None:
131-
filename = observation_id
132-
133-
filename += "".join(suffixes)
134-
135-
self._download_file(link, filename, head_safe=True, cache=cache)
126+
# get desired filename
127+
filename = self._create_filename(filename, observation_id, suffixes)
128+
"""
129+
If prop we change the log level so that it is above 20, this is to stop a log.debug (line 431) in query.py.
130+
This debug reveals the url being sent which in turn reveals the users username and password
131+
"""
132+
if prop:
133+
previouslevel = log.getEffectiveLevel()
134+
log.setLevel(21)
135+
self._download_file(link, filename, head_safe=True, cache=cache)
136+
log.setLevel(previouslevel)
137+
else:
138+
self._download_file(link, filename, head_safe=True, cache=cache)
136139

137140
if verbose:
138-
log.info("Wrote {0} to {1}".format(link, filename))
141+
log.info(f"Wrote {link} to {filename}")
139142

140143
def get_postcard(self, observation_id, *, image_type="OBS_EPIC",
141144
filename=None, verbose=False):
@@ -186,12 +189,12 @@ def get_postcard(self, observation_id, *, image_type="OBS_EPIC",
186189
else:
187190
filename = observation_id + ".png"
188191

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

191194
shutil.move(local_filepath, filename)
192195

193196
if verbose:
194-
log.info("Wrote {0} to {1}".format(link, filename))
197+
log.info(f"Wrote {link} to {filename}")
195198

196199
return filename
197200

@@ -273,14 +276,54 @@ def get_columns(self, table_name, *, only_names=True, verbose=False):
273276
break
274277

275278
if columns is None:
276-
raise ValueError("table name specified is not found in "
277-
"XSA TAP service")
279+
raise ValueError("table name specified is not found in XSA TAP service")
278280

279281
if only_names:
280282
return [c.name for c in columns]
281283
else:
282284
return columns
283285

286+
def _create_link(self, observation_id, **kwargs):
287+
link = f"{self.data_aio_url}obsno={observation_id}"
288+
link = link + "".join("&{0}={1}".format(key, val)
289+
for key, val in kwargs.items())
290+
return link
291+
292+
def _request_link(self, link, cache):
293+
# we can cache this HEAD request - the _download_file one will check
294+
# the file size and will never cache
295+
response = self._request('HEAD', link, save=False, cache=cache)
296+
# Get original extension
297+
if 'Content-Type' in response.headers and 'text' not in response.headers['Content-Type']:
298+
_, params = cgi.parse_header(response.headers['Content-Disposition'])
299+
elif response.status_code == 401:
300+
error = "Data protected by proprietary rights. Please check your credentials"
301+
raise LoginError(error)
302+
elif 'Content-Type' not in response.headers:
303+
error = "Incorrect credentials"
304+
raise LoginError(error)
305+
response.raise_for_status()
306+
return params
307+
308+
def _get_username_and_password(self, credentials_file):
309+
if credentials_file is not None:
310+
self.configuration.read(credentials_file)
311+
xmm_username = self.configuration.get("xmm_newton", "username")
312+
password = self.configuration.get("xmm_newton", "password")
313+
else:
314+
xmm_username = input("Username: ")
315+
password, password_from_keyring = QueryWithLogin._get_password(self, service_name="xmm_newton",
316+
username=xmm_username, reenter=False)
317+
return xmm_username, password
318+
319+
def _create_filename(self, filename, observation_id, suffixes):
320+
if filename is not None:
321+
filename = os.path.splitext(filename)[0]
322+
else:
323+
filename = observation_id
324+
filename += "".join(suffixes)
325+
return filename
326+
284327
def _parse_filename(self, filename):
285328
"""Parses the file's name of a product
286329
@@ -572,9 +615,9 @@ def get_epic_metadata(self, *, target_name=None,
572615
Tables containing the metadata of the target
573616
"""
574617
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")
618+
raise ValueError("Input parameters needed, "
619+
"please provide the name "
620+
"or the coordinates of the target")
578621

579622
epic_source = {"table": "xsa.v_epic_source",
580623
"column": "epic_source_equatorial_spoint"}
@@ -592,37 +635,37 @@ def get_epic_metadata(self, *, target_name=None,
592635
c = SkyCoord.from_name(target_name, parse=True)
593636

594637
if type(c) is not SkyCoord:
595-
raise Exception("The coordinates must be an "
638+
raise TypeError("The coordinates must be an "
596639
"astroquery.coordinates.SkyCoord object")
597640
if not radius:
598641
radius = 0.1
599642

600643
query_fmt = ("select {} from {} "
601644
"where 1=contains({}, circle('ICRS', {}, {}, {}));")
602645
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))
646+
epic_source["table"],
647+
epic_source["column"],
648+
c.ra.degree,
649+
c.dec.degree,
650+
radius))
608651
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))
652+
cat_4xmm["table"],
653+
cat_4xmm["column"],
654+
c.ra.degree,
655+
c.dec.degree,
656+
radius))
614657
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))
658+
stack_4xmm["table"],
659+
stack_4xmm["column"],
660+
c.ra.degree,
661+
c.dec.degree,
662+
radius))
620663
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))
664+
slew_source["table"],
665+
slew_source["column"],
666+
c.ra.degree,
667+
c.dec.degree,
668+
radius))
626669
return epic_source_table, cat_4xmm_table, stack_4xmm_table, slew_source_table
627670

628671
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+
[xmm_newton]
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('data', '*.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

0 commit comments

Comments
 (0)