From 22334f2698bd5480a48860265740a17ac05a2198 Mon Sep 17 00:00:00 2001 From: Jeremy Fee <jmfee@usgs.gov> Date: Fri, 3 Apr 2020 13:34:09 -0600 Subject: [PATCH] Refactor api packaging, clean up --- geomagio/api/__init__.py | 3 + geomagio/api/app.py | 19 ++- geomagio/api/data/DataApiQuery.py | 152 ------------------ geomagio/api/data/data_api.py | 251 ------------------------------ geomagio/api/data/elements.py | 44 ------ geomagio/api/ws/DataApiQuery.py | 166 ++++++++++++++++++++ geomagio/api/ws/Element.py | 41 +++++ geomagio/api/ws/__init__.py | 5 + geomagio/api/ws/app.py | 105 +++++++++++++ geomagio/api/ws/data.py | 109 +++++++++++++ geomagio/api/ws/elements.py | 24 +++ 11 files changed, 467 insertions(+), 452 deletions(-) create mode 100644 geomagio/api/__init__.py delete mode 100644 geomagio/api/data/DataApiQuery.py delete mode 100644 geomagio/api/data/data_api.py delete mode 100644 geomagio/api/data/elements.py create mode 100644 geomagio/api/ws/DataApiQuery.py create mode 100644 geomagio/api/ws/Element.py create mode 100644 geomagio/api/ws/__init__.py create mode 100644 geomagio/api/ws/app.py create mode 100644 geomagio/api/ws/data.py create mode 100644 geomagio/api/ws/elements.py diff --git a/geomagio/api/__init__.py b/geomagio/api/__init__.py new file mode 100644 index 000000000..34f275ed3 --- /dev/null +++ b/geomagio/api/__init__.py @@ -0,0 +1,3 @@ +from .app import app + +__all__ = ["app"] diff --git a/geomagio/api/app.py b/geomagio/api/app.py index f5d6ec339..8e4725cdc 100644 --- a/geomagio/api/app.py +++ b/geomagio/api/app.py @@ -1,12 +1,21 @@ -from fastapi import FastAPI +"""Geomag Web Services -from .data.data_api import app as ws_app +This is an Application Server Gateway Interface (ASGI) application +and can be run using uvicorn, or any other ASGI server: + uvicorn geomagio.api:app -app = FastAPI() +""" +from fastapi import FastAPI +from starlette.responses import RedirectResponse +from . import ws -subapi = ws_app + +app = FastAPI() +app.mount("/ws", ws.app) -app.mount("/ws", subapi) +@app.get("/", include_in_schema=False) +async def redirect_to_ws(): + return RedirectResponse("/ws") diff --git a/geomagio/api/data/DataApiQuery.py b/geomagio/api/data/DataApiQuery.py deleted file mode 100644 index a253490fa..000000000 --- a/geomagio/api/data/DataApiQuery.py +++ /dev/null @@ -1,152 +0,0 @@ -from datetime import datetime -import enum -from typing import Any, List, Union - -from pydantic import BaseModel, root_validator, validator - - -DEFAULT_ELEMENTS = ["X", "Y", "Z", "F"] -REQUEST_LIMIT = 345600 -VALID_DATA_TYPES = ["variation", "adjusted", "quasi-definitive", "definitive"] -VALID_ELEMENTS = [ - "D", - "DIST", - "DST", - "E", - "E-E", - "E-N", - "F", - "G", - "H", - "SQ", - "SV", - "UK1", - "UK2", - "UK3", - "UK4", - "X", - "Y", - "Z", -] -VALID_OBSERVATORIES = [ - "BDT", - "BOU", - "BRT", - "BRW", - "BSL", - "CMO", - "CMT", - "DED", - "DHT", - "FDT", - "FRD", - "FRN", - "GUA", - "HON", - "NEW", - "SHU", - "SIT", - "SJG", - "SJT", - "TST", - "TUC", - "USGS", -] - - -class DataType(str, enum.Enum): - VARIATION = "variation" - ADJUSTED = "adjusted" - QUASI_DEFINITIVE = "quasi-definitive" - DEFINITIVE = "definitive" - - -class OutputFormat(str, enum.Enum): - IAGA2002 = "iaga2002" - JSON = "json" - - -class SamplingPeriod(float, enum.Enum): - TEN_HERTZ = 0.1 - SECOND = 1.0 - MINUTE = 60 - HOUR = 3600 - DAY = 86400 - - -class DataApiQuery(BaseModel): - id: str - starttime: datetime - endtime: datetime - elements: List[str] - sampling_period: SamplingPeriod - data_type: Union[DataType, str] - format: OutputFormat - - @validator("data_type", pre=True, always=True) - def set_and_validate_data_type(cls, data_type): - if not data_type: - return DataType.VARIATION - - if len(data_type) != 2 and data_type not in DataType: - raise ValueError( - f"Bad data type value '{data_type}'. Valid values are: {', '.join(VALID_DATA_TYPES)}" - ) - return data_type - - @validator("elements", pre=True, always=True) - def set_and_validate_elements(cls, elements): - if not elements: - return DEFAULT_ELEMENTS - - if len(elements) == 1 and "," in elements[0]: - elements = [e.strip() for e in elements[0].split(",")] - - for element in elements: - if element not in VALID_ELEMENTS and len(element) != 3: - raise ValueError( - f"Bad element '{element}'." - f"Valid values are: {', '.join(VALID_ELEMENTS)}." - ) - return elements - - @validator("sampling_period", pre=True, always=True) - def set_sampling_period(cls, sampling_period): - return sampling_period or SamplingPeriod.HOUR - - @validator("format", pre=True, always=True) - def set_format(cls, format): - return format or OutputFormat.IAGA2002 - - @validator("id") - def validate_id(cls, id): - if id not in VALID_OBSERVATORIES: - raise ValueError( - f"Bad observatory id '{id}'." - f" Valid values are: {', '.join(VALID_OBSERVATORIES)}." - ) - return id - - @root_validator - def validate_times(cls, values): - starttime, endtime, elements, format, sampling_period = ( - values.get("starttime"), - values.get("endtime"), - values.get("elements"), - values.get("format"), - values.get("sampling_period"), - ) - if starttime > endtime: - raise ValueError("Starttime must be before endtime.") - - if len(elements) > 4 and format == "iaga2002": - raise ValueError("No more than four elements allowed for iaga2002 format.") - - samples = int( - len(elements) * (endtime - starttime).total_seconds() / sampling_period - ) - # check data volume - if samples > REQUEST_LIMIT: - raise ValueError(f"Query exceeds request limit ({samples} > 345600)") - - return values diff --git a/geomagio/api/data/data_api.py b/geomagio/api/data/data_api.py deleted file mode 100644 index 6c102a35d..000000000 --- a/geomagio/api/data/data_api.py +++ /dev/null @@ -1,251 +0,0 @@ -from datetime import datetime -import enum -import os -from typing import Any, Dict, List, Union - -from fastapi import Depends, FastAPI, Query, Request -from fastapi.exceptions import RequestValidationError -from fastapi.exception_handlers import request_validation_exception_handler -from fastapi.responses import JSONResponse -from obspy import UTCDateTime, Stream -from starlette.responses import Response - -from ...edge import EdgeFactory -from ...iaga2002 import IAGA2002Writer -from ...imfjson import IMFJSONWriter -from ...TimeseriesUtility import get_interval_from_delta -from .DataApiQuery import DataApiQuery, DataType, OutputFormat, SamplingPeriod -from .elements import ELEMENTS, ELEMENT_INDEX - -ERROR_CODE_MESSAGES = { - 204: "No Data", - 400: "Bad Request", - 404: "Not Found", - 409: "Conflict", - 500: "Internal Server Error", - 501: "Not Implemented", - 503: "Service Unavailable", -} - -VERSION = "version" - - -def format_error( - status_code: int, exception: str, format: str, request: Request -) -> Response: - """Assign error_body value based on error format.""" - if format == "json": - - error = JSONResponse(json_error(status_code, exception, request.url)) - - else: - error = Response( - iaga2002_error(status_code, exception, request.url), media_type="text/plain" - ) - - return error - - -def format_timeseries(timeseries, query: DataApiQuery) -> Stream: - """Formats timeseries into JSON or IAGA data - - Parameters - ---------- - obspy.core.Stream - timeseries object with requested data - - DataApiQuery - parsed query object - - Returns - ------- - unicode - IAGA2002 or JSON formatted string. - """ - if query.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 get_timeseries(query: DataApiQuery): - """Get timeseries data - - Parameters - ---------- - DataApiQuery - parsed query object - - Returns - ------- - obspy.core.Stream - timeseries object with requested data - """ - data_factory = get_data_factory() - - timeseries = data_factory.get_timeseries( - starttime=UTCDateTime(query.starttime), - endtime=UTCDateTime(query.endtime), - observatory=query.id, - channels=query.elements, - type=query.data_type, - interval=get_interval_from_delta(query.sampling_period), - ) - return timeseries - - -def iaga2002_error(code: int, error: Exception, url: str) -> str: - """Format iaga2002 error message. - - Returns - ------- - error_body : str - body of iaga2002 error message. - """ - status_message = ERROR_CODE_MESSAGES[code] - error_body = f"""Error {code}: {status_message} - -{error} - -Usage details are available from - -Request: -{url} - -Request Submitted: -{UTCDateTime().isoformat()}Z - -Service Version: -{VERSION} -""" - return error_body - - -def json_error(code: int, error: Exception, url: str) -> Dict: - """Format json error message. - - Returns - ------- - error_body : str - body of json error message. - """ - status_message = ERROR_CODE_MESSAGES[code] - error_dict = { - "type": "Error", - "metadata": { - "title": status_message, - "status": code, - "error": str(error), - "generated": UTCDateTime().isoformat() + "Z", - "url": str(url), - }, - } - return error_dict - - -def parse_query( - id: str, - starttime: datetime = Query(None), - endtime: datetime = Query(None), - elements: List[str] = Query(None), - sampling_period: SamplingPeriod = Query(SamplingPeriod.HOUR), - data_type: Union[DataType, str] = Query(DataType.VARIATION), - format: OutputFormat = Query(OutputFormat.IAGA2002), -) -> DataApiQuery: - - if starttime == None: - now = datetime.now() - starttime = UTCDateTime(year=now.year, month=now.month, day=now.day) - - else: - try: - starttime = UTCDateTime(starttime) - - except Exception as e: - raise ValueError( - f"Bad starttime value '{starttime}'." - " Valid values are ISO-8601 timestamps." - ) - - if endtime == None: - endtime = starttime + (24 * 60 * 60 - 1) - - else: - try: - endtime = UTCDateTime(endtime) - except Exception as e: - raise ValueError( - f"Bad endtime value '{endtime}'." - " Valid values are ISO-8601 timestamps." - ) from e - params = DataApiQuery( - id=id, - starttime=starttime, - endtime=endtime, - elements=elements, - sampling_period=sampling_period, - data_type=data_type, - format=format, - ) - - return params - - -app = FastAPI(docs_url="/docs", openapi_prefix="/ws") - - -@app.exception_handler(ValueError) -async def validation_exception_handler(request: Request, exc: RequestValidationError): - if "format" in request.query_params: - data_format = str(request.query_params["format"]) - else: - data_format = "iaga2002" - return format_error(400, str(exc), data_format, request) - - -@app.get("/data/") -def get_data(request: Request, query: DataApiQuery = Depends(parse_query)): - try: - timeseries = get_timeseries(query) - return format_timeseries(timeseries, query) - except Exception as e: - return format_error(500, e, query.format, request) - - -@app.get("/elements/") -def get_elements(): - elements = {"type": "FeatureCollection", "features": []} - for e in ELEMENTS: - elements["features"].append( - { - "type": "Feature", - "id": e.id, - "properties": {"name": e.name, "units": e.units}, - "geometry": None, - } - ) - return JSONResponse(elements) diff --git a/geomagio/api/data/elements.py b/geomagio/api/data/elements.py deleted file mode 100644 index 1ed9c18e3..000000000 --- a/geomagio/api/data/elements.py +++ /dev/null @@ -1,44 +0,0 @@ -from pydantic import BaseModel - - -class Element(BaseModel): - id: str - abbreviation: str - name: str - units: str - - -ELEMENTS = [ - Element(id="H", abbreviation="H", name="Observatory North Component", units="nT"), - Element(id="E", abbreviation="E", name="Observatory East Component", units="nT"), - Element(id="X", abbreviation="X", name="Geographic North Magnitude", units="nT"), - Element(id="Y", abbreviation="Y", name="Geographic East Magnitude", units="nT"), - Element(id="D", abbreviation="D", name="Declination (deci-arcminute)", units="dam"), - Element( - id="Z", abbreviation="Z", name="Observatory Vertical Component", units="nT" - ), - Element(id="F", abbreviation="F", name="Total Field Magnitude", units="nT"), - Element( - id="G", abbreviation="ΔF", name="Observatory Vertical Component", units="∆nT" - ), - Element(id="E-E", abbreviation="E-E", name="E=Field East", units="mV/km"), - Element(id="E-N", abbreviation="E-N", name="E-Field North", units="mV/km"), - Element(id="MDT", abbreviation="DIST", name="Disturbance", units="nT"), - Element(id="MSQ", abbreviation="SQ", name="Solar Quiet", units="nT"), - Element(id="MSV", abbreviation="SV", name="Solar Variation", units="nT"), - Element( - id="UK1", abbreviation="T-Electric", name="Electronics Temperature", units="°C" - ), - Element( - id="UK2", - abbreviation="T-Total Field", - name="Total Field Temperature", - units="°C", - ), - Element( - id="UK3", abbreviation="T-Fluxgate", name="Fluxgate Temperature", units="°C" - ), - Element(id="UK4", abbreviation="T-Outside", name="Outside Temperature", units="°C"), -] - -ELEMENT_INDEX = {e.id: e for e in ELEMENTS} diff --git a/geomagio/api/ws/DataApiQuery.py b/geomagio/api/ws/DataApiQuery.py new file mode 100644 index 000000000..7d89d7381 --- /dev/null +++ b/geomagio/api/ws/DataApiQuery.py @@ -0,0 +1,166 @@ +import datetime +import enum +from typing import Any, Dict, List, Optional, Union + +from obspy import UTCDateTime +from pydantic import BaseModel, root_validator, validator + +from .Element import ELEMENTS, ELEMENT_INDEX + +DEFAULT_ELEMENTS = ["X", "Y", "Z", "F"] +REQUEST_LIMIT = 345600 +VALID_ELEMENTS = [e.id for e in ELEMENTS] + +VALID_OBSERVATORIES = [ + "BDT", + "BOU", + "BRT", + "BRW", + "BSL", + "CMO", + "CMT", + "DED", + "DHT", + "FDT", + "FRD", + "FRN", + "GUA", + "HON", + "NEW", + "SHU", + "SIT", + "SJG", + "SJT", + "TST", + "TUC", + "USGS", +] + + +class DataType(str, enum.Enum): + VARIATION = "variation" + ADJUSTED = "adjusted" + QUASI_DEFINITIVE = "quasi-definitive" + DEFINITIVE = "definitive" + + +class OutputFormat(str, enum.Enum): + IAGA2002 = "iaga2002" + JSON = "json" + + +class SamplingPeriod(float, enum.Enum): + TEN_HERTZ = 0.1 + SECOND = 1.0 + MINUTE = 60.0 + HOUR = 3600.0 + DAY = 86400.0 + + +class DataApiQuery(BaseModel): + id: str + starttime: datetime.datetime = None + endtime: datetime.datetime = None + elements: List[str] = DEFAULT_ELEMENTS + sampling_period: SamplingPeriod = SamplingPeriod.MINUTE + data_type: Union[DataType, str] = DataType.VARIATION + format: OutputFormat = OutputFormat.IAGA2002 + + @validator("data_type") + def validate_data_type( + cls, data_type: Union[DataType, str] + ) -> Union[DataType, str]: + if data_type not in DataType and len(data_type) != 2: + raise ValueError( + f"Bad data type value '{data_type}'." + f" Valid values are: {', '.join(list(DataType))}" + ) + return data_type + + @validator("elements", pre=True, always=True) + def validate_elements(cls, elements: List[str]) -> List[str]: + if not elements: + return DEFAULT_ELEMENTS + if len(elements) == 1 and "," in elements[0]: + elements = [e.strip() for e in elements[0].split(",")] + for element in elements: + if element not in VALID_ELEMENTS and len(element) != 3: + raise ValueError( + f"Bad element '{element}'." + f" Valid values are: {', '.join(VALID_ELEMENTS)}." + ) + return elements + + @validator("id") + def validate_id(cls, id: str) -> str: + if id not in VALID_OBSERVATORIES: + raise ValueError( + f"Bad observatory id '{id}'." + f" Valid values are: {', '.join(VALID_OBSERVATORIES)}." + ) + return id + + @validator("starttime", pre=True, always=True) + def validate_starttime( + cls, starttime: Union[datetime.datetime, datetime.date] + ) -> datetime.datetime: + if not starttime: + # default to start of current day + now = datetime.datetime.now(tz=datetime.timezone.utc) + return datetime.datetime( + year=now.year, + month=now.month, + day=now.day, + tzinfo=datetime.timezone.utc, + ) + elif isinstance(starttime, datetime.date): + return datetime.datetime( + year=starttime.year, + month=starttime.month, + day=starttime.day, + tzinfo=datetime.timezone.utc, + ) + return starttime + + @validator("endtime", always=True, pre=True) + def validate_endtime( + cls, endtime: Union[datetime.datetime, datetime.date], *, values: Dict, **kwargs + ) -> datetime.datetime: + """Default endtime is based on starttime. + + This method needs to be after validate_starttime. + """ + if not endtime: + # endtime defaults to 1 day after startime + starttime = values.get("starttime") + endtime = starttime + datetime.timedelta(seconds=86400 - 0.001) + elif isinstance(endtime, datetime.date): + return datetime.datetime( + year=endtime.year, + month=endtime.month, + day=endtime.day, + tzinfo=datetime.timezone.utc, + ) + return endtime + + @root_validator + def validate_combinations(cls, values): + starttime, endtime, elements, format, sampling_period = ( + values.get("starttime"), + values.get("endtime"), + values.get("elements"), + values.get("format"), + values.get("sampling_period"), + ) + if len(elements) > 4 and format == "iaga2002": + raise ValueError("No more than four elements allowed for iaga2002 format.") + if starttime > endtime: + raise ValueError("Starttime must be before endtime.") + # check data volume + samples = int( + len(elements) * (endtime - starttime).total_seconds() / sampling_period + ) + if samples > REQUEST_LIMIT: + raise ValueError(f"Request exceeds limit ({samples} > {REQUEST_LIMIT})") + # otherwise okay + return values diff --git a/geomagio/api/ws/Element.py b/geomagio/api/ws/Element.py new file mode 100644 index 000000000..eff80cca9 --- /dev/null +++ b/geomagio/api/ws/Element.py @@ -0,0 +1,41 @@ +from typing import Optional +from pydantic import BaseModel + + +class Element(BaseModel): + id: str + abbreviation: Optional[str] + name: str + units: str + + +ELEMENTS = [ + Element(id="H", name="North Component", units="nT"), + Element(id="E", name="East Component", units="nT"), + Element(id="X", name="Geographic North Magnitude", units="nT"), + Element(id="Y", name="Geographic East Magnitude", units="nT"), + Element(id="D", name="Declination (deci-arcminute)", units="dam"), + Element(id="Z", name="Vertical Component", units="nT"), + Element(id="F", name="Total Field Magnitude", units="nT"), + Element(id="G", abbreviation="ΔF", name="Delta F", units="∆nT"), + Element(id="DIST", name="Disturbance", units="nT"), + Element(id="E-E", name="E=Field East", units="mV/km"), + Element(id="E-N", name="E-Field North", units="mV/km"), + Element(id="SQ", name="Solar Quiet", units="nT"), + Element(id="SV", name="Solar Variation", units="nT"), + Element( + id="UK1", abbreviation="T-Electric", name="Electronics Temperature", units="°C", + ), + Element( + id="UK2", + abbreviation="T-Total Field", + name="Total Field Temperature", + units="°C", + ), + Element( + id="UK3", abbreviation="T-Fluxgate", name="Fluxgate Temperature", units="°C" + ), + Element(id="UK4", abbreviation="T-Outside", name="Outside Temperature", units="°C"), +] + +ELEMENT_INDEX = {e.id: e for e in ELEMENTS} diff --git a/geomagio/api/ws/__init__.py b/geomagio/api/ws/__init__.py new file mode 100644 index 000000000..125ea3af7 --- /dev/null +++ b/geomagio/api/ws/__init__.py @@ -0,0 +1,5 @@ +"""Module with application for "/ws" endpoints. +""" +from .app import app + +__all__ = ["app"] diff --git a/geomagio/api/ws/app.py b/geomagio/api/ws/app.py new file mode 100644 index 000000000..04fcebcf4 --- /dev/null +++ b/geomagio/api/ws/app.py @@ -0,0 +1,105 @@ +import datetime +from typing import Dict, Union + +from fastapi import FastAPI, Request, Response +from fastapi.exceptions import RequestValidationError +from starlette.responses import RedirectResponse + +from . import data, elements + + +ERROR_CODE_MESSAGES = { + 204: "No Data", + 400: "Bad Request", + 404: "Not Found", + 409: "Conflict", + 500: "Internal Server Error", + 501: "Not Implemented", + 503: "Service Unavailable", +} + +VERSION = "version" + + +app = FastAPI(docs_url="/docs", openapi_prefix="/ws") +app.include_router(data.router) +app.include_router(elements.router) + + +@app.get("/", include_in_schema=False) +async def redirect_to_docs(): + return RedirectResponse("/ws/docs") + + +@app.exception_handler(ValueError) +async def validation_exception_handler(request: Request, exc: RequestValidationError): + """Value errors are user errors. + """ + if "format" in request.query_params: + data_format = str(request.query_params["format"]) + return format_error(400, str(exc), data_format, request) + + +@app.exception_handler(Exception) +async def validation_exception_handler(request: Request, exc: RequestValidationError): + """Other exceptions are server errors. + """ + if "format" in request.query_params: + data_format = str(request.query_params["format"]) + return format_error(500, str(exc), data_format, request) + + +def format_error( + status_code: int, exception: str, format: str, request: Request +) -> Response: + """Assign error_body value based on error format.""" + if format == "json": + return json_error(status_code, exception, request.url) + else: + return Response( + text_error(status_code, exception, request.url), media_type="text/plain" + ) + + +def json_error(code: int, error: Exception, url: str) -> Dict: + """Format json error message. + + Returns + ------- + error_body : str + body of json error message. + """ + return { + "type": "Error", + "metadata": { + "title": ERROR_CODE_MESSAGES[code], + "status": code, + "error": str(error), + "generated": datetime.datetime.utcnow(), + "url": str(url), + }, + } + + +def text_error(code: int, error: Exception, url: str) -> str: + """Format error message as plain text + + Returns + ------- + error message formatted as plain text. + """ + return f"""Error {code}: {ERROR_CODE_MESSAGES[code]} + +{error} + +Usage details are available from + +Request: +{url} + +Request Submitted: +{datetime.datetime.utcnow().isoformat()} + +Service Version: +{VERSION} +""" diff --git a/geomagio/api/ws/data.py b/geomagio/api/ws/data.py new file mode 100644 index 000000000..128126eac --- /dev/null +++ b/geomagio/api/ws/data.py @@ -0,0 +1,109 @@ +import datetime +import os +from typing import Any, Dict, List, Union + +from fastapi import APIRouter, Depends, Query +from obspy import UTCDateTime, Stream +from starlette.responses import Response + +from ... import TimeseriesFactory, TimeseriesUtility +from ...edge import EdgeFactory +from ...iaga2002 import IAGA2002Writer +from ...imfjson import IMFJSONWriter +from .DataApiQuery import ( + DEFAULT_ELEMENTS, + DataApiQuery, + DataType, + OutputFormat, + SamplingPeriod, +) + + +def get_data_factory() -> TimeseriesFactory: + """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": + return EdgeFactory(host=data_host, port=data_port) + else: + return None + + +def format_timeseries( + timeseries: Stream, format: OutputFormat, elements: List[str] +) -> Response: + """Formats timeseries output + + Parameters + ---------- + timeseries: data to format + format: output format + obspy.core.Stream + timeseries object with requested data + """ + if format == OutputFormat.JSON: + data = IMFJSONWriter.format(timeseries, elements) + media_type = "application/json" + else: + data = IAGA2002Writer.format(timeseries, elements) + media_type = "text/plain" + return Response(data, media_type=media_type) + + +def get_timeseries(data_factory: TimeseriesFactory, query: DataApiQuery) -> Stream: + """Get timeseries data + + Parameters + ---------- + data_factory: where to read data + query: parameters for the data to read + """ + # get data + timeseries = data_factory.get_timeseries( + starttime=UTCDateTime(query.starttime), + endtime=UTCDateTime(query.endtime), + observatory=query.id, + channels=query.elements, + type=query.data_type, + interval=TimeseriesUtility.get_interval_from_delta(query.sampling_period), + ) + return timeseries + + +router = APIRouter() + + +@router.get("/data/") +def get_data( + id: str, + starttime: Union[datetime.datetime, datetime.date] = Query(None), + endtime: Union[datetime.datetime, datetime.date] = Query(None), + elements: List[str] = Query(DEFAULT_ELEMENTS), + sampling_period: Union[SamplingPeriod, float] = Query(SamplingPeriod.MINUTE), + data_type: Union[DataType, str] = Query(DataType.ADJUSTED), + format: OutputFormat = Query(OutputFormat.IAGA2002), + data_factory: TimeseriesFactory = Depends(get_data_factory), +) -> Response: + # parse query + query = DataApiQuery( + id=id, + starttime=starttime, + endtime=endtime, + elements=elements, + sampling_period=sampling_period, + data_type=data_type, + format=format, + ) + # read data + timeseries = get_timeseries(data_factory, query) + # output response + return format_timeseries( + timeseries=timeseries, format=format, elements=query.elements + ) diff --git a/geomagio/api/ws/elements.py b/geomagio/api/ws/elements.py new file mode 100644 index 000000000..11a6c95d7 --- /dev/null +++ b/geomagio/api/ws/elements.py @@ -0,0 +1,24 @@ +from typing import Dict + +from fastapi import APIRouter + +from .Element import ELEMENTS + + +router = APIRouter() + + +@router.get("/elements/") +def get_elements() -> Dict: + return { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "id": e.id, + "properties": {"name": e.name, "units": e.units}, + "geometry": None, + } + for e in ELEMENTS + ], + } -- GitLab