diff --git a/geomagio/API/data_api.py b/geomagio/API/data_api.py index 3905850caaeb1eaef0b3e1a1bf4aba62c59d6ecd..be73b2ce1fef572861af8d5257d3e48d9e62da81 100644 --- a/geomagio/API/data_api.py +++ b/geomagio/API/data_api.py @@ -1,22 +1,24 @@ -from fastapi import FastAPI, Query, HTTPException, Request -from fastapi.responses import JSONResponse -from pydantic import BaseModel +from datetime import datetime +from fastapi import FastAPI, Query, Request +from json import dumps from obspy import UTCDateTime +import os +from pydantic import BaseModel +from starlette.responses import Response from typing import List, Any -from datetime import datetime -from fastapi.exceptions import RequestValidationError -from fastapi.responses import PlainTextResponse -from starlette.exceptions import HTTPException as StarletteHTTPException -from flask.json import dumps +from ..edge import EdgeFactory +from ..iaga2002 import IAGA2002Writer +from ..imfjson import IMFJSONWriter +from ..TimeseriesUtility import get_interval_from_delta -app = FastAPI() DEFAULT_DATA_TYPE = "variation" DEFAULT_ELEMENTS = ["X", "Y", "Z", "F"] DEFAULT_OUTPUT_FORMAT = "iaga2002" DEFAULT_SAMPLING_PERIOD = "60" +DEFAULT_STARTTIME = datetime.now() ERROR_CODE_MESSAGES = { 204: "No Data", 400: "Bad Request", @@ -74,6 +76,10 @@ VALID_OBSERVATORIES = [ ] VALID_OUTPUT_FORMATS = ["iaga2002", "json"] VALID_SAMPLING_PERIODS = [0.1, 1, 60, 3600, 86400] +VERSION = "version" + + +app = FastAPI(docs_url="/data") class WebServiceException(Exception): @@ -92,22 +98,17 @@ class WebServiceQuery(BaseModel): output_format: str = DEFAULT_OUTPUT_FORMAT -@app.get("/") -def read_root(): - return {"Hello": "World"} - - @app.get("/data/") -async def read_query( +def read_query( + request: Request, id: str, - starttime: Any = Query(UTCDateTime(datetime.now())), - endtime: str = Query(None), + starttime: Any = Query(DEFAULT_STARTTIME), + endtime: Any = Query(None), elements: List[str] = Query(DEFAULT_ELEMENTS), - sampling_period: float = DEFAULT_SAMPLING_PERIOD, - data_type: str = DEFAULT_DATA_TYPE, - output_format: str = DEFAULT_OUTPUT_FORMAT, + sampling_period: float = Query(DEFAULT_SAMPLING_PERIOD), + data_type: str = Query(DEFAULT_DATA_TYPE), + format: str = Query(DEFAULT_OUTPUT_FORMAT), ): - starttime = UTCDateTime(starttime) if len(elements) == 1 and "," in elements[0]: elements = [e.strip() for e in elements[0].split(",")] @@ -118,23 +119,128 @@ async def read_query( "elements": elements, "sampling_period": sampling_period, "data_type": data_type, - "output_format": output_format, + "output_format": format, } try: parsed_query = parse_query(observatory) validate_query(parsed_query) except Exception as e: - return format_error(400, e) + return format_error(400, e, format, request) + + try: + timeseries = get_timeseries(parsed_query) + return format_timeseries(timeseries, parsed_query) + except Exception as e: + return format_error(500, e, format, request) + + +def format_error(status_code, exception, format, request): + if format == "json": + error = Response( + json_error(status_code, exception, request), media_type="application/json" + ) + else: + error = Response( + iaga2002_error(status_code, exception, request), media_type="text/plain" + ) - return parsed_query + return error -def format_error(status_code, exception): - return JSONResponse(json_error(status_code, exception), mimetype="application/json") +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": + return Response( + IMFJSONWriter.format(timeseries, query.elements), + media_type="application/json", + ) + else: + return Response( + IAGA2002Writer.format(timeseries, query.elements), media_type="text/plain", + ) + + +def get_data_factory(): + """Reads environment variable to determine the factory to be used + + Returns + ------- + data_factory + Edge or miniseed factory object + """ + data_type = os.getenv("DATA_TYPE", "edge") + data_host = os.getenv("DATA_HOST", "cwbpub.cr.usgs.gov") + data_port = os.getenv("DATA_PORT", 2060) + + if data_type == "edge": + data_factory = EdgeFactory(host=data_host, port=data_port) + return data_factory + else: + return None -def json_error(code: int, error: Exception): + +def get_timeseries(query): + """Get timeseries data + + Parameters + ---------- + WebServiceQuery + parsed query object + + Returns + ------- + obspy.core.Stream + timeseries object with requested data + """ + data_factory = get_data_factory() + + timeseries = data_factory.get_timeseries( + query.starttime, + query.endtime, + query.observatory_id, + query.elements, + query.data_type, + get_interval_from_delta(query.sampling_period), + ) + return timeseries + + +def iaga2002_error(status_code, error, request): + status_message = ERROR_CODE_MESSAGES[status_code] + error_body = f"""Error {status_code}: {status_message} + +{error} + +Usage details are available from + +Request: +{request.url} + +Request Submitted: +{UTCDateTime().isoformat()}Z + +Service Version: +{VERSION} +""" + return error_body + + +def json_error(code: int, error: Exception, request): """Format json error message. Returns @@ -143,22 +249,32 @@ def json_error(code: int, error: Exception): body of json error message. """ status_message = ERROR_CODE_MESSAGES[code] + url = request.url.__dict__ error_dict = { "type": "Error", "metadata": { - "status": code, - "generated": UTCDateTime().isoformat() + "Z", - "url": "url", "title": status_message, + "status": code, "error": str(error), + "generated": UTCDateTime().isoformat() + "Z", + "url": url, }, } - return dumps(error_dict, sort_keys=True).encode("utf8") + return dumps(error_dict).encode("utf8") def parse_query(query): - if not query["endtime"]: + try: + query["starttime"] = UTCDateTime(query["starttime"]) + + except Exception as e: + raise WebServiceException( + f"Bad starttime value '{query['starttime']}'." + " Valid values are ISO-8601 timestamps." + ) from e + + if query["endtime"] == None: endtime = query["starttime"] + (24 * 60 * 60 - 1) query["endtime"] = endtime @@ -172,6 +288,7 @@ def parse_query(query): f"Bad endtime value '{query['endtime']}'." " Valid values are ISO-8601 timestamps." ) from e + params = WebServiceQuery(**query) return params @@ -210,13 +327,3 @@ def validate_query(query): ) if samples > REQUEST_LIMIT: raise WebServiceException(f"Query exceeds request limit ({samples} > 345600)") - - -@app.exception_handler(RequestValidationError) -async def validation_exception_handler(request, exc): - return PlainTextResponse(str(exc), status_code=400) - - -@app.exception_handler(StarletteHTTPException) -async def http_exception_handler(request, exc): - return PlainTextResponse(str(exc.detail), status_code=exc.status_code)