Skip to content
Snippets Groups Projects
WebService.py 11.2 KiB
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_DATA_TYPE = 'variation'
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'
}
VALID_DATA_TYPES = [
        '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
    ------
    WebServiceException
        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


class WebService(object):
    def __init__(self, factory=None, version=None, metadata=None):
        self.factory = factory or EdgeFactory()
        self.metadata = metadata or ObservatoryMetadata().metadata

    def __call__(self, environ, start_response):
        """Implement WSGI interface"""
        try:
            # parse params
            query = self.parse(parse_qs(environ['QUERY_STRING']))
            query._verify_parameters()
        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

    def fetch(self, query):
        """Get requested timeseries.

        Parameters
        ----------
        query : dictionary of parsed query parameters

        Returns
        -------
        obspy.core.Stream
            timeseries object with requested data.
        """
        if query.sampling_period == '1':
            sampling_period = 'second'
        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,
                type=query.data_type,
                interval=sampling_period)
        return timeseries
    def format_data(self, query, timeseries, start_response):
        """Format requested timeseries.

        Parameters
        ----------
        query : dictionary of parsed query parameters
        timeseries : obspy.core.Stream
            timeseries object with data to be written

        Returns
        -------
        unicode
          IAGA2002 formatted string.
        """
        # 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)
        return http_error_body
    def parse(self, params):
        """Parse query string parameters and set defaults.
        params : dictionary
            parameters dictionary.

        Returns
        -------
        WebServiceQuery
            parsed query object.
        WebServiceException
            if any parameters are not supported.
        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():
                   '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)
            except TypeError:
                        '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)
            except TypeError:
                        '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(',', '')]
        if not sampling_period:
            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 = WebServiceQuery()
        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
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