# Copyright (c) 2026, tephpy Contributors.
#
# This file is part of tephpy and is distributed under the 3-Clause BSD license.
# See the LICENSE file in the package root directory for licensing details.
"""Sounding data shipped with tephpy (gallery spec §3.1).
Four samples from two stations, each read by name through a public reader.
.. code-block:: python
from tephpy import samples
snd = samples.sounding("norman-12z")
**Norman, Oklahoma, 2013-05-20** — the morning of the Moore EF5 tornado: the
12Z ascent of the canonical example (spec §4), and the 17Z special released
about three hours before the tornado touched down. Both are in one IGRA v2
station file.
**Camborne, 2026-07-21 12Z** — one ascent, shipped twice: as the IGRA record
and as the University of Wyoming ``TEXT:CSV`` body. The pair exists so the
reader how-to can show both supported routes into a
:class:`~tephpy.sounding.Sounding` converging on the same profile
(narrative spec §3.6), which needs the same physical ascent on both sides.
**Provenance.** All four captured by
``tests/fixtures/generate_io_fixtures.py``.
- The Norman file, 2026-08-20, from `NCEI's IGRA v2 period-of-record file for
station USM00072357 <https://www.ncei.noaa.gov/data/
integrated-global-radiosonde-archive/access/data-por/
USM00072357-data.txt.zip>`__, keeping the two ascents of that date as whole
byte-faithful blocks — a header record and its declared level count.
- The Camborne IGRA file, 2026-07-27, the same way from that station's
year-to-date file.
- The Camborne Wyoming body, 2026-07-27, from the `University of Wyoming
sounding archive <https://weather.uwyo.edu/upperair/sounding.shtml>`__,
thinned to every 40th data row plus the first and last; kept rows are
byte-faithful and the header row is complete.
**Attribution.** The IGRA files are from the NOAA/NCEI Integrated Global
Radiosonde Archive version 2, a U.S. Government work in the public domain;
cite it as Durre, I., X. Yin, R. S. Vose, S. Applequist, and J. Arnfield
(2016), doi:10.7289/V5X63K0Q. The Wyoming body is sounding data courtesy of
the University of Wyoming, College of Engineering, Department of Atmospheric
Science. That archive publishes no redistribution terms, so it travels here
as a considered risk rather than a granted permission — narrative spec §3.6
states the position and :issue:`202` carries the question.
Notes
-----
.. versionadded:: 0.1.0
"""
from __future__ import annotations
from pathlib import Path
from typing import TYPE_CHECKING, NamedTuple
from tephpy.io import igra, wyoming
if TYPE_CHECKING:
from tephpy.sounding import Sounding
__all__ = ["available", "path", "sounding"]
class _Sample(NamedTuple):
"""One shipped ascent: the file holding it, and how to read it."""
file: str
reader: str
time: str
station: str | None = None
# Every shipped sample, in the order the ascents were measured. ``reader``
# names a public function of ``tephpy.io``, because a sample a user cannot
# reproduce by the documented route proves nothing about that route. The
# Wyoming body carries ``station`` too: an IGRA record identifies its station
# and a ``TEXT:CSV`` body does not, so the identifier is supplied here to get
# the same metadata on both halves of the Camborne pair.
_SAMPLES = {
"norman-12z": _Sample("USM00072357-data-trimmed.txt", "igra", "2013-05-20 12:00"),
"norman-17z": _Sample("USM00072357-data-trimmed.txt", "igra", "2013-05-20 17:00"),
"camborne-igra-12z": _Sample(
"UKM00003808-data-trimmed.txt", "igra", "2026-07-21 12:00"
),
"camborne-wyoming-12z": _Sample(
"wyoming-03808-2026-07-21-12Z.csv", "wyoming", "2026-07-21 12:00", "03808"
),
}
[docs]
def available() -> tuple[str, ...]:
"""Report the sample names :func:`sounding` accepts.
Returns
-------
tuple of str
The names, in the order the ascents were measured.
Notes
-----
.. versionadded:: 0.1.0
"""
return tuple(_SAMPLES)
def _select(name: str) -> _Sample:
"""Look one sample up, or say what the caller could have asked for.
Parameters
----------
name : str
A candidate sample name.
Returns
-------
_Sample
The entry for `name`.
Raises
------
ValueError
If `name` is not a shipped sample.
"""
sample = _SAMPLES.get(name)
if sample is None:
msg = f"unknown sample {name!r}; available: {', '.join(available())}"
raise ValueError(msg)
return sample
[docs]
def path(name: str) -> Path:
"""Return the file a shipped sample is read from.
Two formats ship, so there is no one file to return without being asked
— this took no argument while the package carried a single IGRA station
file. It is a file beside this module rather than an
:mod:`importlib.resources` traversable: the caller this exists for is a
reader opening it with :func:`igra.read <tephpy.io.igra.read>` or
:func:`wyoming.parse <tephpy.io.wyoming.parse>`, and a zip-imported
install would hand them a path that vanishes on the next line.
Note that one file may hold several samples: both Norman ascents are in
one IGRA station record, so two names give the same path and differ only
in the ascent they select from it.
Parameters
----------
name : str
One of the names :func:`available` reports.
Returns
-------
pathlib.Path
The file holding that sample.
Raises
------
ValueError
If `name` is not a shipped sample.
Notes
-----
.. versionadded:: 0.1.0
"""
return Path(__file__).parent / _select(name).file
[docs]
def sounding(name: str) -> Sounding:
"""Read a shipped sounding by name.
Every sample goes through a public reader — :func:`igra.read
<tephpy.io.igra.read>` or :func:`wyoming.parse
<tephpy.io.wyoming.parse>` — which is the same documented route a user's
own file takes. A sample reached by a private path would demonstrate
nothing a reader could repeat.
Parameters
----------
name : str
One of the names :func:`available` reports.
Returns
-------
Sounding
The ascent.
Raises
------
ValueError
If `name` is not a shipped sample.
Notes
-----
.. versionadded:: 0.1.0
"""
sample = _select(name)
if sample.reader == "wyoming":
body = path(name).read_text(encoding="utf-8")
return wyoming.parse(body, station=sample.station, time=sample.time)
return igra.read(path(name), time=sample.time)