Skip to content
Snippets Groups Projects
  • Erin (Josh) Rigler's avatar
    7cee7440
    Partially obscure parameters in swagger and api · 7cee7440
    Erin (Josh) Rigler authored
    We can't totally obscure things and fairly call this an "open" api.
    However, if we prefer to limit experimentation by novice developers,
    it seems prudent to at least partially obscure some options in the
    user-friendly Swagger for our api. This seemed especially important
    for the data_host option, which could be susceptible to abuse. So,
    all models/schemas are published via `ws/openapi.json`, but only
    "default" options are displayed on the Swagger page's "Try it out"
    section.
    7cee7440
    History
    Partially obscure parameters in swagger and api
    Erin (Josh) Rigler authored
    We can't totally obscure things and fairly call this an "open" api.
    However, if we prefer to limit experimentation by novice developers,
    it seems prudent to at least partially obscure some options in the
    user-friendly Swagger for our api. This seemed especially important
    for the data_host option, which could be susceptible to abuse. So,
    all models/schemas are published via `ws/openapi.json`, but only
    "default" options are displayed on the Swagger page's "Try it out"
    section.
DataApiQuery.py 4.77 KiB
import datetime
import enum
import os
from typing import Dict, List, Union

from obspy import UTCDateTime
from pydantic import BaseModel, root_validator, validator

from ... import pydantic_utcdatetime
from .Element import ELEMENTS
from .Observatory import OBSERVATORY_INDEX


DEFAULT_ELEMENTS = ["X", "Y", "Z", "F"]
REQUEST_LIMIT = 345600
VALID_ELEMENTS = [e.id for e in ELEMENTS]


class DataType(str, enum.Enum):
    VARIATION = "variation"
    ADJUSTED = "adjusted"
    QUASI_DEFINITIVE = "quasi-definitive"
    DEFINITIVE = "definitive"

    @classmethod
    def values(cls) -> List[str]:
        return [t.value for t in cls]


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 DataHost(str, enum.Enum):
    # recognized public Edge data hosts, plus one user-specified
    DEFAULT = os.getenv("DATA_HOST", "edgecwb.usgs.gov")
    EDGECWB = "edgecwb.usgs.gov"
    CWBPUB = "cwbpub.cr.usgs.gov"
    CWBPUB2 = "cwbp2.cr.usgs.gov"

    @classmethod
    def values(cls) -> List[str]:
        return [t.value for t in cls]


class DataApiQuery(BaseModel):
    id: str
    starttime: UTCDateTime = None
    endtime: UTCDateTime = None
    elements: List[str] = DEFAULT_ELEMENTS
    sampling_period: SamplingPeriod = SamplingPeriod.MINUTE
    data_type: Union[DataType, str] = DataType.VARIATION
    format: Union[OutputFormat, str] = OutputFormat.IAGA2002
    data_host: Union[DataHost, str] = DataHost.DEFAULT

    @validator("data_type")
    def validate_data_type(
        cls, data_type: Union[DataType, str]
    ) -> Union[DataType, str]:
        if data_type not in DataType.values() and len(data_type) != 2:
            raise ValueError(
                f"Bad data type value '{data_type}'."
                f" Valid values are: {', '.join(DataType.values())}"
            )
        return data_type

    @validator("data_host")
    def validate_data_host(
        cls, data_host: Union[DataHost, str]
    ) -> Union[DataHost, str]:
        if data_host not in DataHost.values():
            raise ValueError(
                # don't advertise acceptable hosts
                f"Bad data_host value '{data_host}'."
            )
        return data_host

    @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 OBSERVATORY_INDEX:
            raise ValueError(
                f"Bad observatory id '{id}'."
                f" Valid values are: {', '.join(sorted(OBSERVATORY_INDEX.keys()))}."
            )
        return id

    @validator("starttime", always=True)
    def validate_starttime(cls, starttime: UTCDateTime) -> UTCDateTime:
        if not starttime:
            # default to start of current day
            now = datetime.datetime.now(tz=datetime.timezone.utc)
            return UTCDateTime(year=now.year, month=now.month, day=now.day)
        return starttime

    @validator("endtime", always=True)
    def validate_endtime(
        cls, endtime: UTCDateTime, *, values: Dict, **kwargs
    ) -> UTCDateTime:
        """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 + (86400 - 0.001)
        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) / sampling_period)
        if samples > REQUEST_LIMIT:
            raise ValueError(f"Request exceeds limit ({samples} > {REQUEST_LIMIT})")
        # otherwise okay
        return values