From b3bc7a8e748327280fc17f24731699bac68616ef Mon Sep 17 00:00:00 2001 From: Heather Schovanec <hschovanec@usgs.gov> Date: Thu, 24 Aug 2017 09:13:38 -0600 Subject: [PATCH] Add WebServiceError class Added a class to create error messages and wrapped parse, fetch, and format_data in try/except statement to catch raised exceptions and generate error pages. --- geomagio/WebService.py | 410 ++++++++++++++++++++++++++--------------- 1 file changed, 261 insertions(+), 149 deletions(-) diff --git a/geomagio/WebService.py b/geomagio/WebService.py index c634fd47..a9344e40 100644 --- a/geomagio/WebService.py +++ b/geomagio/WebService.py @@ -2,42 +2,126 @@ """ from __future__ import print_function -from builtins import str from cgi import parse_qs, escape from datetime import datetime +import sys 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_PERIOD = '60' -DEFAULT_TYPE = 'variation' -VALID_TYPES = [ +DEFAULT_OUTPUT_FORMAT = 'iaga2002' +DEFAULT_SAMPLING_PERIOD = '60' +VALID_DATA_TYPES = [ 'variation', 'adjusted', 'quasi-definitive', 'definitive' ] -VALID_PERIODS = ['1', '60'] -VALID_FORMATS = ['iaga2002'] +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 + ------ + WebServiceError + if the parameter is specified more than once + or if required paramenter is not specified. + """ + value = params.get(key) + if type(value) in (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 _verify_parameters(query): + """Verify that parameters are valid. + + Parameters + ---------- + query : WebServiceQuery + parsed query object. + + Raises + ------ + WebServiceError + if any parameters are not supported. + """ + if len(query.elements) > 4 and query.output_format == 'iaga2002': + raise WebServiceException( + 'No more than 4 elements allowed for iaga2002 format.') + if query.starttime > query.endtime: + raise WebServiceException( + 'Starttime must be before endtime.') + if query.data_type not in VALID_DATA_TYPES: + raise WebServiceException( + 'Bad type value "%s".' + ' Valid values are: %s' % (query.data_type, VALID_DATA_TYPES)) + if query.sampling_period not in VALID_SAMPLING_PERIODS: + raise WebServiceException( + 'Bad sampling_period value "%s".' + ' Valid values are: %s' + % (query.sampling_period, VALID_SAMPLING_PERIODS)) + if query.output_format not in VALID_OUTPUT_FORMATS: + raise WebServiceException( + 'Bad format value "%s".' + ' Valid values are: %s' + % (query.output_format, VALID_OUTPUT_FORMATS)) class WebService(object): - def __init__(self, factory): + def __init__(self, factory, metadata=None): self.factory = factory + self.metadata = metadata or ObservatoryMetadata().metadata def __call__(self, environ, start_response): """Implement WSGI interface""" - # parse params - query = WebServiceQuery.parse(environ['QUERY_STRING']) - # fetch data - data = self.fetch(query) - # format data - data_string = self.format(query, data) - if isinstance(data_string, str): - data_string = data_string.encode('utf8') + try: + # parse params + query = self.parse(parse_qs(environ['QUERY_STRING'])) + # fetch data + data = self.fetch(query) + # format data + data_string = self.format_data(query, data) + if isinstance(data_string, str): + data_string = data_string.encode('utf8') + except Exception: + exception = sys.exc_info()[1] + message = exception.args[0] + error = WebServiceError('BAD_REQUEST', message, environ) + start_response(error.status, + [ + ("Content-Type", "text/plain") + ]) + return [error.error_body] # send response start_response('200 OK', [ @@ -57,16 +141,21 @@ class WebService(object): obspy.core.Stream timeseries object with requested data. """ + _verify_parameters(query) + if query.sampling_period == '1': + query.sampling_period = 'second' + if query.sampling_period == '60': + query.sampling_period = 'minute' data = self.factory.get_timeseries( - observatory=query.id, + observatory=query.observatory_id, channels=query.elements, starttime=query.starttime, endtime=query.endtime, - type=query.type, + type=query.data_type, interval=query.sampling_period) return data - def format(self, query, data): + def format_data(self, query, data): """Format requested data. Parameters @@ -78,53 +167,19 @@ class WebService(object): Returns ------- unicode - IMFJSON or IAGA2002 formatted string. + IAGA2002 formatted string. """ # TODO: Add option for json format data_string = IAGA2002Writer.format(data, query.elements) return data_string - -class WebServiceQuery(object): - """Query parameters for a web service request. - - Parameters - ---------- - 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. - type : {'variation', 'adjusted', 'quasi-definitive', 'definitive'} - data type - default 'variation'. - format : {'iaga2002', 'json'} - output format. - default 'iaga2002'. - """ - def __init__(self, id=None, starttime=None, endtime=None, elements=None, - sampling_period=60, type='variation', format='iaga2002'): - self.id = id - self.starttime = starttime - self.endtime = endtime - self.elements = elements - self.sampling_period = sampling_period - self.type = type - self.format = format - - @classmethod - def parse(cls, params): + def parse(self, params): """Parse query string parameters and set defaults. Parameters ---------- - params : query string + params : dictionary + parameters dictionary. Returns ------- @@ -133,132 +188,189 @@ class WebServiceQuery(object): Raises ------ - TimeseriesFactoryException - if id, type, sampling_period, or format are not supported. + WebServiceError + if any parameters are not supported. """ - # Create dictionary of lists - dict = parse_qs(params) # Get values - if len(dict.get('id', [])) <= 1: - id = dict.get('id', [''])[0] - else: - raise WebServiceException( - '"id" accepts only one value') - if len(dict.get('starttime', [])) <= 1: - starttime = dict.get('starttime', [''])[0] - else: - raise WebServiceException( - '"starttime" accepts only one value') - if len(dict.get('endtime', [])) <= 1: - endtime = dict.get('endtime', [''])[0] - else: - raise WebServiceException( - '"endtime" accepts only one value') - if len(dict.get('elements', [])) <= 1: - elements = dict.get('elements', [''])[0] + 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 else: + output_format = output_format.lower() + observatory_id = observatory_id.upper() + if observatory_id not in self.metadata.keys(): raise WebServiceException( - '"elements" accepts only one set of values') - if len(dict.get('sampling_period', [])) <= 1: - sampling_period = dict.get('sampling_period', [''])[0] - else: - raise WebServiceException( - '"sampling_period" accepts only one value') - if len(dict.get('type', [])) <= 1: - type = dict.get('type', [''])[0] - else: - raise WebServiceException( - '"type" accepts only one value') - if len(dict.get('format', [])) <= 1: - format = dict.get('format', [''])[0] + '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: - raise WebServiceException( - '"format" accepts only one value') - # Escape to avoid script injection - id = escape(id) - starttime = escape(starttime) - endtime = escape(endtime) - elements = escape(elements) - sampling_period = escape(sampling_period) - type = escape(type).lower() - format = escape(format) - # Check for parameters and set defaults - if not id: - raise WebServiceException( - '"id" is a required parameter') - now = datetime.now() - if starttime: try: starttime = UTCDateTime(starttime) except: raise WebServiceException( - 'Invalid starttime "%s"' % starttime) + 'Bad starttime value "%s".' + ' Valid values are ISO-8601 timestamps.' % starttime) + if not endtime: + endtime = starttime + (24 * 60 * 60 - 1) else: - starttime = UTCDateTime( - year=now.year, - month=now.month, - day=now.day, - hour=0) - if endtime: try: endtime = UTCDateTime(endtime) except: raise WebServiceException( - 'Invalid endtime "%s"' % endtime) + 'Bad endtime value "%s".' + ' Valid values are ISO-8601 timestamps.' % endtime) + if not elements: + elements = DEFAULT_ELEMENTS else: - endtime = starttime + (24 * 60 * 60 - 1) - if starttime > endtime: - raise WebServiceException( - 'Starttime before endtime "%s" "%s"' - % (starttime, endtime)) - if elements: elements = [el.strip().upper() for el in elements.split(',')] - else: - elements = DEFAULT_ELEMENTS if not sampling_period: - sampling_period = DEFAULT_PERIOD - if sampling_period not in VALID_PERIODS: - raise WebServiceException( - 'Invalid sampling period.' - ' Valid sampling periods: %s' % VALID_PERIODS) - # TODO: Add hourly option - if sampling_period == '1': - sampling_period = 'second' - if sampling_period == '60': - sampling_period = 'minute' - if not type: - type = DEFAULT_TYPE - if type not in VALID_TYPES: - raise WebServiceException( - 'Invalid data type.' - ' Valid data types: %s' % VALID_TYPES) - # TODO: Add json to valid formats - if not format: - format = 'iaga2002' - if format not in VALID_FORMATS: - raise WebServiceException( - 'Invalid format.' - ' Valid formats: %s' % VALID_FORMATS) + 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.id = id + query.observatory_id = observatory_id query.starttime = starttime query.endtime = endtime query.elements = elements query.sampling_period = sampling_period - query.type = type - query.format = format + query.data_type = data_type + query.output_format = output_format return query +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 + + class WebServiceException(Exception): """Base class for exceptions thrown by web services.""" pass +class WebServiceError(object): + """Base class for creating error pages.""" + def __init__(self, code, message, environ): + self.code = code + self.message = message + self.environ = environ + self.error_body = None + self.error_types = { + 'NO_DATA': { + 'code': 204, + 'status': 'No Data' + }, + 'BAD_REQUEST': { + 'code': 400, + 'status': 'Bad Request' + }, + 'NOT_FOUND': { + 'code': 404, + 'status': 'Not Found' + }, + 'CONFLICT': { + 'code': 409, + 'status': 'Conflict' + }, + 'SERVER_ERROR': { + 'code': 500, + 'status': 'Internal Server Error' + }, + 'NOT_IMPLEMENTED': { + 'code': 501, + 'status': 'Not Implemented' + }, + 'SERVICE_UNAVAILABLE': { + 'code': 503, + 'status': 'Service Unavailable' + }, + } + self.status = None + self.error() + + def error(self): + """Assign error_body value based on error format.""" + # TODO: Add option for json formatted error + self.error_body = self.http_error() + + def http_error(self): + """Format http error message. + + Returns + ------- + error_body : str + body of http error message. + """ + code_message = str(self.error_types[self.code]['code']) + status_message = self.error_types[self.code]['status'] + self.status = code_message + ' ' + status_message + server_software = self.environ['SERVER_SOFTWARE'].split(' ') + error_body = 'Error ' + code_message + ': ' + status_message + \ + '\n\n' + self.message + '\n\n' + \ + 'Usage details are available from ' + \ + 'http://geomag.usgs.gov/ws/edge/ \n\n' + \ + 'Request:\n' + \ + self.environ['QUERY_STRING'] + '\n\n' + \ + 'Request Submitted:\n' + \ + datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ") + '\n\n' + \ + 'Service version:\n' + \ + server_software[0] + return error_body + + if __name__ == '__main__': from wsgiref.simple_server import make_server app = WebService(EdgeFactory()) - httpd = make_server('', 8080, app) + httpd = make_server('', 7981, app) httpd.serve_forever() -- GitLab