Source code for pyiem.models.shef

"""SHEF Data Model."""
# pylint: disable=too-few-public-methods

# stdlib
from datetime import datetime, timedelta
from typing import Optional

from metpy.units import units

# third party
from pydantic import BaseModel, ConfigDict, Field

# Local
from pyiem.reference import (
    shef_english_units,
    shef_send_codes,
    shef_standard_units,
    shef_table7,
)
from pyiem.util import LOG

# Manually defined and used within shef_{english,standard}_units.txt
units.define("KCFS = 1000 * feet ^ 3 / second")
units.define("MCM = 1000000 * meter ^ 3")
units.define("DEG10 = 10 * degree")  # UH, UR


[docs] class SHEFElement(BaseModel): """A PEDTSEP Element.""" model_config = ConfigDict(validate_assignment=True) station: str = Field(..., max_length=8) basevalid: datetime = Field(...) # Prevent multiple DH24 from trouble valid: datetime = Field(...) dv_interval: Optional[timedelta] = Field(default=None) # DV physical_element: Optional[str] = Field(default=None) # PE duration: Optional[str] = Field(default=None) type: str = Field(default="R") # Table 7 source: str = Field(default="Z") # Table 7 extremum: str = Field(default="Z") # Table 7 probability: str = Field(default="Z") # Table 7 str_value: str = Field(default="") num_value: Optional[float] = Field(default=None) data_created: Optional[datetime] = Field(default=None) depth: Optional[int] = Field( default=None, ge=0, le=32767 ) # database as smallint unit_convention: str = Field(default="E") # DU qualifier: Optional[str] = Field(default=None) # DQ comment: Optional[str] = Field( default=None ) # This is found after the value narrative: Optional[str] = Field( default=None ) # Free text after some Wxcoder/IVROCS raw: Optional[str] = Field(default=None) # The SHEF message
[docs] def to_english(self) -> float: """Return an English value representation. Implementation Note: In the case of wind direction (UH, UR), this returns the un-scaled value. """ if ( self.physical_element in ["UH", "UR"] and self.num_value is not None ): return self.num_value * 10 # NOOP if self.unit_convention == "E" or self.num_value is None: return self.num_value # We have work to do. ename = shef_english_units.get(self.physical_element) sname = shef_standard_units.get(self.physical_element) if ename is None or sname is None: LOG.warning("Unknown unit conv %s", self.physical_element) return self.num_value return (units(sname) * self.num_value).to(units(ename)).m
[docs] def varname(self) -> Optional[str]: """Return the Full SHEF Code.""" if self.physical_element is None or self.duration is None: return None return ( f"{self.physical_element}{self.duration}{self.type}{self.source}" f"{self.extremum}{self.probability}" )
[docs] def consume_code(self, text: str) -> None: """Fill out element based on provided text.""" # Ensure we have no cruft taging along text = text.strip().split()[0] if text.startswith("D"): # Reserved per 3.3.1 raise ValueError(f"Cowardly refusing to set D {text}") if len(text) < 2: # Invalid, but after 9 months, I gave up, just consume it. text = f"{text}_" # Reset the rest of codes to defaults in case new code is shorter self.duration = None self.type = "R" self.source = "Z" self.extremum = "Z" self.probability = "Z" # Table 2: Override for some special codes text = shef_send_codes.get(text, text) length = len(text) # Always present self.physical_element = text[:2] if length >= 3: self.duration = text[2] else: # SHEF Manual Table 7 provides duration defaults self.duration = shef_table7.get(self.physical_element, "I") if length >= 4: self.type = text[3] if length >= 5: self.source = text[4] if length >= 6: self.extremum = text[5] if length >= 7: self.probability = text[6] # 4.4.3 has to be a V, or else if self.dv_interval and self.duration != "V": self.dv_interval = None
[docs] def lonlat(self) -> tuple[Optional[float], Optional[float]]: """For 'Stranger Locations', return longitude and latitude.""" # 4.1.2 Must be 8 char char0 = self.station[0] if ( len(self.station) != 8 or char0 not in ["W", "X", "Y", "Z"] or any(x.isalpha() for x in self.station[1:]) ): return None, None lat = float(self.station[1:4]) / 10.0 lon = float(self.station[4:]) / 10.0 if char0 in ["W", "X"]: lon *= -1 if char0 in ["W", "Z"]: lat *= -1 return lon, lat