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

Fixing LCO archive #911

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
82 changes: 82 additions & 0 deletions astroquery/lco/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Licensed under a 3-clause BSD style license - see LICENSE.rst
"""
Las Cumbres Observatory public archive Query Tool
===============
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor style: make the # of ='s same as # of characters above


This module contains various methods for querying
Las Cumbres Observatory data archive as hosted by IPAC.
"""
from astropy import config as _config

class Conf(_config.ConfigNamespace):
"""
Configuration parameters for `astroquery.lco`.
"""

_base_path = 'https://archive-api.lco.global'

server = _config.ConfigItem(
_base_path,
'Las Cumbres Observatory archive API base URL')

aggregate = _config.ConfigItem(
_base_path + '/aggregate/',
'Returns the unique values shared across all fits files for site, telescope, instrument, filter and obstype.')

get_token = _config.ConfigItem(
_base_path + '/api-token-auth/',
'Obtain an api token for use with authenticated requests.')

frames = _config.ConfigItem(
_base_path + '/frames/',
'Return a list of frames.')

frame = _config.ConfigItem(
_base_path + '/frames/{id}/',
'Return a single frame.')

frames_related = _config.ConfigItem(
_base_path + '/frames/{id}/related/',
'Return a list of frames related to this frame (calibration frames, catalogs, etc).')

frames_headers = _config.ConfigItem(
_base_path + '/frames/{id}/headers/',
'Return the headers for a single frame.')

frames_zip = _config.ConfigItem(
_base_path + '/frames/zip/',
"Returns a zip file containing all of the requested frames. Note this is not the preferred method for downloading files. Use the frame's url property instead.")

profile = _config.ConfigItem(
_base_path + '/profile/',
'Returns information about the currently authenticated user.')

row_limit = _config.ConfigItem(
10,
'Maximum number of rows to retrieve in result')

timeout = _config.ConfigItem(
60,
'Time limit for connecting to the Las Cumbres Observatory archive')

username = _config.ConfigItem(
"",
'Optional default username for Las Cumbres Observatory archive.')

dtypes = _config.ConfigItem(
['i','str','str','str','str','str','str','str','str','f','str','str'],
"NumPy data types for archive response")

names = _config.ConfigItem(
['id','filename','url','RLEVEL','DATE_OBS','PROPID','OBJECT','SITEID','TELID','EXPTIME','FILTER','REQNUM'],
"Archive response items")


conf = Conf()


from .core import Lco, LcoClass

__all__ = ['Lco', 'LcoClass',
'Conf', 'conf',
]
216 changes: 216 additions & 0 deletions astroquery/lco/core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
# Licensed under a 3-clause BSD style license - see LICENSE.rst
from __future__ import print_function

"""
Las Cumbres Observatory
====
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto


API from

https://archive-api.lco.global

The following are endpoints of the Las Cumbres Observatory archive query service,

Endpoint Method Usage
/aggregate/ GET Returns the unique values shared accross all fits files for site, telescope, instrument, filter and obstype.
/api-token-auth/ POST Obtain an api token for use with authenticated requests.
/frames/ GET Return a list of frames.
/frames/{id}/ GET Return a single frame.
/frames/{id}/related/ GET Return a list of frames related to this frame (calibration frames, catalogs, etc).
/frames/{id}/headers/ GET Return the headers for a single frame.
/frames/zip/ POST Returns a zip file containing all of the requested frames. Note this is not the preferred method for downloading files. Use the frame's url property instead.
/profile/ GET Returns information about the currently authenticated user.

The service accepts the following keywords,

"""

import json
import keyring
import getpass
import warnings
from datetime import datetime

import astropy.units as u
import astropy.coordinates as coord
import astropy.io.votable as votable
from astropy.table import Table
from astropy.io import fits
from astropy import log

from ..query import BaseQuery, QueryWithLogin
from ..utils import commons, system_tools, prepend_docstr_noreturns, async_to_sync
from . import conf

@async_to_sync
class LcoClass(QueryWithLogin):

# use the Configuration Items imported from __init__.py to set the URL,
# TIMEOUT, etc.
URL = conf.server
TIMEOUT = conf.timeout
FRAMES_URL = conf.frames
DTYPES = ['i','S39','S230','i','S19','S16','S20','S3','S4','f','S2','i']
DATA_NAMES = ['id','filename','url','RLEVEL','DATE_OBS','PROPID','OBJECT','SITEID','TELID','EXPTIME','FILTER','REQNUM']
TOKEN = None

