From 300e5c6db00f8c05245c68614381b4be3fc26564 Mon Sep 17 00:00:00 2001 From: Leif Denby Date: Tue, 27 Oct 2026 11:48:21 +0100 Subject: [PATCH 1/4] Move cli, runner, verification and their helpers into pipeline/ The YAML-driven pipeline (cli, runner, verification, transformations and the config/dates/save helpers it alone uses) now lives in mxalign.pipeline. The console script points at mxalign.pipeline.cli:main. Public re-exports from mxalign/__init__.py are unchanged. Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- pyproject.toml | 2 +- src/mxalign/__init__.py | 11 +++++++---- src/mxalign/pipeline/__init__.py | 5 +++++ src/mxalign/{ => pipeline}/cli.py | 0 src/mxalign/{utils => pipeline}/config.py | 0 src/mxalign/{utils => pipeline}/dates.py | 0 src/mxalign/{ => pipeline}/runner.py | 8 ++++---- src/mxalign/{utils => pipeline}/save.py | 0 .../{ => pipeline}/transformations/__init__.py | 0 src/mxalign/{ => pipeline}/transformations/base.py | 0 .../{ => pipeline}/transformations/external.py | 0 .../{ => pipeline}/transformations/registry.py | 0 .../{ => pipeline}/transformations/transform.py | 0 src/mxalign/{ => pipeline}/verification.py | 0 tests/test_dates.py | 6 +++--- tests/test_runner.py | 2 +- tests/test_transformations.py | 4 ++-- 18 files changed, 24 insertions(+), 16 deletions(-) create mode 100644 src/mxalign/pipeline/__init__.py rename src/mxalign/{ => pipeline}/cli.py (100%) rename src/mxalign/{utils => pipeline}/config.py (100%) rename src/mxalign/{utils => pipeline}/dates.py (100%) rename src/mxalign/{ => pipeline}/runner.py (97%) rename src/mxalign/{utils => pipeline}/save.py (100%) rename src/mxalign/{ => pipeline}/transformations/__init__.py (100%) rename src/mxalign/{ => pipeline}/transformations/base.py (100%) rename src/mxalign/{ => pipeline}/transformations/external.py (100%) rename src/mxalign/{ => pipeline}/transformations/registry.py (100%) rename src/mxalign/{ => pipeline}/transformations/transform.py (100%) rename src/mxalign/{ => pipeline}/verification.py (100%) diff --git a/README.md b/README.md index 6f4da9b..b5c57ff 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ mxalign slurm path/to/config.yaml --account your_account --queue your_queue --co You can also execute the entire configuration-driven pipeline directly from Python using the `Runner` class. ```python -from mxalign.runner import Runner +from mxalign.pipeline.runner import Runner # Initialize the runner with a YAML config file or a dictionary runner = Runner("path/to/config.yaml") diff --git a/pyproject.toml b/pyproject.toml index 81cd43a..f1a9cee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,7 @@ dependencies = [ ] [project.scripts] -mxalign = "mxalign.cli:main" +mxalign = "mxalign.pipeline.cli:main" [project.optional-dependencies] earthkit = [ diff --git a/src/mxalign/__init__.py b/src/mxalign/__init__.py index d104858..c4549b8 100644 --- a/src/mxalign/__init__.py +++ b/src/mxalign/__init__.py @@ -1,5 +1,8 @@ -from .transformations.transform import transform -from .transformations.registry import available_transformations, register_transformation +from .pipeline.transformations.transform import transform +from .pipeline.transformations.registry import ( + available_transformations, + register_transformation, +) from .alignment.interpolations.interpolate import interpolate from .alignment.interpolations.registry import ( available_interpolations, @@ -10,7 +13,7 @@ from . import accessor from . import alignment -from . import transformations +from . import pipeline __all__ = [ "transform", @@ -23,5 +26,5 @@ "align_space", "accessor", "alignment", - "transformations", + "pipeline", ] diff --git a/src/mxalign/pipeline/__init__.py b/src/mxalign/pipeline/__init__.py new file mode 100644 index 0000000..50dece2 --- /dev/null +++ b/src/mxalign/pipeline/__init__.py @@ -0,0 +1,5 @@ +from . import transformations + +__all__ = [ + "transformations", +] diff --git a/src/mxalign/cli.py b/src/mxalign/pipeline/cli.py similarity index 100% rename from src/mxalign/cli.py rename to src/mxalign/pipeline/cli.py diff --git a/src/mxalign/utils/config.py b/src/mxalign/pipeline/config.py similarity index 100% rename from src/mxalign/utils/config.py rename to src/mxalign/pipeline/config.py diff --git a/src/mxalign/utils/dates.py b/src/mxalign/pipeline/dates.py similarity index 100% rename from src/mxalign/utils/dates.py rename to src/mxalign/pipeline/dates.py diff --git a/src/mxalign/runner.py b/src/mxalign/pipeline/runner.py similarity index 97% rename from src/mxalign/runner.py rename to src/mxalign/pipeline/runner.py index 3b21117..57014b2 100644 --- a/src/mxalign/runner.py +++ b/src/mxalign/pipeline/runner.py @@ -3,11 +3,11 @@ from mlwp_data_loaders import load_and_validate_dataset -from .utils.config import Config +from .config import Config from .transformations.transform import transform -from .alignment.time import align_time -from .alignment.nans import broadcast_nans -from .utils.save import save_dataset, save_metrics +from ..alignment.time import align_time +from ..alignment.nans import broadcast_nans +from .save import save_dataset, save_metrics from .verification import Metric diff --git a/src/mxalign/utils/save.py b/src/mxalign/pipeline/save.py similarity index 100% rename from src/mxalign/utils/save.py rename to src/mxalign/pipeline/save.py diff --git a/src/mxalign/transformations/__init__.py b/src/mxalign/pipeline/transformations/__init__.py similarity index 100% rename from src/mxalign/transformations/__init__.py rename to src/mxalign/pipeline/transformations/__init__.py diff --git a/src/mxalign/transformations/base.py b/src/mxalign/pipeline/transformations/base.py similarity index 100% rename from src/mxalign/transformations/base.py rename to src/mxalign/pipeline/transformations/base.py diff --git a/src/mxalign/transformations/external.py b/src/mxalign/pipeline/transformations/external.py similarity index 100% rename from src/mxalign/transformations/external.py rename to src/mxalign/pipeline/transformations/external.py diff --git a/src/mxalign/transformations/registry.py b/src/mxalign/pipeline/transformations/registry.py similarity index 100% rename from src/mxalign/transformations/registry.py rename to src/mxalign/pipeline/transformations/registry.py diff --git a/src/mxalign/transformations/transform.py b/src/mxalign/pipeline/transformations/transform.py similarity index 100% rename from src/mxalign/transformations/transform.py rename to src/mxalign/pipeline/transformations/transform.py diff --git a/src/mxalign/verification.py b/src/mxalign/pipeline/verification.py similarity index 100% rename from src/mxalign/verification.py rename to src/mxalign/pipeline/verification.py diff --git a/tests/test_dates.py b/tests/test_dates.py index c8e6380..c37a6bb 100644 --- a/tests/test_dates.py +++ b/tests/test_dates.py @@ -2,9 +2,9 @@ import pytest -from mxalign.utils.config import Config -from mxalign.utils.dates import Dates -from mxalign.utils.save import DatasetPath +from mxalign.pipeline.config import Config +from mxalign.pipeline.dates import Dates +from mxalign.pipeline.save import DatasetPath # 3 reference times (daily) × 3 lead times (0, 12, 24h) DATES = dict(start="2020-01-01", end="2020-01-03", period="1D", range="24h", step="12h") diff --git a/tests/test_runner.py b/tests/test_runner.py index a4c52be..b3aa11d 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -1,6 +1,6 @@ import pytest -from mxalign.runner import Runner +from mxalign.pipeline.runner import Runner def _runner(datasets: dict, alignment: dict) -> Runner: diff --git a/tests/test_transformations.py b/tests/test_transformations.py index fd737c3..ec85306 100644 --- a/tests/test_transformations.py +++ b/tests/test_transformations.py @@ -1,11 +1,11 @@ import pytest import xarray as xr -from mxalign.transformations.registry import ( +from mxalign.pipeline.transformations.registry import ( available_transformations, register_transformation, ) -from mxalign.transformations.transform import transform +from mxalign.pipeline.transformations.transform import transform @register_transformation("_test_double") From 6d5aca535ca40847697d404d85bba217e3cb65dd Mon Sep 17 00:00:00 2001 From: Leif Denby Date: Tue, 27 Oct 2026 12:25:09 +0100 Subject: [PATCH 2/4] Update Runner import in advanced example notebook Co-Authored-By: Claude Opus 5.5 --- examples/advanced.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/advanced.ipynb b/examples/advanced.ipynb index 2c15b56..c577565 100644 --- a/examples/advanced.ipynb +++ b/examples/advanced.ipynb @@ -384,7 +384,7 @@ "metadata": {}, "outputs": [], "source": [ - "from mxalign.runner import Runner\n", + "from mxalign.pipeline.runner import Runner\n", "\n", "runner = Runner(\"./config.yaml\")" ] From 80e6cd34a784eeb5b7066ca4ab03bb955bdd728b Mon Sep 17 00:00:00 2001 From: Leif Denby Date: Tue, 27 Oct 2026 12:26:26 +0100 Subject: [PATCH 3/4] Validate pipeline config against an explicit pydantic schema Replace the untyped Config dict wrapper with pydantic models (PipelineConfig and one model per section) loaded via load_config(). Unknown or missing keys now fail when the Runner is created instead of deep inside a run, and the Runner reads typed attributes rather than popping keys (which also stops align() mutating the config). Keys that are forwarded to loaders, transformations, metrics and save methods are kept as pass-through kwargs. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + README.md | 2 +- examples/advanced.ipynb | 2 +- pyproject.toml | 1 + src/mxalign/pipeline/config.py | 178 +++++++++++++++++++++++---------- src/mxalign/pipeline/runner.py | 123 ++++++++++------------- tests/test_config.py | 75 ++++++++++++++ tests/test_dates.py | 7 +- tests/test_runner.py | 13 ++- uv.lock | 148 ++++++++++++++++++++++++++- 10 files changed, 418 insertions(+), 132 deletions(-) create mode 100644 tests/test_config.py diff --git a/CHANGELOG.md b/CHANGELOG.md index cbc0c92..3e3cdbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Changed +- **Breaking:** the pipeline config is validated against an explicit pydantic schema (`mxalign.pipeline.config.PipelineConfig`, loaded with `load_config`) instead of being read as a free-form dict. Unknown sections or keys, and missing required keys, raise a `ValidationError` when the `Runner` is created; `Runner.config` is now the validated model. Per-dataset `dates` entries override individual fields of the top-level `dates`. @leifdenby - **Breaking:** file path patterns (`datasets..files` with a `dates:` section, and `alignment.save.path`) now use Python `str.format` syntax instead of earthkit-data patterns: `{reference_time:%Y%m%d%H}` instead of `{reference_time:strftime(%Y%m%d%H)}`, `{year:02d}` instead of `{year:int(%02d)}`. `{lead_time}` (integer hours) and `{valid_time}` are now supported in `files` patterns. `earthkit-data` is no longer a dependency. @michielv - **Breaking:** temporal alignment always runs in the pipeline; `alignment.time` is optional and only accepts `lead_time` (`reference` | `intersection` | `union`, forecast reference only). Unknown keys such as `return_as` or `method` now raise an error. `align_time` takes `reference` and an explicit `lead_time` instead of `**kwargs`. @michielv - Remove dataset loading and dataset validation and outsource it to [mlwp-data-loaders](https://github.com/mlwp-tools/mlwp-data-loaders) and [mlwp-data-specs](https://github.com/mlwp-tools/mlwp-data-specs) @michielv diff --git a/README.md b/README.md index b5c57ff..d359155 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ For a more comprehensive interactive example, check out the [introductory notebo ### Configuration Contents -The configuration file is divided into several main sections: +The configuration file is divided into several main sections. It is validated on load against the schema in `mxalign.pipeline.config` (`PipelineConfig`): unknown sections or keys, and missing required ones such as `alignment.reference`, raise an error up front. Keys that are passed through to a function, such as loader, transformation, metric and save options, are accepted as-is. ```yaml datasets: diff --git a/examples/advanced.ipynb b/examples/advanced.ipynb index c577565..b234aa2 100644 --- a/examples/advanced.ipynb +++ b/examples/advanced.ipynb @@ -455,7 +455,7 @@ } ], "source": [ - "runner.config()" + "runner.config" ] }, { diff --git a/pyproject.toml b/pyproject.toml index f1a9cee..8a70481 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ dependencies = [ "mlwp-data-specs", "mlwp-data-loaders", "pandas>=2.2", + "pydantic>=2", ] [project.scripts] diff --git a/src/mxalign/pipeline/config.py b/src/mxalign/pipeline/config.py index a4e0682..39b4a6d 100644 --- a/src/mxalign/pipeline/config.py +++ b/src/mxalign/pipeline/config.py @@ -1,62 +1,136 @@ +"""Schema for the pipeline YAML config. + +Sections and keys not listed here are rejected, except where a model allows +extra keys (``extra="allow"``); those are passed on as keyword arguments to +the function the section configures (loader, transformation, metric, save). +""" + +from datetime import datetime +from typing import Literal + import yaml +from pydantic import BaseModel, ConfigDict, model_validator from .dates import Dates -def load_yaml(fn: str) -> dict: - """Load a YAML file and return its contents as a dict.""" - with open(fn, "r") as f: - return yaml.safe_load(f) +class _Strict(BaseModel): + model_config = ConfigDict(extra="forbid") + + +class _PassThrough(BaseModel): + """Model whose unknown keys are kept in ``model_extra`` as function kwargs.""" + model_config = ConfigDict(extra="allow") -class Config: - """Pipeline configuration loaded from a YAML file or a plain dict. + @property + def kwargs(self) -> dict: + return dict(self.model_extra or {}) - Top-level ``dates`` entries are merged into each dataset's loader config - so that file-path patterns can be expanded to concrete paths. + +class DatesConfig(_Strict): + """Date range used to expand ``{reference_time}``/``{lead_time}``/``{valid_time}`` in ``files``. + + Every field is optional so a dataset can override part of the top-level + ``dates`` section; after merging, all five must be set. """ - def __init__(self, config: str | dict) -> None: - self.config = load_yaml(config) if isinstance(config, str) else config - if not isinstance(self.config, dict): - raise TypeError("config should be a dictionary.") - self.dates = self.config.pop("dates", None) - self._init_datasets() - print("Config initialized") - - def __getitem__(self, key: str) -> dict | None: - config = self.config.get(key, None) - if config: - return config.copy() - else: - return config - - def __call__(self) -> dict: - return self.config - - def _init_datasets(self) -> None: - for key, loader in self.config["datasets"].items(): - dates_loader = loader.pop("dates", None) - if self.dates: - if dates_loader: - keys_all = list(set(self.dates.keys()).union(dates_loader.keys())) - dates = { - key: ( - dates_loader[key] - if key in dates_loader.keys() - else self.dates[key] - ) - for key in keys_all - } - else: - dates = self.dates.copy() - else: - if dates_loader: - dates = dates_loader.copy() - else: - dates = None - - if dates: - dates = Dates(**dates) - loader["files"] = dates.substitute(loader["files"]) - self.config["datasets"][key] = loader + start: datetime | str | None = None + end: datetime | str | None = None + period: str | None = None + range: str | None = None + step: str | None = None + + +class DatasetConfig(_PassThrough): + """One entry of ``datasets``; extra keys go to ``load_and_validate_dataset``.""" + + loader: str + files: str | list[str] + variables: str | list[str] | None = None + grid_mapping: str | dict | None = None + dates: DatesConfig | None = None + + +class TransformationConfig(_PassThrough): + """One entry of ``transformations`` (keyed by transformation name); extra keys go to the transformation.""" + + datasets: list[str] | None = None + + +class TimeAlignmentConfig(_Strict): + lead_time: str | list[str] | None = None + + +class SpaceAlignmentConfig(_Strict): + """Interpolator/regridder kwargs, chosen by the kind of space alignment needed.""" + + interpolation: dict | None = None + regrid: dict | None = None + + +class SaveConfig(_PassThrough): + """Output for ``ds.(path, **kwargs)``; extra keys go to the save method.""" + + method: str + path: str + + +class AlignmentSaveConfig(SaveConfig): + datasets: Literal["all", "merge"] = "all" + + +class AlignmentConfig(_Strict): + reference: str + broadcast_nans: bool = True + time: TimeAlignmentConfig | None = None + space: SpaceAlignmentConfig | None = None + save: AlignmentSaveConfig | None = None + + +class MetricConfig(_PassThrough): + """One entry of ``verification.metrics``; extra keys go to the metric function.""" + + function: str + inputs: dict[str, Literal["forecast", "reference"]] + + +class VerificationConfig(_Strict): + reference: str + metrics: dict[str, MetricConfig] | None = None + save: SaveConfig | None = None + + +class PipelineConfig(_Strict): + dates: DatesConfig | None = None + datasets: dict[str, DatasetConfig] + transformations: dict[str, TransformationConfig] | None = None + alignment: AlignmentConfig + verification: VerificationConfig | None = None + + @model_validator(mode="after") + def _expand_files(self) -> "PipelineConfig": + """Expand each dataset's ``files`` pattern(s) over its (merged) ``dates``.""" + top = self.dates.model_dump(exclude_none=True) if self.dates else {} + for name, ds in self.datasets.items(): + own = ds.dates.model_dump(exclude_none=True) if ds.dates else {} + dates = {**top, **own} + if not dates: + continue + missing = set(DatesConfig.model_fields) - dates.keys() + if missing: + raise ValueError( + f"datasets.{name}: dates is missing {', '.join(sorted(missing))}" + ) + patterns = [ds.files] if isinstance(ds.files, str) else ds.files + dates = Dates(**dates) + ds.files = [path for p in patterns for path in dates.substitute(p)] + return self + + +def load_config(config: str | dict) -> PipelineConfig: + """Validate a pipeline config given as a YAML file path or a dict.""" + if isinstance(config, str): + with open(config, "r") as f: + config = yaml.safe_load(f) + return PipelineConfig.model_validate(config) diff --git a/src/mxalign/pipeline/runner.py b/src/mxalign/pipeline/runner.py index 57014b2..080201d 100644 --- a/src/mxalign/pipeline/runner.py +++ b/src/mxalign/pipeline/runner.py @@ -3,7 +3,12 @@ from mlwp_data_loaders import load_and_validate_dataset -from .config import Config +from .config import ( + PipelineConfig, + SpaceAlignmentConfig, + TimeAlignmentConfig, + load_config, +) from .transformations.transform import transform from ..alignment.time import align_time from ..alignment.nans import broadcast_nans @@ -13,7 +18,7 @@ class Runner: def __init__(self, config: str | dict): - self.config = Config(config) + self.config: PipelineConfig = load_config(config) self.datasets = {} def run(self): @@ -26,16 +31,11 @@ def run(self): self.verify() def load_datasets(self): - config = self.config["datasets"] - if config is None: - raise ValueError("No datasets section in the config.") - for name, config_ds in config.items(): - config_ds = config_ds.copy() - loader = config_ds.pop("loader") - variables = config_ds.pop("variables", None) - grid_mapping = config_ds.pop("grid_mapping", None) - - raw_files = config_ds.pop("files") + for name, config_ds in self.config.datasets.items(): + variables = config_ds.variables + grid_mapping = config_ds.grid_mapping + + raw_files = config_ds.files if isinstance(raw_files, str): raw_files = [raw_files] files = [] @@ -48,8 +48,8 @@ def load_datasets(self): ds = load_and_validate_dataset( dataset_path, - loader=loader, - **config_ds, + loader=config_ds.loader, + **config_ds.kwargs, ) if variables is not None: @@ -61,85 +61,74 @@ def load_datasets(self): self.datasets[name] = ds def transform_datasets(self): - config = self.config["transformations"] - if config is None: + if self.config.transformations is None: return - for transformation, config_trans in config.items(): - config_trans = config_trans.copy() + for transformation, config_trans in self.config.transformations.items(): # if no datasets specified, apply to all datasets - names_ds = config_trans.pop("datasets", self.datasets.keys()) + names_ds = config_trans.datasets + if names_ds is None: + names_ds = self.datasets.keys() for name in names_ds: ds = self.datasets[name] self.datasets[name] = transform( - name=transformation, datasets=ds, **config_trans + name=transformation, datasets=ds, **config_trans.kwargs ) def align(self): - config = self.config["alignment"] - reference = config.pop("reference") - brdcst_nans = config.pop("broadcast_nans", True) - config_align_time = config.get("time", None) - config_align_space = config.get("space", None) - config_align_save = config.get("save", None) + config = self.config.alignment + reference = config.reference # align in time (always; the time section only holds options) - self.align_time(config_align_time, reference_name=reference) + self.align_time(config.time, reference_name=reference) # align in space - if config_align_space: - self.align_space(reference=reference, config=config_align_space) + if config.space is not None: + self.align_space(reference=reference, config=config.space) else: print("Skipping spatial alignment") # broadcast NaNs - if brdcst_nans: + if config.broadcast_nans: self.datasets = broadcast_nans(self.datasets) # Save aligned datasets - if config_align_save: - config = config_align_save.copy() - method = config.pop("method") - datasets = config.pop("datasets", "all") - if datasets == "all": + if config.save is not None: + save = config.save + kwargs = {"path": save.path, **save.kwargs} + if save.datasets == "all": for name, ds in self.datasets.items(): - save_dataset(method, name, ds, **config) - elif datasets == "merge": + save_dataset(save.method, name, ds, **kwargs) + elif save.datasets == "merge": ds = xr.concat( self.datasets.values(), dim=xr.Variable("model", list(self.datasets.keys())), ) - save_dataset(method, "merged", ds, **config) - else: - raise ValueError("Unknown option for dataset saving.") + save_dataset(save.method, "merged", ds, **kwargs) def verify(self): - config = self.config["verification"] - reference = self.datasets[config["reference"]] - config_metrics = config.get("metrics", None) - config_save_metrics = config.get("save", None) + config = self.config.verification + if config is None: + return + reference = self.datasets[config.reference] common_vars = set(reference.data_vars) for ds in self.datasets.values(): common_vars.intersection_update(set(ds.data_vars)) common_vars = list(common_vars) - if config_metrics: + if config.metrics: metrics = {} - for metric_name, config_metric in config["metrics"].items(): - config_metric = config_metric.copy() - func_path = config_metric.pop("function") - inputs = config_metric.pop("inputs") - + for metric_name, config_metric in config.metrics.items(): metric = Metric( name=metric_name, - func_path=func_path, + func_path=config_metric.function, ds_ref=reference[common_vars], - inputs=inputs, - **config_metric, + inputs=config_metric.inputs, + **config_metric.kwargs, ) models = {} for ds_name, ds in self.datasets.items(): - if ds_name != config["reference"]: + if ds_name != config.reference: models[ds_name] = metric.compute(ds[common_vars]) models = xr.concat( models.values(), dim=xr.Variable("model", list(models.keys())) @@ -150,28 +139,24 @@ def verify(self): ) self.metrics = metrics.transpose("model", "metric", ...).compute() - if config_save_metrics: - config = config_save_metrics.copy() - method = config.pop("method") - save_metrics(method, self.metrics, **config) - - def align_time(self, config, reference_name=None): - config = dict(config or {}) - lead_time = config.pop("lead_time", None) - if config: - raise ValueError( - f"Unknown option(s) in alignment.time: {', '.join(config)}. " - "Only lead_time is supported." - ) + if config.save is not None: + save = config.save + save_metrics(save.method, self.metrics, path=save.path, **save.kwargs) + + def align_time( + self, config: TimeAlignmentConfig | None, reference_name: str | None = None + ): + lead_time = config.lead_time if config is not None else None self.datasets = align_time( self.datasets, reference=reference_name, lead_time=lead_time ) - def align_space(self, reference, config): + def align_space(self, reference: str, config: SpaceAlignmentConfig): ds_ref = self.datasets[reference] for name, ds in self.datasets.items(): if name != reference: - options = config.get(get_spatial_alignment(ds, ds_ref), {}) + kind = get_spatial_alignment(ds, ds_ref) + options = getattr(config, kind, None) or {} self.datasets[name] = ds.mx.align_space_with(ds_ref, **options) diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..64f2b5d --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,75 @@ +from pathlib import Path + +import pytest +from pydantic import ValidationError + +from mxalign.pipeline.config import load_config + +EXAMPLE_CONFIG = Path(__file__).parent.parent / "examples" / "config.yaml" + +DATES = dict(start="2020-01-01", end="2020-01-02", period="1D", range="12h", step="12h") + + +def _config(**sections) -> dict: + return { + "datasets": {"fc": {"loader": "x", "files": "/d/fc.nc"}}, + "alignment": {"reference": "fc"}, + **sections, + } + + +def test_example_config_validates(): + config = load_config(str(EXAMPLE_CONFIG)) + assert config.alignment.reference == "cerra" + assert config.alignment.space.interpolation == {"method": "delaunay"} + assert config.verification.metrics["rmse"].kwargs == { + "dim": ["reference_time", "grid_index"], + "skipna": True, + } + # files patterns are expanded over the dates section + assert len(config.datasets["la-1024-ea-00"].files) > 1 + + +def test_unknown_top_level_key_is_rejected(): + with pytest.raises(ValidationError, match="alignmnet"): + load_config(_config(alignmnet={"reference": "fc"})) + + +def test_missing_alignment_reference_is_rejected(): + config = _config() + config["alignment"] = {"broadcast_nans": False} + with pytest.raises(ValidationError, match="alignment.reference"): + load_config(config) + + +def test_unknown_save_datasets_option_is_rejected(): + save = {"method": "to_netcdf", "path": "out.nc", "datasets": "some"} + with pytest.raises(ValidationError, match="alignment.save.datasets"): + load_config(_config(alignment={"reference": "fc", "save": save})) + + +def test_extra_dataset_keys_are_passed_to_loader(): + config = _config() + config["datasets"]["fc"]["chunks"] = {"time": 1} + assert load_config(config).datasets["fc"].kwargs == {"chunks": {"time": 1}} + + +def test_dataset_dates_override_top_level_dates(): + config = _config(dates=DATES) + config["datasets"]["fc"] = { + "loader": "x", + "files": "/d/{reference_time:%Y%m%d}.nc", + "dates": {"end": "2020-01-03"}, + } + assert load_config(config).datasets["fc"].files == [ + "/d/20200101.nc", + "/d/20200102.nc", + "/d/20200103.nc", + ] + + +def test_incomplete_dates_are_rejected(): + config = _config() + config["datasets"]["fc"]["dates"] = {"start": "2020-01-01"} + with pytest.raises(ValidationError, match="dates is missing end, period"): + load_config(config) diff --git a/tests/test_dates.py b/tests/test_dates.py index c37a6bb..441eac5 100644 --- a/tests/test_dates.py +++ b/tests/test_dates.py @@ -2,7 +2,7 @@ import pytest -from mxalign.pipeline.config import Config +from mxalign.pipeline.config import load_config from mxalign.pipeline.dates import Dates from mxalign.pipeline.save import DatasetPath @@ -45,15 +45,16 @@ def test_substitute_lead_time_requires_whole_hours(): def test_config_expands_files_from_dates_section(): # YAML parses unquoted timestamps to datetime objects - config = Config( + config = load_config( { "dates": {**DATES, "start": datetime.datetime(2020, 1, 1)}, "datasets": { "fc": {"loader": "x", "files": "/d/{reference_time:%Y%m%d}.nc"} }, + "alignment": {"reference": "fc"}, } ) - assert config["datasets"]["fc"]["files"] == [ + assert config.datasets["fc"].files == [ "/d/20200101.nc", "/d/20200102.nc", "/d/20200103.nc", diff --git a/tests/test_runner.py b/tests/test_runner.py index b3aa11d..d84eded 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -1,4 +1,5 @@ import pytest +from pydantic import ValidationError from mxalign.pipeline.runner import Runner @@ -19,10 +20,14 @@ def test_align_runs_time_alignment_without_time_section(ds_fcst, ds_obs): assert runner.datasets["obs"].mx.is_forecast() -def test_align_time_rejects_unknown_options(ds_fcst, ds_obs): - runner = _runner({"fcst": ds_fcst, "obs": ds_obs}, {"reference": "obs"}) - with pytest.raises(ValueError, match="Unknown option.*return_as"): - runner.align_time({"return_as": "forecast"}, reference_name="obs") +def test_align_time_rejects_unknown_options(): + with pytest.raises(ValidationError, match="alignment.time.return_as"): + Runner( + { + "datasets": {}, + "alignment": {"reference": "obs", "time": {"return_as": "forecast"}}, + } + ) def test_transform_datasets_without_transformations_section(ds_obs): diff --git a/uv.lock b/uv.lock index 6ce6399..1e99d4f 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 5 +revision = 3 requires-python = ">=3.12" resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", @@ -204,6 +204,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, ] +[[package]] +name = "annotated-types" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, +] + [[package]] name = "appnope" version = "1.0.0" @@ -1808,6 +1817,7 @@ dependencies = [ { name = "mlwp-data-specs" }, { name = "netcdf4" }, { name = "pandas" }, + { name = "pydantic" }, { name = "pyyaml" }, { name = "scipy" }, { name = "xarray" }, @@ -1853,6 +1863,7 @@ requires-dist = [ { name = "mlwp-data-specs", git = "https://github.com/mlwp-tools/mlwp-data-specs?rev=059f382" }, { name = "netcdf4", specifier = ">=1.7.4" }, { name = "pandas", specifier = ">=2.2" }, + { name = "pydantic", specifier = ">=2" }, { name = "pyyaml", specifier = ">=6.0.3" }, { name = "scipy", specifier = ">=1.17.0" }, { name = "xarray", specifier = ">=2026.1.0" }, @@ -2125,7 +2136,7 @@ name = "pexpect" version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ptyprocess" }, + { name = "ptyprocess", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } wheels = [ @@ -2432,6 +2443,127 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b6/ae/315bbeee126a769cbde43cf4908acb4b40408d8bf8bbf334dd7368d8dadf/pycparser-3.12-py3-none-any.whl", hash = "sha256:a076f3f489b39b43ed13563a1dafe42447445bec2e294154ec5c57fcdda4c85e", size = 62600, upload-time = "2026-10-22T13:40:02.021Z" }, ] +[[package]] +name = "pydantic" +version = "2.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7c/0b/8e10b2e693af8ec54346a14caf36221334775975a747c9ceaa3f8371d96d/pydantic-2.14.1.tar.gz", hash = "sha256:94f478203dd03404682a1ada216965651dd74b1d2d5ffd62e00e0837caab5c26", size = 900362, upload-time = "2026-10-11T18:37:55.396Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/ea/a56b9fe5066f3537b7882f77e9c5dfb26d8c2eefdaed9b5fc73d57b4dc22/pydantic-2.14.1-py3-none-any.whl", hash = "sha256:9195d967ec791692a04438115466764fb8b9a27b31f14a760437694f40d6b454", size = 488106, upload-time = "2026-10-11T18:37:53.437Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.50.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/24/af4ec4be49fbc810f35b0bdcacc3d433b56bbfa469fd3bfd4ae116cd9bf1/pydantic_core-2.50.1.tar.gz", hash = "sha256:e50d7b94baac6c7d09927fa5ca5800a0c7ee5015c7fcff65beb3a1931b5a6e09", size = 513515, upload-time = "2026-10-11T18:35:44.82Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/94/101b49a6c63f99586755690696eae14532772587d9e39e93f7b8d28a49da/pydantic_core-2.50.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:704075d10b74f2f3c6e15407c696d88701df35fc8953f434a431add0d0074db0", size = 2222428, upload-time = "2026-10-11T18:32:20.228Z" }, + { url = "https://files.pythonhosted.org/packages/db/a3/f8b1b39349c33037c73d7a1b794c9c29d8f07d5bd2b74399c1375a529cc6/pydantic_core-2.50.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e8e1d6ce820aa23317e8209a86bd65a540973c12dc7552b48a4f6c8e9926815e", size = 2025779, upload-time = "2026-10-11T18:32:21.723Z" }, + { url = "https://files.pythonhosted.org/packages/d2/2d/862a7d115305cc621358e51ba13f296e0ffdf4fbd984be9f63bc7862730b/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c18db21573bd2c6489f9a544b7499f0df2853958c568e5e783536ee1f690af41", size = 2048429, upload-time = "2026-10-11T18:32:23.3Z" }, + { url = "https://files.pythonhosted.org/packages/c0/7d/fed95d18bf2b5abe2d5ab2bbec4184bfc22b6805994a33ccdae8edffd25a/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb57f304525a5e3c13333b772bf9a473f36326e9c821b2e8e1b2fd36f80ae2c3", size = 2173274, upload-time = "2026-10-11T18:32:24.697Z" }, + { url = "https://files.pythonhosted.org/packages/93/1e/c6f22ceac65dc3d291ff79bea0eb74cbd1de8b128f01fee48af03bcea1e1/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a27c09d86600f1bf2fe3f37e1ae697faf3143931c09322cd799da94deee923b5", size = 2360403, upload-time = "2026-10-11T18:32:26.281Z" }, + { url = "https://files.pythonhosted.org/packages/2c/97/190b5b6bfe4c72f265feb657913eb30749193927a5ad7b83cfa7866a25a2/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:46b3301d3b5c886f77de7546e47274a5842c622ea2020b8c6524c6b66913b4a6", size = 2439799, upload-time = "2026-10-11T18:32:27.844Z" }, + { url = "https://files.pythonhosted.org/packages/90/24/48cd98388e4ac2c08d0800af36d15413db15e20387f1038123a2c86cb2f6/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93ba4e9d8210d941c200431a56b2c0400b131865947903937ed3ec5404307d2e", size = 2160157, upload-time = "2026-10-11T18:32:29.553Z" }, + { url = "https://files.pythonhosted.org/packages/00/86/d909133d0b9979ed14c20f110f21124a0e346caf071bd66ac5af3009062b/pydantic_core-2.50.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:e5faeaee74a57d32b3ab3aebad2e348f06d3ba946fc5d28c1728455f00a3d13a", size = 2229523, upload-time = "2026-10-11T18:32:31.027Z" }, + { url = "https://files.pythonhosted.org/packages/90/31/4b43f08131b2098e73a852fd932063385f3103b33839fd9d1abdb89e673d/pydantic_core-2.50.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a3cda0e538208e5d722bbf3698b24f19c0a7d05bc8d5f8a7f9b121ea7fa243d9", size = 2242700, upload-time = "2026-10-11T18:32:32.622Z" }, + { url = "https://files.pythonhosted.org/packages/88/77/de39658569dc7a4c293d110a89629f61145e7538f00d1e4280c28debefd3/pydantic_core-2.50.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:57f51b31ff826e2859120cf4737c5a758a48d96f3e97da40ccee1796d58078ff", size = 2278452, upload-time = "2026-10-11T18:32:34.165Z" }, + { url = "https://files.pythonhosted.org/packages/46/bd/1eeb9e4e791e81210ef9096f58080b32ba15b5b13c108d50aaa3f8ce9f73/pydantic_core-2.50.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8daa7ee75245d43ad7d747e5c9ecc1b1d06552f72b14887e9276f787d57375f4", size = 2450098, upload-time = "2026-10-11T18:32:35.547Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e6/b2accfdff17e5a88d6358536702146de00f9339adaa4d5bbe1ee93cb4832/pydantic_core-2.50.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:acbf31f37c53a5ac0c34706c80b4f5107ba20b05fdd3816124bf236ef0c57dd2", size = 2476839, upload-time = "2026-10-11T18:32:37.119Z" }, + { url = "https://files.pythonhosted.org/packages/3b/7e/c8a83eb5d2cd42c2e13507150fb777b1edea5a018adc9cbf2858a1d08285/pydantic_core-2.50.1-cp312-cp312-win32.whl", hash = "sha256:45b11cac094aa25725581d9304eee93c9028516b9ea80dd9e175e13a5a2c840e", size = 2101425, upload-time = "2026-10-11T18:32:38.593Z" }, + { url = "https://files.pythonhosted.org/packages/c5/17/8bbd530b8659e9d963f5b16f6cb8e159f4cd174b4d6304e48969ab12dba8/pydantic_core-2.50.1-cp312-cp312-win_amd64.whl", hash = "sha256:132529c83901437ff642f585216831bf5fd7a91df66829907e155192ead62498", size = 2172522, upload-time = "2026-10-11T18:32:40.112Z" }, + { url = "https://files.pythonhosted.org/packages/af/9f/a79d690b127e4e3cff143d031538ab9aa715f6b2fbeecbd07cd6f2bf9fb7/pydantic_core-2.50.1-cp312-cp312-win_arm64.whl", hash = "sha256:4e834f6a8e4ff772dcc34f58ef5504147a3ea5b0f4eeb13b0f8eb2ca75ac57f1", size = 2161366, upload-time = "2026-10-11T18:32:41.606Z" }, + { url = "https://files.pythonhosted.org/packages/ae/57/0e237d7091d2cd44a35d243b7344227f90440166860469cd036afc23a04d/pydantic_core-2.50.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:d5e062c01286d861fd6a1c4ff6e063547b3e713067f2df033c0ff97ac2ca006b", size = 2222774, upload-time = "2026-10-11T18:32:43.103Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b9/c720e56858d4e1539503297ed37063e0c08e0f3541c41b777f6a800f75fa/pydantic_core-2.50.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0c003c3b7f49debb893d2d85ae099ac5959c9839e2f330fadb1fcdf7a6594482", size = 2024738, upload-time = "2026-10-11T18:32:44.587Z" }, + { url = "https://files.pythonhosted.org/packages/4d/b8/fbfc25875219cc060e613170ff10e850c6d8924beb4910da57c2ee3ba1d2/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:409e0ea40ec30d9158f33574fd758e689f6045a0f2596701828c27816ca9687d", size = 2047049, upload-time = "2026-10-11T18:32:46.164Z" }, + { url = "https://files.pythonhosted.org/packages/d7/43/34210124d504c553688f2f04b48500b131237528ac545b445e0d6d30e0d5/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:131059670f1d2444269b8585cb888963994871932447c08b39ac6a51fcfef658", size = 2173415, upload-time = "2026-10-11T18:32:47.722Z" }, + { url = "https://files.pythonhosted.org/packages/65/cf/6e178e8fdc11da5965bef980983bf46326a42f436871dd51ba0a57f39df1/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6dbcbee53bf17196a7f745aa9bf5a9603953a1e365b1f020be3207c676a3e7c4", size = 2361719, upload-time = "2026-10-11T18:32:49.217Z" }, + { url = "https://files.pythonhosted.org/packages/11/14/bd5169d356aa91bf777e28ba0b281c192d286d03c2812c9c9db023a2f00e/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:325c23f3e35cfbf0fe3486fa5f7260d1e45885173002d30a28ca019994124255", size = 2439803, upload-time = "2026-10-11T18:32:51.025Z" }, + { url = "https://files.pythonhosted.org/packages/1c/bc/d79d000e5203ebef39af839f2ce77a777fcad6e08ad26af9a2fcd114ffc8/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17e722e156d0444ecaefbe640bdb60928752bf2013e2b7a11cdb099aaae19bec", size = 2158402, upload-time = "2026-10-11T18:32:52.714Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a5/4902cb5fd599422c130bd3124ab31ee8b771199bd56662702eed07d3fec7/pydantic_core-2.50.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:aa8224f10880d9bf1b5993988ba153d42a8b4f3f4f511f93b1f09c93ff613c72", size = 2229755, upload-time = "2026-10-11T18:32:54.126Z" }, + { url = "https://files.pythonhosted.org/packages/1a/f5/c1481f8669f6060d89110c9b1374173fc8767ca276b84f4870bd8acd5e3f/pydantic_core-2.50.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:41bc8237121bd8dc8d888dfd6279fc166ffc88c1f1bf3a8bf00869680533ca4c", size = 2242303, upload-time = "2026-10-11T18:32:55.641Z" }, + { url = "https://files.pythonhosted.org/packages/04/f9/77fc3c7653ba9b6e42049e17e96b25388187d4a7c274ab1cb07f58ac8419/pydantic_core-2.50.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:45c6266d071c241f2a168d45bf8c54344f0effce35e7e6b73afdec11f3687568", size = 2278345, upload-time = "2026-10-11T18:32:57.38Z" }, + { url = "https://files.pythonhosted.org/packages/95/9b/0579c5d12e7f2b16b27e6782427987341fcce07b0725e02ddb0b74add0c4/pydantic_core-2.50.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:1deeacb112d14d3f4fcb16b165f7dbaf76c70ba6e82f37ba042bdab51970a0b8", size = 2449947, upload-time = "2026-10-11T18:32:58.896Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ac/1b677db91eba54cc5922f4de6edc46ba45c2bd712dfdc0130382d158e214/pydantic_core-2.50.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1c96fd793b73d1b92e65570132505498fe7b21eaef73cdf74e67e5dfba7ac9e4", size = 2476889, upload-time = "2026-10-11T18:33:00.665Z" }, + { url = "https://files.pythonhosted.org/packages/4d/2a/3a9f6624ee3ea9ccba5249dde11418bb4c35780a7a92608f8768bd3fea39/pydantic_core-2.50.1-cp313-cp313-win32.whl", hash = "sha256:06ead20d39ffd6f2f6f2a8f8a6de67ff8bb1b4f14a8a30e058502514ee2ac685", size = 2101118, upload-time = "2026-10-11T18:33:02.329Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1f/323f78ddd9d9938aac420c1abb4e8ba799fc8bdab0acc67c0593b837c979/pydantic_core-2.50.1-cp313-cp313-win_amd64.whl", hash = "sha256:7816e98acc08119dc0f340ab167048ecc54126316330c1f0caf7c6756c88e28f", size = 2170840, upload-time = "2026-10-11T18:33:03.919Z" }, + { url = "https://files.pythonhosted.org/packages/cb/09/8497c52a739ae425c3ac2f7f56414cbc711c67d374346174f40fe2062644/pydantic_core-2.50.1-cp313-cp313-win_arm64.whl", hash = "sha256:c17799a62c142d61b8a3c51752a7cbc87fe2ad4ccfab10e628a77b405075c662", size = 2161633, upload-time = "2026-10-11T18:33:05.518Z" }, + { url = "https://files.pythonhosted.org/packages/49/33/28b96e81677153715e3eafb9f26663a80841e859fde282a380359b0d3fa1/pydantic_core-2.50.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:1cf41f1ae3fa155cf167a72689ad044bcc1e3c97e064123677149bdfb5dafc4a", size = 2224428, upload-time = "2026-10-11T18:33:07.153Z" }, + { url = "https://files.pythonhosted.org/packages/94/40/15c06410c9b7b8da5805d27b64e09bd3f900e986728106db2689dbc51513/pydantic_core-2.50.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4df197990c15b5a37c5a277d131d9f2c67de6133f2e5dafd80d9bba4b99f46f9", size = 2023740, upload-time = "2026-10-11T18:33:08.763Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4e/5eb629f6efc2a27e421d789dd4bfacbb5f6d09c80c28b13d1e87d73163d5/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0036473f5583e6a60e50b8b21651511564277a3f05cc5dab8cf579f552cd5f6c", size = 2049435, upload-time = "2026-10-11T18:33:10.366Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8e/f195aebec49ad12318876ac2368c197a7939f5d52f8e156d2238ef8a4588/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:992c3514ec891fa7858099183e4d64e6bd5a5d4ff452fae29df22faa77a006bb", size = 2172975, upload-time = "2026-10-11T18:33:12.368Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f5/7ad9fb83010cd5ea0948409db105676ed779c4e709e2d3d89b9fe2558794/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:739dc730e6be3bd5ec2f4ab5cfc7eb047cc45fc1497b3bafec74ff2ed07df597", size = 2364197, upload-time = "2026-10-11T18:33:14.244Z" }, + { url = "https://files.pythonhosted.org/packages/ca/fb/bf0aab3e78301d202b82a0322ec968cd703b11fc0623fde9a28708936f62/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:32fad3a91e51b6d2039c572db04a5a873260b399f6bd62c3552671fa7a4a2899", size = 2444259, upload-time = "2026-10-11T18:33:15.79Z" }, + { url = "https://files.pythonhosted.org/packages/45/35/38f6d6564fae57d9b12e5676dfa947e0e7d5c46dbeaa7eb3352261d6d299/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42b54c2c90ad348b5e3a85e03e715d572c1fde357ef104cdfe3b03b697a404ea", size = 2159857, upload-time = "2026-10-11T18:33:17.51Z" }, + { url = "https://files.pythonhosted.org/packages/65/a0/fb0a3ca10f139dcf765b2d312d10cf0f65c57c59993229d00ad12b14ceff/pydantic_core-2.50.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:2df1ff41884de2bc4b307bafd7c40a691094fad2ff8e767e5b45a319257bcf4e", size = 2233954, upload-time = "2026-10-11T18:33:19.591Z" }, + { url = "https://files.pythonhosted.org/packages/8c/6a/e63842252702aa4ec6e6b7178ca85e541a77459076592c23ebe2d9840b33/pydantic_core-2.50.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe90228920fd8ff2be62622b6bb8a2b11acd65046d50c6b130614b5879605a20", size = 2243876, upload-time = "2026-10-11T18:33:21.393Z" }, + { url = "https://files.pythonhosted.org/packages/39/25/5991cf8318b37e0dfab47b87541619a1df8501a793cba0d978846cba37a7/pydantic_core-2.50.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:844b869f118e22a41a091bdcedda8a71bc1b0f62c38d1a0c3211cece47e1d8fc", size = 2277908, upload-time = "2026-10-11T18:33:23.324Z" }, + { url = "https://files.pythonhosted.org/packages/a7/3e/3ee8baaa6cc25a6961c69168cf9ff0f002d56f4e0d541ad6724c18fb61f3/pydantic_core-2.50.1-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:2eb75304506894a281d346220a4f7481a1b8729577c5ed2a05395991966a8396", size = 2449285, upload-time = "2026-10-11T18:33:24.942Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/acbec6deac13fe697a80c275a9b6661db62c4d323bac8a47347b1f39c7cd/pydantic_core-2.50.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6b20a4bffabdad0db2927ac034ae3b8a681b1f7a0182f3e60b479ad2fde21ebb", size = 2479013, upload-time = "2026-10-11T18:33:26.925Z" }, + { url = "https://files.pythonhosted.org/packages/10/08/21a3f237b264389f6219d053ee78fc1b5c2fd402c1b1cb2b6cb8b85f9834/pydantic_core-2.50.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:99ba9bc2b8062ea0c326a990f7f00e6530c23579de66dd246e72c4cafef950a5", size = 1484400, upload-time = "2026-10-11T18:33:28.693Z" }, + { url = "https://files.pythonhosted.org/packages/09/ce/077a6d262d12ef09108377ac0717f029f420d773ace63cafd6143af75568/pydantic_core-2.50.1-cp314-cp314-win32.whl", hash = "sha256:cf356f70551d40374eaffb1aa63f1eb6d2006681cbd7a9faea173ce0f4dd7cd2", size = 2103222, upload-time = "2026-10-11T18:33:30.326Z" }, + { url = "https://files.pythonhosted.org/packages/14/4c/350a2415209c43d670eb71d3c040f332c04a30583d39e311b05c7ac15762/pydantic_core-2.50.1-cp314-cp314-win_amd64.whl", hash = "sha256:d32f3acc081cc3923386d88f422cde8892335e95f034e0104bb4cf9310d9915f", size = 2171586, upload-time = "2026-10-11T18:33:32.139Z" }, + { url = "https://files.pythonhosted.org/packages/bf/92/9bea6ca96580a0902fed366f064f0829e404f41889b34543279a1162b888/pydantic_core-2.50.1-cp314-cp314-win_arm64.whl", hash = "sha256:bed5163e03b98bc1fa2eb05d74c63d9c5c95d8ed6254985481640fbf5e237dea", size = 2155773, upload-time = "2026-10-11T18:33:33.896Z" }, + { url = "https://files.pythonhosted.org/packages/86/8c/f121f073cf32bdd5cba7e6230b1ce0ac845b0cf43e44dd597d95af272db2/pydantic_core-2.50.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:9572c1369e9c9da2d64a7b7992c786d90ff295abc93964cfe3125e4290768070", size = 2234017, upload-time = "2026-10-11T18:33:35.588Z" }, + { url = "https://files.pythonhosted.org/packages/a4/69/2bb2bcd6146cffe98d760a46c42ae71efd5151d9b2f9c9bf6619a3b32083/pydantic_core-2.50.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2005207aafe1231315718bf6ed5d064a7300fb4772754af35ee72fc68159492e", size = 2001016, upload-time = "2026-10-11T18:33:37.57Z" }, + { url = "https://files.pythonhosted.org/packages/20/b3/fbf854c7d07ec114260c26e9e2071a4381740f9ae09641dbfcbdf2a18c45/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64f6047f62a6c5ae08d0a6afb035667aa2d97c3d20d69762e034c5ea144d92a5", size = 2023015, upload-time = "2026-10-11T18:33:39.433Z" }, + { url = "https://files.pythonhosted.org/packages/65/20/6de55b2f92cdb614b745c6e9ced639fc4fd7e1e77604825a88bdece6fcbd/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ef800dd7d85bcdadf4c3076e4c94e43939493558a3b69a1ea830c706d4617bb", size = 2192543, upload-time = "2026-10-11T18:33:41.381Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d9/19e91c94bd5c405945ce2f15526808aea37e162c253160de8ed7bf70b406/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b0135bcdcaa0f23573f286e4cb5e0fd2962700964ed13df085b85f2b97aeab9e", size = 2388704, upload-time = "2026-10-11T18:33:43.167Z" }, + { url = "https://files.pythonhosted.org/packages/b6/bc/2e24c8415eae1a25ee5a5484946a917123bad2e9d01689a759236928175a/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0b3a6f334c6a2345ca15318ff894502a90012536404b37c844a976c76c846e0b", size = 2464118, upload-time = "2026-10-11T18:33:44.856Z" }, + { url = "https://files.pythonhosted.org/packages/bd/1f/945b8053cb061c64e102bcaf7bfb9ed740c0bd4349f9bb978a7d4ddff4ab/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06e01fbbfdb9be777b316a71b6c49efaf4a08b615d0a98d678cda3023f79d019", size = 2150142, upload-time = "2026-10-11T18:33:46.661Z" }, + { url = "https://files.pythonhosted.org/packages/2b/78/96a3e50bf0d64aaae781107eb9335b7529d05a85793ed6e7241c4cd1d931/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:a29a061fec0b4e2d714f277e70a3a18125ecff803f2fea6eade2f2e53711d112", size = 2245577, upload-time = "2026-10-11T18:33:48.574Z" }, + { url = "https://files.pythonhosted.org/packages/7a/5d/a6038a0322232758a6ebfa709f4ca14a60bf5b93940cd0b345056a557da4/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f5187624823423e1d1b82b1072ac41dc837389e18d3d0572cc19bbee46cd550a", size = 2263728, upload-time = "2026-10-11T18:33:50.527Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4b/76ded3333a457a9344c4f2d63f2d82d0101654b05178fa4ddfe7d3627674/pydantic_core-2.50.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:3e46a9eb0a0901dd6275e6b06ac3a464885ef350ec4121fe486869de8053e4bb", size = 2293630, upload-time = "2026-10-11T18:33:52.362Z" }, + { url = "https://files.pythonhosted.org/packages/a9/00/9eca378335c9c1b72bc779bf6e4c2a82ce784f14ec48a0e87102c9870e05/pydantic_core-2.50.1-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:756d669f04e62ec4148ecfe22be6a4484d9b1181a6ef32e205ebfd200540858b", size = 2468048, upload-time = "2026-10-11T18:33:54.118Z" }, + { url = "https://files.pythonhosted.org/packages/35/ca/e3832e9cf93651251de43c8c5c029ed680a3c1a0a1b17ee26c81bed08cbd/pydantic_core-2.50.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:c516cc5367ca3448995d42cb994bf3f4c9002d2a7c22eac9622551269ad1b807", size = 2512159, upload-time = "2026-10-11T18:33:55.99Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f2/773469b5a10a39116a2cb17edaf6d722f017dd8da07161b371465311c542/pydantic_core-2.50.1-cp314-cp314t-win32.whl", hash = "sha256:9d1bed94af6a63835461f3cf7502058eb166c58c4778e11d0f433cfb1bd69e19", size = 2098406, upload-time = "2026-10-11T18:33:58.043Z" }, + { url = "https://files.pythonhosted.org/packages/ee/42/0bb74f8f25204b259b11ab7c12dc7f180893b6bd706118b385147fb6efd5/pydantic_core-2.50.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c8dce1f1e0e5358b682a6ad3fa5e31b31d4560997b8e61417e9217c8d60f8a0c", size = 2148481, upload-time = "2026-10-11T18:33:59.913Z" }, + { url = "https://files.pythonhosted.org/packages/47/0d/d801646c9679a4e630e15cf521d4108b93854b42a6e6e02391bd4c6b1095/pydantic_core-2.50.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ceff0acc940be2715bd6ad17b24c0e5304abf44f6efd0f81ee8499e640f9dc86", size = 2174972, upload-time = "2026-10-11T18:34:01.875Z" }, + { url = "https://files.pythonhosted.org/packages/b2/84/23984b763d8862a02a13d27a44b6e8169428fd85ecfed88f54c29108604d/pydantic_core-2.50.1-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:8a6791afa2245e6c6b180122d105941644f5bd410bb18623b408808cc41a3102", size = 2224599, upload-time = "2026-10-11T18:34:04.016Z" }, + { url = "https://files.pythonhosted.org/packages/4d/90/a63cf8586abc1d1a3f6d9b18f0224789ebf003851f092ebda3c7c863fd32/pydantic_core-2.50.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:84f34323a61a365b4e9295de6028474754829aaddd59c7bf1a040e7487ef8f3c", size = 2024191, upload-time = "2026-10-11T18:34:05.901Z" }, + { url = "https://files.pythonhosted.org/packages/a6/6a/f34bff9808ffb4907fbf5f5040457d253cacf2da7fce9efbc99ca1b1a44d/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23edad659e8dbd8ca7e4e877fe6c81573abbdf215bd25a68b53e1272f58b80c7", size = 2048252, upload-time = "2026-10-11T18:34:07.757Z" }, + { url = "https://files.pythonhosted.org/packages/b2/54/13f419bf1eb59852003818e25935aaf175687d978f4c4bca70e08fe40a3b/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a5fce22f1e87d181e924e12da7d81cfe031fb3881a5ddf26ad28f141756ca43", size = 2173376, upload-time = "2026-10-11T18:34:09.592Z" }, + { url = "https://files.pythonhosted.org/packages/6d/6c/b5a34d24cd0c81669d8f8339d74e6815abcf2f8fb48ab4b49b84c09be1d5/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c73622ef819328873b53109ee4f77ceb598bffedd02daf916102be3228866b78", size = 2364559, upload-time = "2026-10-11T18:34:11.534Z" }, + { url = "https://files.pythonhosted.org/packages/eb/8d/d64d6216a8df365082665927ff923f183056f9049fee08e9777c9ac05296/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce8c25ca38cc0e3d7753ba180808de2c0c8cb24eae0df64491e40921454e9831", size = 2444697, upload-time = "2026-10-11T18:34:13.535Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0d/1b1149f60a00ea21ba5f70e28acbd40feb4598af414f80f53c921fac07c9/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7689580e72a642ab5ec64d5f55b2e33636fa43b4ebe63c0c2c965ef307c7d1aa", size = 2159961, upload-time = "2026-10-11T18:34:15.56Z" }, + { url = "https://files.pythonhosted.org/packages/1e/35/f236549299dcc78e71e945495d7ec67e78d20844d0999c60601685003031/pydantic_core-2.50.1-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:d5c0e32fdbce7f1e8ef4d11f655694bf5f4175c757a9f1dc2be09b8864e5bcf5", size = 2234581, upload-time = "2026-10-11T18:34:17.502Z" }, + { url = "https://files.pythonhosted.org/packages/6a/85/26901a490522b7f75ef9bb9a7afb73e5bb550f0e1cb5b99a0069183e2eb9/pydantic_core-2.50.1-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:40f523349960fa30f3ea51404308ff50f9997a90df639590f47a057c1f32b415", size = 2244090, upload-time = "2026-10-11T18:34:19.402Z" }, + { url = "https://files.pythonhosted.org/packages/ca/2c/481bcfc70ceeb77a778ca6e5b705fe592cb64735c108157f81b7dd9c280e/pydantic_core-2.50.1-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:d4193206b6587047437f6f11d7e776df23e1c1e23af2a54d9347275614791e10", size = 2277948, upload-time = "2026-10-11T18:34:21.317Z" }, + { url = "https://files.pythonhosted.org/packages/24/eb/f1e09333faa7ba447cde967310f758430cc7c5e987bdab5228816c70030d/pydantic_core-2.50.1-cp315-cp315-musllinux_1_1_armv7l.whl", hash = "sha256:84bc765b282a9d5b7fe0348b8648904f25a6a04b2139da52b1dd30c8ac3a2c8f", size = 2449688, upload-time = "2026-10-11T18:34:23.321Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b3/036bde636db8f76d92996e81aefc75678ab5cec4a07eea1ad0c72a893fc3/pydantic_core-2.50.1-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:ed1e728b39a383c81035b2459cfcb35d99dfb01f7d6ebe3a913bc1cc5b81e459", size = 2479286, upload-time = "2026-10-11T18:34:25.214Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a3/07f018294ee18d144afeb6df1a47d9e92960f014be45c5ed13519fa5af95/pydantic_core-2.50.1-cp315-cp315-win32.whl", hash = "sha256:bc94f474417604bd383d2cd445d071b07dd55fedceed3ce33407bf1fcc107290", size = 2103683, upload-time = "2026-10-11T18:34:27.42Z" }, + { url = "https://files.pythonhosted.org/packages/5f/98/f9bd7e1f9b6709f155acb9ef826d9c3884fe925f811fd8e55b9b52280bad/pydantic_core-2.50.1-cp315-cp315-win_amd64.whl", hash = "sha256:983a662de2571cb2502fc8ff47b6770b03d025d2eb314c92f77b3f07c74720ed", size = 2171681, upload-time = "2026-10-11T18:34:29.508Z" }, + { url = "https://files.pythonhosted.org/packages/10/87/4bb3e1e7f385c076ab5af4d6dd0571b22042cd8207eb810d5b9fef15ae31/pydantic_core-2.50.1-cp315-cp315-win_arm64.whl", hash = "sha256:94845ff54dc5193f228cab81b2662a04bfbb892e95bdc15edf7399000ce57d54", size = 2156251, upload-time = "2026-10-11T18:34:31.455Z" }, + { url = "https://files.pythonhosted.org/packages/47/47/83643225b08f2aef6c8cc4bbe6e3f79c5e139c4364a6e450d0f399d87774/pydantic_core-2.50.1-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:4a53d13cdfbedbfa87f08b83c1a0a5efcc767d785a4b41934fa9cb672670493a", size = 2234363, upload-time = "2026-10-11T18:34:33.65Z" }, + { url = "https://files.pythonhosted.org/packages/5e/66/127ca649ba2f2462039dc1e694c6c01e00a3689f023a617364d3507e6d97/pydantic_core-2.50.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:efbecf43d321f7b9281441f1f213f7c21c66988b0e06c2730ba13ed47a46bb08", size = 2000972, upload-time = "2026-10-11T18:34:36.037Z" }, + { url = "https://files.pythonhosted.org/packages/5e/4f/e421e0a5d653b1203b090b2e48745976988d7338a647940704b5b9c2b399/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc1f08f68dac9f9e83845a8039880aba2ab553eb9b2259c3243a313182c253fe", size = 2022925, upload-time = "2026-10-11T18:34:37.972Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/ea5568e2491e1a71100f15ae8c2d01ef52db42a184a2d4e716bc79e5eb8f/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5dfe41f232befddb9c4377f6cfc702b51595e2d78ed082672adf8758d2c4619f", size = 2192867, upload-time = "2026-10-11T18:34:39.921Z" }, + { url = "https://files.pythonhosted.org/packages/ae/5e/3b8c3a35acbe219909ada5defad5d7d9fed845fb2b37bd5ec518f453c119/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:adc06d218a1cadfd2ec4628424d7d79ce4eba69c2965e7e7b55106f0da5208c8", size = 2388687, upload-time = "2026-10-11T18:34:42.184Z" }, + { url = "https://files.pythonhosted.org/packages/b6/aa/7889b4e515f91a2e8c0ae6b5081fec0feb30c4398d1434e14793c60f173a/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2cf91809d0721ab81592ba67bea7694821679c10b1a2e3c3460082b286c1918a", size = 2464415, upload-time = "2026-10-11T18:34:44.384Z" }, + { url = "https://files.pythonhosted.org/packages/08/78/93449e628eb8a6fdcce3eff9043081179d1bc7ce6f1bff32dc5006f41e00/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23923ab9292c40da026330b1ecf4dc2618c8e86e0422e5d1fbf50d94d64ca4f8", size = 2150823, upload-time = "2026-10-11T18:34:46.392Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f1/72c5bc129fceb0d00f05dc1e67f518c1728de1928c55f81fc13d7690de39/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:f3377c8c2b3ce898423c5e5dd94c7982e30aa7717a7e6ab2470b9de364963709", size = 2245806, upload-time = "2026-10-11T18:34:48.805Z" }, + { url = "https://files.pythonhosted.org/packages/28/2a/922a0e78f3aa6ab837b59f88190233fb8dad546c17995fde9bb3ed3b9b49/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:455a773617b5913bf5c20d0692e5787b119e52c4d40ea644ca31f5758fd31be2", size = 2264160, upload-time = "2026-10-11T18:34:50.876Z" }, + { url = "https://files.pythonhosted.org/packages/52/a8/0f1449e3e1b20941c9372faa18e7b6a092e30cdfa841902473f7989532ac/pydantic_core-2.50.1-cp315-cp315t-musllinux_1_1_aarch64.whl", hash = "sha256:1a9006395dece0e32e704c315eff8a00bede494f6108546cfc5539c89fef4f9a", size = 2294031, upload-time = "2026-10-11T18:34:52.876Z" }, + { url = "https://files.pythonhosted.org/packages/d1/d0/1031f492857de70355fb16524bbb03efce5fd34c93ed4a1ec60be07e0d4b/pydantic_core-2.50.1-cp315-cp315t-musllinux_1_1_armv7l.whl", hash = "sha256:d2d82aa62521c55ddfb000ae70f88cdd8de974078f6024e821dfe5addd0c818f", size = 2468423, upload-time = "2026-10-11T18:34:54.995Z" }, + { url = "https://files.pythonhosted.org/packages/46/52/269ffffa645b8e47906395a39cf9db1151ae9fa4bcd7f47b960bc31baf37/pydantic_core-2.50.1-cp315-cp315t-musllinux_1_1_x86_64.whl", hash = "sha256:009634b83993777ddcd69cad0ffcace43dabde692109528e35f0fde91e386a8b", size = 2512598, upload-time = "2026-10-11T18:34:57.149Z" }, + { url = "https://files.pythonhosted.org/packages/31/5c/e47e28281f20326ff6f3c31d626f0a83e615d2d94ba41bb0ad6ec237184a/pydantic_core-2.50.1-cp315-cp315t-win32.whl", hash = "sha256:3fde4fdc6487a58d944ca87cf5adc95d5f266e872c19599f5f4c0a8a1b1f9f9f", size = 2098535, upload-time = "2026-10-11T18:34:59.313Z" }, + { url = "https://files.pythonhosted.org/packages/03/ad/759e181e69c1b472c60b2049e5f61d5da1deefdd5ce1df4bb2ebcf771f25/pydantic_core-2.50.1-cp315-cp315t-win_amd64.whl", hash = "sha256:1c8632d4ac04e6f91128fca584b3a8a507d81604c24eeaaad00d4be42765c32b", size = 2148645, upload-time = "2026-10-11T18:35:01.571Z" }, + { url = "https://files.pythonhosted.org/packages/6d/56/8a702c27e5be9f47e5f19d8669227424290e4c024e7c370279cbaf244b4e/pydantic_core-2.50.1-cp315-cp315t-win_arm64.whl", hash = "sha256:c3ede305158e75510be50869b319550ab072008c13d64d4ab1e094fb286b6f44", size = 2175297, upload-time = "2026-10-11T18:35:04.079Z" }, + { url = "https://files.pythonhosted.org/packages/a5/09/1bcf160f3cd6333e2a76982fae5963e5032de0bcaaba38464223dc266bfe/pydantic_core-2.50.1-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:76e2e83fa6ec8cdc972d438dafc2522b3a47bee4ec0ae668b29cfb1977ab5242", size = 2217070, upload-time = "2026-10-11T18:35:15.688Z" }, + { url = "https://files.pythonhosted.org/packages/3d/81/f02de95eec7794dfc7a5bc69a834f0e834b31b3009014ce6de903855f775/pydantic_core-2.50.1-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:a51eee75939cf811ac09b278745a6cee7dc873ccfbc8b9af3cc88fe4b7ce25b5", size = 2039027, upload-time = "2026-10-11T18:35:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/4d/16/227746ed3771d9bf2304568b55134849ece34e13b38b1d76de763015c77f/pydantic_core-2.50.1-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae28183297fb0d2b8dc46a1f01d51f5e45825fc5afe76a835a6cb7fb34821295", size = 2109009, upload-time = "2026-10-11T18:35:20.218Z" }, + { url = "https://files.pythonhosted.org/packages/e5/b9/7664d1592a0e5a74903f357cd5b28ceb7336f3473256c5a4e8c8432f1881/pydantic_core-2.50.1-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88e492e8b9d0312e7dc13667c30222abf284dc3b79b5302b3607b41a5784ce61", size = 2269883, upload-time = "2026-10-11T18:35:22.48Z" }, +] + [[package]] name = "pygments" version = "2.21.0" @@ -2985,6 +3117,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, ] +[[package]] +name = "typing-inspection" +version = "0.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size = 76928, upload-time = "2026-08-12T12:37:25.997Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" }, +] + [[package]] name = "tzdata" version = "2026.5" From 5986c748ebc4e22f45c1292980bfa2a16a2ce65d Mon Sep 17 00:00:00 2001 From: Leif Denby Date: Tue, 27 Oct 2026 13:43:35 +0100 Subject: [PATCH 4/4] Use a non-typo unknown key in config test to satisfy codespell Co-Authored-By: Claude Opus 5.5 --- tests/test_config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_config.py b/tests/test_config.py index 64f2b5d..dd5fbda 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -31,8 +31,8 @@ def test_example_config_validates(): def test_unknown_top_level_key_is_rejected(): - with pytest.raises(ValidationError, match="alignmnet"): - load_config(_config(alignmnet={"reference": "fc"})) + with pytest.raises(ValidationError, match="not_a_section"): + load_config(_config(not_a_section={"reference": "fc"})) def test_missing_alignment_reference_is_rejected():