Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ ci:
autoupdate_schedule: quarterly
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: "v0.15.0"
rev: "v0.15.1"
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ All notable changes to this library are documented in this file.
- Generate Jabber messages for Daily Summary Messages.
- Increased LSR jabber message generation limit to 20 (#1154).
- Support Storm Prediction Center updated `CIG[1-3]` thresholds (#1156).
- Support Terminal Aerodome Forecast (TAF) designation of amendments with
addition to TAFReport data model and persistence to database
(akrherz/iem#1514).
- Update CI testing to include python=3.14

### Bug Fixes
Expand Down
29 changes: 15 additions & 14 deletions src/pyiem/models/taf.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
"""TAF Data Model."""
# pylint: disable=too-few-public-methods

from datetime import datetime
from typing import List, Optional
from typing import Annotated, List, Optional

# third party
from pydantic import BaseModel, Field
Expand All @@ -11,29 +10,29 @@
class WindShear(BaseModel):
"""A Wind Shear Value."""

level: int = Field(..., ge=0, le=100000)
drct: int = Field(..., ge=0, le=360)
sknt: int = Field(..., ge=0, le=199)
level: Annotated[int, Field(ge=0, le=100000)]
drct: Annotated[int, Field(ge=0, le=360)]
sknt: Annotated[int, Field(ge=0, le=199)]


class SkyCondition(BaseModel):
"""The Sky condition."""

amount: str
level: Optional[int] = Field(None, ge=0, le=100000)
level: Annotated[int | None, Field(ge=0, le=100000)] = None


class TAFForecast(BaseModel):
"""A TAF forecast."""

valid: datetime
raw: str
ftype: int = Field(..., ge=0, le=5)
ftype: Annotated[int, Field(ge=0, le=5)]
end_valid: Optional[datetime] = None
sknt: Optional[int] = Field(default=None, ge=0, le=199)
drct: Optional[int] = Field(default=None, ge=0, le=360)
gust: Optional[int] = Field(default=None, ge=0, le=199)
visibility: Optional[float] = Field(default=None, ge=0, le=6)
sknt: Annotated[int | None, Field(ge=0, le=199)] = None
drct: Annotated[int | None, Field(ge=0, le=360)] = None
gust: Annotated[int | None, Field(ge=0, le=199)] = None
visibility: Annotated[float | None, Field(ge=0, le=6)] = None
presentwx: List[str] = Field(default_factory=list)
sky: List[SkyCondition] = Field(default_factory=list)
shear: Optional[WindShear] = None
Expand All @@ -42,8 +41,10 @@ class TAFForecast(BaseModel):
class TAFReport(BaseModel):
"""A TAF Report consisting of forecasts."""

station: str = Field(..., min_length=4, max_length=4)
station: Annotated[str, Field(min_length=4, max_length=4)]
valid: datetime
product_id: str = Field(..., min_length=28, max_length=35)
product_id: Annotated[str, Field(min_length=28, max_length=35)]
observation: TAFForecast
forecasts: List[TAFForecast] = Field(default_factory=list)
is_amendment: Annotated[bool, Field(description="Is this amended?")]
# Type checkers do not handle Annotated for this case
forecasts: list[TAFForecast] = Field(default_factory=list)
17 changes: 10 additions & 7 deletions src/pyiem/nws/products/taf.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
"""TAF Parsing"""

# stdlib
import re
from datetime import datetime, timedelta

from pyiem import reference
from pyiem.models.taf import SkyCondition, TAFForecast, TAFReport, WindShear

# local
from pyiem.nws.product import TextProduct

TEMPO_TIME = re.compile(r"^(?P<ddhh1>\d{4})/(?P<ddhh2>\d{4}) ")
Expand Down Expand Up @@ -35,7 +32,7 @@
}


def add_forecast_info(fx, text):
def add_forecast_info(fx: TAFForecast, text: str):
"""Common things."""
m = WIND_RE.search(text)
if m:
Expand Down Expand Up @@ -175,6 +172,7 @@ def parse_prod(prod: TextProduct, segtext: str) -> TAFReport:
station=d["station"] if len(d["station"]) == 4 else f"K{d['station']}",
valid=valid,
product_id=prod.get_product_id(),
is_amendment="TAF AMD" in prod.unixtext, # Believe OK for collectives
observation=TAFForecast(
valid=valid,
raw=" ".join(lines[0].split()).strip(),
Expand Down Expand Up @@ -248,9 +246,14 @@ def sql(self, txn):

# Create an entry
txn.execute(
"INSERT into taf(station, valid, product_id) "
"VALUES (%s, %s, %s) RETURNING id",
(taf.station, taf.valid, self.get_product_id()),
"INSERT into taf(station, valid, product_id, is_amendment) "
"VALUES (%s, %s, %s, %s) RETURNING id",
(
taf.station,
taf.valid,
self.get_product_id(),
taf.is_amendment,
),
)
taf_id = txn.fetchone()["id"]
# Insert obs / forecast
Expand Down
2 changes: 2 additions & 0 deletions tests/nws/products/test_taf.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ def test_tafpam():
utcnow = utc(2025, 8, 7, 0)
prod = real_tafparser(get_test_file("TAF/TAFPAM.txt"), utcnow=utcnow)
taf = prod.data[0]
assert not taf.is_amendment
assert taf.observation.ftype == 0
assert taf.forecasts[0].ftype == 2
assert taf.forecasts[1].ftype == 5
Expand All @@ -105,6 +106,7 @@ def test_gh1104_tafhky():
utcnow = utc(2025, 8, 15, 0)
prod = real_tafparser(get_test_file("TAF/TAFHKY.txt"), utcnow=utcnow)
assert prod.data[0].observation.ftype == 0
assert prod.data[0].is_amendment
answers = [2, 1, 1, 3, 1]
for idx in range(5):
assert prod.data[0].forecasts[idx].ftype == answers[idx]
Expand Down