def query_object_async(self, object_name, get_query_payload=False,
cache=True, start=None, end=None):
"""
This method is for services that can parse object names. Otherwise
use :meth:`astroquery.lco.LcoClass.query_region`.
Put a brief description of what the class does here.

Parameters
----------
object_name : str
name of the identifier to query.
get_query_payload : bool, optional
This should default to False. When set to `True` the method
should return the HTTP request parameters as a dict.
start: str, optional
Default is `None`. When set this must be in iso E8601Dw.d datestamp format
YYYY-MM-DD HH:MM
end: str, optional
Default is `None`. When set this must be in iso E8601Dw.d datestamp format
YYYY-MM-DD HH:MM
Returns
-------
response : `requests.Response`
The HTTP response returned from the service.
All async methods should return the raw HTTP response.

Examples
--------
While this section is optional you may put in some examples that
show how to use the method. The examples are written similar to
standard doctests in python.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just a reminder to remove this reminder text and replace it with something useful

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added some useful text describing data format


"""

request_payload = self._args_to_payload(**{'object_name':object_name,'start':start, 'end':end})
# similarly fill up the rest of the dict ...

if get_query_payload:
return request_payload
# BaseQuery classes come with a _request method that includes a
# built-in caching system
if not self.TOKEN:
warnings.warn("You have not authenticated and will only get results for non-proprietary data")
headers=None
else:
headers = {'Authorization': 'Token ' + self.TOKEN}
response = self._request('GET', self.FRAMES_URL, params=request_payload,
timeout=self.TIMEOUT, cache=cache, headers=headers)
if response.status_code == 200:
resp = json.loads(response.content)
return self._parse_result(resp)
else:
log.exception("Failed!")
return False

def _args_to_payload(self, *args, **kwargs):
request_payload = dict()
request_payload['OBJECT'] = kwargs['object_name']
request_payload['REVEL'] = '91'
request_payload['OBSTYPE'] = 'EXPOSE'
if kwargs['start']:
request_payload['start'] = validate_datetime(kwargs['start'])
if kwargs['end']:
request_payload['end'] = validate_datetime(kwargs['end'])
return request_payload

def _login(self, username=None, store_password=False,
reenter_password=False):
"""
Login to the LCO Archive.

Parameters
----------
username : str, optional
Username to the Las Cumbres Observatory archive. If not given, it should be
specified in the config file.
store_password : bool, optional
Stores the password securely in your keyring. Default is False.
reenter_password : bool, optional
Asks for the password even if it is already stored in the
keyring. This is the way to overwrite an already stored password
on the keyring. Default is False.
"""
if username is None:
if self.USERNAME == "":
raise LoginError("If you do not pass a username to login(), "
"you should configure a default one!")
else:
username = self.USERNAME

# Get password from keyring or prompt
if reenter_password is False:
password_from_keyring = keyring.get_password(
"astroquery:archive-api.lco.global", username)
else:
password_from_keyring = None

if password_from_keyring is None:
if system_tools.in_ipynb():
log.warning("You may be using an ipython notebook:"
" the password form will appear in your terminal.")
password = getpass.getpass("{0}, enter your Las Cumbres Observatory password:\n"
.format(username))
else:
password = password_from_keyring
# Authenticate
log.info("Authenticating {0} with lco.global...".format(username))
# Do not cache pieces of the login process
login_response = self._request("POST", conf.get_token,
cache=False, data={'username': username,
'password': password})
# login form: method=post action=login [no id]

if login_response.status_code == 200:
log.info("Authentication successful!")
token = json.loads(login_response.content)
self.TOKEN = token['token']
else:
log.exception("Authentication failed!")
token = None
# When authenticated, save password in keyring if needed
if token and password_from_keyring is None and store_password:
keyring.set_password("astroquery:archive-api.lco.global", username, password)
return

# the methods above call the private _parse_result method.
# This should parse the raw HTTP response and return it as
# an `astropy.table.Table`. Below is the skeleton:

def _parse_result(self, response, verbose=False):
# if verbose is False then suppress any VOTable related warnings
if not verbose:
commons.suppress_vo_warnings()
# try to parse the result into an astropy.Table, else
# return the raw result with an informative error message.
log.info(len(self.DTYPES),len(self.DATA_NAMES))
t = Table(names=self.DATA_NAMES, dtype=self.DTYPES)
if response['count']>0:
try:
for line in response['results']:
filtered_line = { key: line[key] for key in self.DATA_NAMES }
t.add_row(filtered_line)
except ValueError:
# catch common errors here, but never use bare excepts
# return raw result/ handle in some way
pass

return t


Lco = LcoClass()

def validate_datetime(input):
format_string = '%Y-%m-%d %H:%M'
try:
datetime.strptime(input, format_string)
return input
except ValueError:
warning.warning('Input {} is not in format {} - ignoring input'.format(input, format_string))
return ''
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@
def get_package_data():
paths = [os.path.join('data', '*.xml'),
]
return {'astroquery.lcogt.tests': paths}
return {'astroquery.lco.tests': paths}
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@

from ...utils.testing_tools import MockResponse
from ...utils import commons
from ... import lcogt
from ...lcogt import conf
from ... import lco
from ...lco import conf

DATA_FILES = {'Cone': 'Cone.xml',
'Box': 'Box.xml',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import requests
import imp

from ... import lcogt
from ... import lco

imp.reload(requests)

Expand Down