Source code for bsrn.archive.records_base
"""
Shared Pydantic base classes for BSRN logical records.
"""
from __future__ import annotations
import numpy as np
import pandas as pd
from pydantic import BaseModel, ConfigDict
from .formatting import ArchiveFormatMixin
from .specs import LR_SPECS
def _validation_callable(val_module, spec_validate_name: str):
"""
Resolve ``LR_SPECS["validate_func"]`` to a function in ``validation``.
Names follow the BSRN archive convention (e.g. ``F12.4_validateFunction``); Python
identifiers use underscores (``F12_4_validateFunction``). Try the spec string first,
then the underscored form.
"""
fn = getattr(val_module, spec_validate_name, None)
if fn is not None:
return fn
py_name = spec_validate_name.replace(".", "_")
if py_name != spec_validate_name:
return getattr(val_module, py_name)
raise AttributeError(
f"bsrn.archive.validation has no {spec_validate_name!r} (or {py_name!r})"
)
[docs]
def make_archive_after_validator(lr_code: str, field_name: str):
"""
Build a unary callable for :class:`pydantic.functional_validators.AfterValidator`.
Reads ``LR_SPECS[lr_code][field_name]`` (``validate_func`` + ``format``). Minute
LR0100 / LR4000 columns must use :func:`field_validator` with ``yearMonth`` instead.
"""
meta = LR_SPECS[lr_code][field_name]
vfn = meta["validate_func"]
if vfn in ("LR0100_validateFunction", "LR4000_validateFunction"):
raise ValueError(
f"{lr_code}.{field_name}: use field_validator with yearMonth for minute columns"
)
def validate(value):
if value is None:
return value
import bsrn.archive.validation as val_module
fn = _validation_callable(val_module, vfn)
try:
clean = fn(value)
except Exception as e:
raise ValueError(f"{field_name}\n {str(e)}") from e
if isinstance(value, (np.ndarray, pd.Series, list, tuple)):
return clean if clean is not value else value
return ArchiveFormatMixin._coerce_stored_scalar(field_name, clean, meta)
return validate
[docs]
class ArchiveRecordBase(ArchiveFormatMixin, BaseModel):
"""
Base class for archive LRs: Pydantic ``BaseModel`` plus ``ArchiveFormatMixin`` for
Fortran-width ASCII output. Subclasses attach BSRN checks via
:class:`pydantic.functional_validators.AfterValidator` and, for LR0100 / LR4000
minute vectors, :func:`pydantic.field_validator` (with ``ValidationInfo`` for
``yearMonth``).
"""
model_config = ConfigDict(extra="ignore", frozen=False)
@property
def _private(self):
"""
Field values as a dict for ``get_bsrn_format`` implementations.
"""
return {name: getattr(self, name) for name in type(self).model_fields}