Newer
Older
"""WSGI implementation of Intermagnet Web Service
"""
from __future__ import print_function
from cgi import parse_qs, escape
from datetime import datetime
from geomagio.edge import EdgeFactory
from geomagio.iaga2002 import IAGA2002Writer
from geomagio.ObservatoryMetadata import ObservatoryMetadata
from obspy.core import UTCDateTime
DEFAULT_ELEMENTS = ('X', 'Y', 'Z', 'F')
DEFAULT_OUTPUT_FORMAT = 'iaga2002'
DEFAULT_SAMPLING_PERIOD = '60'
ERROR_CODE_MESSAGES = {
204: 'No Data',
400: 'Bad Request',
404: 'Not Found',
409: 'Conflict',
500: 'Internal Server Error',
501: 'Not Implemented',
503: 'Service Unavailable'
}
'variation',
'adjusted',
'quasi-definitive',
'definitive'
]
VALID_OUTPUT_FORMATS = ['iaga2002']
VALID_SAMPLING_PERIODS = ['1', '60']
def _get_param(params, key, required=False):
"""Get parameter from dictionary.
Parameters
----------
params : dict
parameters dictionary.
key : str
parameter name.
required : bool
if required parameter.
Returns
-------
value : str
value from dictionary.
Raises
------
if the parameter is specified more than once
or if required paramenter is not specified.
"""
value = params.get(key)
if isinstance(value, (list, tuple)):
if len(value) > 1:
raise WebServiceException('"' + key +
'" may only be specified once.')
value = escape(value[0])
if value is None:
if required:
raise WebServiceException('"' + key +
'" is a required parameter.')
return value
def __init__(self, factory=None, version=None, metadata=None):
self.factory = factory or EdgeFactory()
self.metadata = metadata or ObservatoryMetadata().metadata
self.version = version
def __call__(self, environ, start_response):
"""Implement WSGI interface"""
try:
# parse params
query = self.parse(parse_qs(environ['QUERY_STRING']))
except Exception:
exception = sys.exc_info()[1]
message = exception.args[0]
error_body = self.error(400, message, environ, start_response)
return [error_body]
try:
# fetch timeseries
timeseries = self.fetch(query)
# format timeseries
timeseries_string = self.format_data(
query, timeseries, start_response)
if isinstance(timeseries_string, str):
timeseries_string = timeseries_string.encode('utf8')
except Exception:
exception = sys.exc_info()[1]
message = exception.args[0]
error_body = self.error(500, message, environ, start_response)
return [error_body]
return [timeseries_string]
def error(self, code, message, environ, start_response):
"""Assign error_body value based on error format."""
# TODO: Add option for json formatted error
error_body = self.http_error(code, message, environ)
status = str(code) + ' ' + ERROR_CODE_MESSAGES[code]
start_response(status,
[
("Content-Type", "text/plain")
])
if isinstance(error_body, str):
error_body = error_body.encode('utf8')
return error_body
Parameters
----------
query : dictionary of parsed query parameters
Returns
-------
obspy.core.Stream
timeseries object with requested data.
"""
if query.sampling_period == '1':
if query.sampling_period == '60':
sampling_period = 'minute'
timeseries = self.factory.get_timeseries(
observatory=query.observatory_id,
channels=query.elements,
starttime=query.starttime,
endtime=query.endtime,
interval=sampling_period)
return timeseries
def format_data(self, query, timeseries, start_response):
Parameters
----------
query : dictionary of parsed query parameters
timeseries object with data to be written
Returns
-------
unicode
"""
# TODO: Add option for json format
timeseries_string = IAGA2002Writer.format(timeseries, query.elements)
start_response('200 OK',
[
("Content-Type", "text/plain")
])
return timeseries_string
def http_error(self, code, message, environ):
"""Format http error message.
Returns
-------
error_body : str
body of http error message.
"""
status_message = ERROR_CODE_MESSAGES[code]
http_error_body = 'Error ' + str(code) + ': ' + status_message + \
'\n\n' + message + '\n\n' + \
'Usage details are available from ' + \
'http://geomag.usgs.gov/ws/edge/ \n\n' + \
'Request:\n' + \
environ['PATH_INFO'] + '?' + environ['QUERY_STRING'] + \
'\n\n' + 'Request Submitted:\n' + \
datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ") + '\n\n'
# Check if there is version information available
if self.version is not None:
http_error_body += 'Service version:\n' + str(self.version)
"""Parse query string parameters and set defaults.
Parameters
----------
params : dictionary
parameters dictionary.
Returns
-------
WebServiceQuery
parsed query object.
if any parameters are not supported.
# Get values
observatory_id = _get_param(params, 'id', required=True)
starttime = _get_param(params, 'starttime')
endtime = _get_param(params, 'endtime')
elements = _get_param(params, 'elements')
sampling_period = _get_param(params, 'sampling_period')
data_type = _get_param(params, 'type')
output_format = _get_param(params, 'format')
# Assign values or defaults
if not output_format:
output_format = DEFAULT_OUTPUT_FORMAT
output_format = output_format.lower()
observatory_id = observatory_id.upper()
if observatory_id not in self.metadata.keys():
raise WebServiceException(
'Bad id value "%s".'
' Valid values are: %s'
% (observatory_id, self.metadata.keys()))
if not starttime:
now = datetime.now()
today = UTCDateTime(
year=now.year,
month=now.month,
day=now.day,
hour=0)
starttime = today
else:
try:
starttime = UTCDateTime(starttime)
raise WebServiceException(
'Bad starttime value "%s".'
' Valid values are ISO-8601 timestamps.' % starttime)
if not endtime:
endtime = starttime + (24 * 60 * 60 - 1)
else:
try:
endtime = UTCDateTime(endtime)
raise WebServiceException(
'Bad endtime value "%s".'
' Valid values are ISO-8601 timestamps.' % endtime)
if not elements:
elements = DEFAULT_ELEMENTS
elements = [e.strip().upper() for e in elements.replace(',', '')]
sampling_period = DEFAULT_SAMPLING_PERIOD
else:
sampling_period = sampling_period
if not data_type:
data_type = DEFAULT_DATA_TYPE
else:
data_type = data_type.lower()
# Create WebServiceQuery object and set properties
query.observatory_id = observatory_id
query.starttime = starttime
query.endtime = endtime
query.elements = elements
query.sampling_period = sampling_period
query.data_type = data_type
query.output_format = output_format
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
class WebServiceQuery(object):
"""Query parameters for a web service request.
Parameters
----------
observatory_id : str
observatory
starttime : obspy.core.UTCDateTime
time of first requested sample
endtime : obspy.core.UTCDateTime
time of last requested sample
elements : array_like
list of requested elements
sampling_period : int
period between samples in seconds
default 60.
data_type : {'variation', 'adjusted', 'quasi-definitive', 'definitive'}
data type
default 'variation'.
output_format : {'iaga2002', 'json'}
output format.
default 'iaga2002'.
"""
def __init__(self, observatory_id=None, starttime=None, endtime=None,
elements=None, sampling_period=60, data_type='variation',
output_format='iaga2002'):
self.observatory_id = observatory_id
self.starttime = starttime
self.endtime = endtime
self.elements = elements
self.sampling_period = sampling_period
self.data_type = data_type
self.output_format = output_format
def _verify_parameters(self):
"""Verify that parameters are valid.
Raises
------
WebServiceException
if any parameters are not supported.
"""
if len(self.elements) > 4 and self.output_format == 'iaga2002':
raise WebServiceException(
'No more than four elements allowed for iaga2002 format.')
if self.starttime > self.endtime:
raise WebServiceException(
'Starttime must be before endtime.')
if self.data_type not in VALID_DATA_TYPES:
raise WebServiceException(
'Bad type value "%s".'
' Valid values are: %s'
% (self.data_type, VALID_DATA_TYPES))
if self.sampling_period not in VALID_SAMPLING_PERIODS:
raise WebServiceException(
'Bad sampling_period value "%s".'
' Valid values are: %s'
% (self.sampling_period, VALID_SAMPLING_PERIODS))
if self.output_format not in VALID_OUTPUT_FORMATS:
raise WebServiceException(
'Bad format value "%s".'
' Valid values are: %s'
% (self.output_format, VALID_OUTPUT_FORMATS))
class WebServiceException(Exception):
"""Base class for exceptions thrown by web services."""
pass