From 31ec1c0f1a0eb83af6f0617acb53c7d7e41169be Mon Sep 17 00:00:00 2001 From: Travis Rivers <travrivers88@gmail.com> Date: Mon, 24 Feb 2020 16:42:10 -0700 Subject: [PATCH] reorganize code, add more error handling --- geomagio/webservice/data.py | 241 ++++++++++++++--------- geomagio/webservice/static/usage.css | 128 +----------- geomagio/webservice/templates/usage.html | 102 ---------- 3 files changed, 152 insertions(+), 319 deletions(-) delete mode 100644 geomagio/webservice/templates/usage.html diff --git a/geomagio/webservice/data.py b/geomagio/webservice/data.py index e28bc5a4..774a194a 100644 --- a/geomagio/webservice/data.py +++ b/geomagio/webservice/data.py @@ -1,14 +1,15 @@ +from collections import OrderedDict from datetime import datetime -import os from flask import Blueprint, Flask, jsonify, render_template, request, Response -from obspy import UTCDateTime -from collections import OrderedDict from json import dumps +from obspy import UTCDateTime +import os from geomagio.edge import EdgeFactory from geomagio.iaga2002 import IAGA2002Writer from geomagio.imfjson import IMFJSONWriter + ERROR_CODE_MESSAGES = { 204: "No Data", 400: "Bad Request", @@ -19,22 +20,33 @@ ERROR_CODE_MESSAGES = { 503: "Service Unavailable" } VALID_DATA_TYPES = ["variation", "adjusted", "quasi-definitive", "definitive"] +VALID_OBSERVATORIES = ["BRT", "BRW", "DED", "DHT", "CMO", "CMT", "SIT", "SHU", "NEW", +"BDT", "BOU", "TST", "USGS", "FDT", "FRD", "FRN", "TUC", "BSL", "HON", "SJG", +"GUA", "SJT"] VALID_OUTPUT_FORMATS = ["iaga2002", "json"] VALID_SAMPLING_PERIODS = ["1", "60"] + blueprint = Blueprint("data", __name__) -factory = EdgeFactory( +edge_factory = EdgeFactory( host=os.getenv('host', 'cwbpub.cr.usgs.gov'), port=os.getenv('port', 2060), write_port=os.getenv('write_port', 7981) ) + def init_app(app: Flask): global blueprint - global factory + global edge_factory app.register_blueprint(blueprint) + +class WebServiceException(Exception): + """Base class for exceptions thrown by web services.""" + pass + + class WebServiceQuery(object): """Query parameters for a web service request. Parameters @@ -65,7 +77,7 @@ class WebServiceQuery(object): elements=("X", "Y", "Z", "F"), sampling_period=60, data_type="variation", - output_format="iaga2002", + output_format="iaga2002" ): self.observatory_id = observatory_id self.starttime = starttime @@ -86,52 +98,56 @@ class WebServiceQuery(object): raise WebServiceException( "No more than four elements allowed for iaga2002 format." ) + if self.observatory_id not in VALID_OBSERVATORIES: + raise WebServiceException( + 'Bad observatory ID "%s".' + " Valid values are: %s" % (self.observatory_id, ', '.join(VALID_OBSERVATORIES) + '.') + ) 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) + " Valid values are: %s" % (self.data_type, ', '.join(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) + " Valid values are: %s" % (self.sampling_period, ', '.join(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) + " Valid values are: %s" % (self.output_format, ', '.join(VALID_OUTPUT_FORMATS) + '.') ) + @blueprint.route("/data", methods=["GET"]) def get_data(): query_params = request.args - url = request.url - if not query_params: - return render_template("usage.html") - - parsed_query = parse_query(query_params) + return render_template("data/usage.html") try: + parsed_query = parse_query(query_params) parsed_query._verify_parameters() except Exception as e: message = str(e) - error_body = error(400, message, parsed_query, url) + error_body = error(400, message, parsed_query, request) return error_body try: timeseries = get_timeseries(parsed_query) + return format_timeseries(timeseries, parsed_query) except Exception as e: message = str(e) - error_body = error(500, message, parsed_query, url) + error_body = error(500, message, parsed_query, request) return error_body - return format_timeseries(timeseries, parsed_query) def error(code, message, query, url): + """Assign error_body value based on error format.""" error_body = http_error(code, message, query, url) status = str(code) + ' ' + ERROR_CODE_MESSAGES[code] @@ -139,15 +155,127 @@ def error(code, message, query, url): return error_body -def http_error(code, message, query, url): + +def format_timeseries(timeseries, query): + """Formats timeseries into JSON or IAGA data + + Parameters + ---------- + obspy.core.Stream + timeseries object with requested data + + WebServiceQuery + parsed query object + + Returns + ------- + unicode + IAGA2002 or JSON formatted string. + """ + if query.output_format == "json": + json_output = IMFJSONWriter.format(timeseries, query.elements) + json_output = Response(json_output, mimetype="application/json") + + return json_output + + else: + iaga_output = IAGA2002Writer.format(timeseries, query.elements) + iaga_output = Response(iaga_output, mimetype="text/plain") + + return iaga_output + + +def get_interval(query): + """ + Parameters + ---------- + WebServiceQuery + parsed query object + + Returns + ------- + WebServiceQuery + with sampling period converted to interval + """ + if query.sampling_period == "1": + query.sampling_period = "second" + + if query.sampling_period == "60": + query.sampling_period = "minute" + + return query + + +def get_timeseries(query): + """ + Parameters + ---------- + WebServiceQuery + parsed query object + + Returns + ------- + obspy.core.Stream + timeseries object with requested data + """ + get_interval(query) + + timeseries = edge_factory.get_timeseries( + query.starttime, + query.endtime, + query.observatory_id, + query.elements, + query.data_type, + query.sampling_period) + + return timeseries + + +def http_error(code, message, query, request): + """Format http error message. + + Returns + ------- + http_error_body : str + body of http error message. + """ if query.output_format == 'json': - http_error_body = json_error(code, message, url) + http_error_body = json_error(code, message, request.url) return http_error_body else: - http_error_body = iaga2002_error(code, message, url) + http_error_body = iaga2002_error(code, message, request.query_string) return http_error_body + +def iaga2002_error(code, message, request_args): + """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' + 'ws/edge/?' + str(request_args)[2:-1] + '\n\n' + 'Request Submitted:\n'\ + + datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ") + '\n' + + error_body = Response(error_body, mimetype="text/plain") + return error_body + + def json_error(code, message, url): + """Format json error message. + + Returns + ------- + error_body : str + body of json error message. + """ error_dict = OrderedDict() error_dict['type'] = "Error" error_dict['metadata'] = OrderedDict() @@ -163,17 +291,6 @@ def json_error(code, message, url): return error_body -def iaga2002_error(code, message, url): - 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' + url + '\n\n' + 'Request Submitted:\n'\ - + datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ") + '\n' - - error_body = Response(error_body, mimetype="text/plain") - return error_body def parse_query(query): """Parse request arguments into a set of parameters @@ -231,64 +348,4 @@ def parse_query(query): params.starttime = start_time params.endtime = end_time - return params - -def get_timeseries(query): - """ - Parameters - ---------- - WebServiceQuery - parsed query object - - Returns - ------- - obspy.core.Stream - timeseries object with requested data - """ - if query.sampling_period == "1": - query.sampling_period = "second" - - if query.sampling_period == "60": - query.sampling_period = "minute" - - timeseries = factory.get_timeseries( - query.starttime, - query.endtime, - query.observatory_id, - query.elements, - query.data_type, - query.sampling_period) - - return timeseries - -def format_timeseries(timeseries, query): - """Formats timeseries into JSON or IAGA data - - Parameters - ---------- - obspy.core.Stream - timeseries object with requested data - - WebServiceQuery - parsed query object - - Returns - ------- - unicode - IAGA2002 or JSON formatted string. - """ - if query.output_format == "json": - json_output = IMFJSONWriter.format(timeseries, query.elements) - json_output = Response(json_output, mimetype="application/json") - - return json_output - - else: - iaga_output = IAGA2002Writer.format(timeseries, query.elements) - iaga_output = Response(iaga_output, mimetype="text/plain") - - return iaga_output - -class WebServiceException(Exception): - """Base class for exceptions thrown by web services.""" - pass + return params \ No newline at end of file diff --git a/geomagio/webservice/static/usage.css b/geomagio/webservice/static/usage.css index c951334f..0bb4b75d 100644 --- a/geomagio/webservice/static/usage.css +++ b/geomagio/webservice/static/usage.css @@ -2,142 +2,20 @@ p { font-weight: bold; } -/* Import your custom theme */ -.hazdev-site-header { - background-color: #1b3c43; - height: 100px; - position: relative; -} -/* position usgs logo over background. */ -.hazdev-site-header > .hazdev-site-logo { - /* bottom padding of 0 + overflow: hidden + image height 140% = hide "science for a changing world" on small screens. */ - display: block; - height: 30px; - left: 0; - overflow: hidden; - padding: 14px 14px 0; - position: absolute; - top: 0; -} -.hazdev-site-header > .hazdev-site-logo > img { - height: 140%; - border: none; -} -hazdev-template-cooperator { - display: none; - height: 100%; - left: 136px; - overflow: hidden; - padding-left: 1em; - position: absolute; -} -/* visually hide navigation jumplink. */ -.hazdev-jumplink-navigation { - display: block; - left: -9999px; - position: absolute; - top: 0; -} -@media screen and (min-width: 768px) { - /* make banner larger on large screens. */ - /* also adjust size of usgs logo */ - /*width: 157px; - */ - .hazdev-site-header { - background-color: #1b3c43; - background-position: 186px 0; - background-repeat: no-repeat; - background-size: auto 100%; - height: 90px; - /* 1.5 line-height on 1em text, with 0.25em padding top/bottom */ - margin-bottom: 1em; - } - .hazdev-site-header > .hazdev-site-logo { - height: 100%; - padding: 0; - } - .hazdev-site-header > .hazdev-site-logo > img { - height: 100%; - padding: 1em; - } - hazdev-template-cooperator { - background-image: none; - display: block; - left: 186px; - padding-left: 0; - } -} -@media print { - /** Only show USGS logo when printing, requires background colors... */ - [role='banner'] { - background-color: #fff; - height: 0; - margin: 0; - padding: 0; - visibility: hidden; - width: 100%; - } - [role='banner']:after { - color: #333; - content: 'U.S. Geological Survey - National Geomagnetism Program'; - display: block; - visibility: visible; - } - [role='main'] { - padding: 1em 0 0 !important; - } -} - -.site-commonnav { - display: block; - font-size: 0.88em; - margin: 4em 0 0; - padding: 2em 1em; - position: relative; -} -.site-commonnav > .hazdev-site-logo { - display: block; - height: 56px; - margin-bottom: 2em; - width: 152px; -} -.site-commonnav > .hazdev-site-logo > img { - height: 100%; - border: none; -} -.interior-nav, -.common-nav, -.social-nav, .space-between { margin: 1em 0 0; } -.interior-nav > a, -.common-nav > a, -.social-nav > a, + .space-between > a { color: #fff; display: inline-block; margin: 0.5em 1em 0.5em 0; } -.interior-nav > a:hover, -.common-nav > a:hover, -.social-nav > a:hover, -.space-between > a:hover, -.interior-nav > a:visited, -.common-nav > a:visited, -.social-nav > a:visited, -.space-between > a:visited, -.interior-nav > a:active, -.common-nav > a:active, -.social-nav > a:active, + .space-between > a:active { color: #fff; } -@media print { - .site-commonnav { - display: none; - } -} + .space-between { border-top: 1px solid #eee; } diff --git a/geomagio/webservice/templates/usage.html b/geomagio/webservice/templates/usage.html deleted file mode 100644 index ff32b308..00000000 --- a/geomagio/webservice/templates/usage.html +++ /dev/null @@ -1,102 +0,0 @@ -{% extends '_template.html' %} {% block header %} -<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='usage.css') }}"/> - -{% endblock %} {% block content %} -<header role="banner" class="hazdev-site-header"> - <a - class="hazdev-site-logo" - href="https://www.usgs.gov/" - title="U.S. Geological Survey" - > - <img src="/static/usgs-logo.svg" alt="U.S. Geological Survey logo" /> - </a> - <a class="hazdev-jumplink-navigation" href="#site-sectionnav"> - Jump to Navigation - </a> - <ng-content></ng-content> -</header> - - - <div class="views-field views-field-field-intro"> <div class="field-content lead pane-border"><p><strong>Example Requests</strong><br /></p><dl><dt>BOU observatory data for current UTC day in IAGA2002 format</dt> -<dd><a href="http://geomag.usgs.gov/ws/edge/?id=BOU">http://geomag.usgs.gov/ws/edge/?id=BOU</a></dd> -<dt>BOU observatory data for current UTC day in JSON format</dt> -<dd><a href="http://geomag.usgs.gov/ws/edge/?id=BOU&format=json">http://geomag.usgs.gov/ws/edge/?id=BOU&format=json</a></dd> -<dt>BOU electric field data for current UTC day in IAGA2002 format</dt> -<dd><a href="http://geomag.usgs.gov/ws/edge/?id=BOU&elements=E-N,E-E">http://geomag.usgs.gov/ws/edge/?id=BOU&elements=E-N,E-E</a></dd> -</dl><p><a href="/natural-hazards/geomagnetism/science/more-examples">See more examples</a></p> -</div> </div> - <div class="views-field views-field-field-science-tab-intro"> <div class="field-content"></div> </div> - <div class="views-field views-field-field-science-object-body"> <div class="field-content"><div class="tex2jax"><p><strong>Request Limits</strong></p> -<p>To ensure availability for users, the web service restricts the amount of data that can be retrieved in one request. The amount of data requested is computed as follows, where an interval is the number of seconds between start time and end time:</p> -<p>samples = count(elements) * interval / sampling_period</p> -<p><strong>Limits by the output format</strong></p> -<p><strong>json</strong></p> -<ul><li>172800 samples = 4 elements * 12 hours * 3600 samples/hour.</li> -</ul><p><strong>iaga2002</strong></p> -<ul><li>345600 samples = 4 elements * 24 hours * 3600 samples/hour.</li> -<li>NOTE: while the json format supports fewer total samples per request, users may request fewer elements to retrieve longer intervals.</li> -</ul><p><strong>Parameters</strong></p> -<p><strong>id</strong></p> -<ul><li>Observatory code. Required.</li> -<li>Valid values: BDT, BOU, TST, BRW, BRT, BSL, CMO, CMT, DED, DHT, FRD, FRN, GUA, HON, NEW, SHU, SIT, SJG, TUC, USGS, BLC, BRD, CBB, EUA, FCC, IQA, MEA, OTT, RES, SNK, STJ, VIC, YKC, HAD, HER, KAK</li> -</ul><p><strong>starttime</strong></p> -<ul><li>Time of first requested data.</li> -<li>Default: start of current UTC day</li> -<li>Format: ISO8601 (YYYY-MM-DDTHH:MM:SSZ)</li> -<li>Example: 2018-08-06T22:10:14Z</li> -</ul><p><strong>endtime</strong></p> -<ul><li>Time of last requested data.</li> -<li>Default: starttime + 24 hours</li> -<li>Format: ISO8601 (YYYY-MM-DDTHH:MM:SSZ)</li> -<li>Example: 2018-08-06T22:10:14Z</li> -</ul><p><strong>elements</strong></p> -<ul><li>Comma separated list of requested elements.</li> -<li>Default: X,Y,Z,F</li> -<li>Valid values: D, DIST, DST, E, E-E, E-N, F, G, H, SQ, SV, UK1, UK2, UK3, UK4, X, Y, Z</li> -</ul><p><strong>sampling_period</strong></p> -<ul><li>Interval in seconds between values.</li> -<li>Default: 60</li> -<li>Valid values: 1, 60, 3600</li> -</ul><p><strong>type</strong></p> -<ul><li>Type of data.</li> -<li>Default: variation Valid values: variation, adjusted, quasi-definitive,definitive</li> -<li>NOTE: the USGS web service also supports specific EDGE location codes. For example: R0 is "internet variation",R1 is "satellite variation".</li> -</ul><p><strong>format</strong></p> -<ul><li>Output format.</li> -<li>Default: iaga2002</li> -<li>Valid values: iaga2002, json.</li> -</ul></div></div> </div> - - -<footer class="site-commonnav"> - <a class="hazdev-site-logo" href="/" title="U.S. Geological Survey"> - <img src="/static/usgs-logo.svg" alt="U.S. Geological Survey logo" /> - </a> - <div class="interior-nav"> - <a href="https://www.doi.gov/"> - U.S. Department of the Interior - </a> - <a href="https://www.usgs.gov/"> - U.S. Geological Survey - </a> - </div> - <div class="space-between"> - <div class="common-nav"> - <a href="https://www.doi.gov/privacy"> - DOI Privacy Policy - </a> - <a href="https://www.usgs.gov/policies-and-notices"> - Legal - </a> - <a href="https://www.usgs.gov/sitemap"> - Site Map - </a> - <a href="https://answers.usgs.gov/"> - Contact USGS - </a> - </div> - - </div> -</footer></body> -</html> -{% endblock %} \ No newline at end of file -- GitLab