Skip to content
Snippets Groups Projects
WebService.py 13.6 KiB
Newer Older
  • Learn to ignore specific revisions
  • """WSGI implementation of Intermagnet Web Service
    """
    
    from __future__ import print_function
    
    from cgi import escape, parse_qs
    
    from collections import OrderedDict
    
    from datetime import datetime
    
    Jeremy M Fee's avatar
    Jeremy M Fee committed
    import sys
    
    
    from geomagio.edge import EdgeFactory
    from geomagio.iaga2002 import IAGA2002Writer
    
    from geomagio.imfjson import IMFJSONWriter
    
    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', 'json']
    
    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()
    
                self.output_format = query.output_format
    
            except Exception as e:
                message = str(e)
    
                ftype = parse_qs(environ['QUERY_STRING']).get('format', [''])[0]
                if ftype == 'json':
                    self.output_format = 'json'
                else:
                    self.output_format = 'iaga2002'
    
                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, environ)
    
                if isinstance(timeseries_string, str):
                    timeseries_string = timeseries_string.encode('utf8')
    
            except Exception as e:
    
    Jeremy M Fee's avatar
    Jeremy M Fee committed
                print("Error processing request: %s" % str(e),
                        file=sys.stderr)
    
                message = "Server error."
    
                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."""
    
            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.
    
            query : dict
                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, environ):
    
            """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.
    
            url = environ['HTTP_HOST'] + environ['PATH_INFO'] + \
                    environ['QUERY_STRING']
            if query.output_format == 'json':
                timeseries_string = IMFJSONWriter.format(timeseries,
                        query.elements, url)
            else:
                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
            -------
    
                body of http error message.
            """
    
            query_string = environ['QUERY_STRING']
            path_info = environ['PATH_INFO']
            host = environ['HTTP_HOST']
            if self.output_format == 'json':
                http_error_body = self.json_error(code, message, path_info,
                        query_string, host)
            else:
                http_error_body = self.iaga2002_error(code, message, path_info,
                        query_string)
            return http_error_body
    
        def iaga2002_error(self, code, message, path_info, query_string):
            """Format iaga2002 error message.
    
            Returns
            -------
            error_body : str
                body of iaga2002 error message.
            """
    
            status_message = ERROR_CODE_MESSAGES[code]
    
            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' + \
    
                    path_info + '?' + 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:
    
                error_body += 'Service version:\n' + str(self.version)
            return error_body
    
        def json_error(self, code, message, path_info, query_string, host):
            """Format json error message.
    
            Returns
            -------
            error_body : str
                body of json error message.
            """
            error_dict = OrderedDict()
            error_dict['type'] = "Error"
            error_dict['metadata'] = OrderedDict()
            error_dict['metadata']['status'] = 400
            date = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.%fZ")
            error_dict['metadata']['generated'] = date
            error_dict['metadata']['url'] = host + path_info + '?' + query_string
            status_message = ERROR_CODE_MESSAGES[code]
            error_dict['metadata']['title'] = status_message
            error_dict['metadata']['api'] = str(self.version)
            error_dict['metadata']['error'] = message
            error_body = dumps(error_dict,
                    ensure_ascii=True).encode('utf8')
            error_body = str(error_body)
            return 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)
    
                            '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)
    
                            '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