Skip to content
Snippets Groups Projects
WebService.py 11.5 KiB
Newer Older
  • Learn to ignore specific revisions
  • """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 geomagio.WebServiceUsage import WebServiceUsage
    
    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,
                usage_documentation=None):
    
            self.factory = factory or EdgeFactory()
    
            self.metadata = metadata or ObservatoryMetadata().metadata
    
            self.usage_documentation = usage_documentation or WebServiceUsage()
    
    
        def __call__(self, environ, start_response):
            """Implement WSGI interface"""
    
            if environ['QUERY_STRING'] == '':
    
                return self.usage_documentation.__call__(environ, start_response)
    
            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