diff --git a/docs/api.rst b/docs/api.rst index bb7cb2f9..f6b7658c 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -65,6 +65,13 @@ DataArray xarray.DataArray.pint.bfill xarray.DataArray.pint.interpolate_na +Wrapping quantity-unaware functions +----------------------------------- +.. autosummary:: + :toctree: generated/ + + pint_xarray.expects + Testing ------- diff --git a/docs/terminology.rst b/docs/terminology.rst index 4fe6534d..df784fae 100644 --- a/docs/terminology.rst +++ b/docs/terminology.rst @@ -5,6 +5,7 @@ Terminology unit-like A `pint`_ unit definition, as accepted by :py:class:`pint.Unit`. - May be either a :py:class:`str` or a :py:class:`pint.Unit` instance. + May be a :py:class:`str`, a :py:class:`pint.Unit` instance or + :py:obj:`None`. .. _pint: https://pint.readthedocs.io/en/stable diff --git a/docs/whats-new.rst b/docs/whats-new.rst index 1b9dcd2e..08f80293 100644 --- a/docs/whats-new.rst +++ b/docs/whats-new.rst @@ -18,6 +18,8 @@ What's new By `Justus Magin `_. - Switch to using pixi for all dependency management (:pull:`314`). By `Justus Magin `_. +- Added the :py:func:`pint_xarray.expects` decorator to allow wrapping quantity-unaware functions (:issue:`141`, :pull:`316`). + By `Justus Magin `_ and `Tom Nicholas `_. 0.5.1 (10 Aug 2025) ------------------- diff --git a/pint_xarray/__init__.py b/pint_xarray/__init__.py index 7fb5b327..d7890a54 100644 --- a/pint_xarray/__init__.py +++ b/pint_xarray/__init__.py @@ -3,13 +3,14 @@ import pint from pint_xarray import accessors, formatting, testing # noqa: F401 +from pint_xarray._expects import expects from pint_xarray.accessors import default_registry as unit_registry from pint_xarray.accessors import setup_registry from pint_xarray.index import PintIndex try: __version__ = version("pint-xarray") -except Exception: +except Exception: # pragma: no cover # Local copy or not installed with setuptools. # Disable minimum version checks on downstream libraries. __version__ = "999" @@ -23,4 +24,5 @@ "unit_registry", "setup_registry", "PintIndex", + "expects", ] diff --git a/pint_xarray/_expects.py b/pint_xarray/_expects.py new file mode 100644 index 00000000..24a5ffdb --- /dev/null +++ b/pint_xarray/_expects.py @@ -0,0 +1,260 @@ +import functools +import inspect +import itertools +from inspect import Parameter + +import pint +import pint.testing +import xarray as xr + +from pint_xarray.accessors import get_registry +from pint_xarray.conversion import extract_units +from pint_xarray.itertools import zip_mappings + +variable_parameters = (Parameter.VAR_POSITIONAL, Parameter.VAR_KEYWORD) + + +def _number_of_results(result): + if isinstance(result, tuple): + return len(result) + elif result is None: + return 0 + else: + return 1 + + +def expects(*args_units, return_value=None, **kwargs_units): + """ + Decorator which ensures the inputs and outputs of the decorated + function are expressed in the expected units. + + Arguments to the decorated function are checked for the specified + units, converting to those units if necessary, and then stripped + of their units before being passed into the undecorated + function. Therefore the undecorated function should expect + unquantified DataArrays, Datasets, or numpy-like arrays, but with + the values expressed in specific units. + + Parameters + ---------- + func : callable + Function to decorate, which accepts zero or more + xarray.DataArrays or numpy-like arrays as inputs, and may + optionally return one or more xarray.DataArrays or numpy-like + arrays. + *args_units : unit-like or mapping of hashable to unit-like, optional + Units to expect for each positional argument given to func. + + The decorator will first check that arguments passed to the + decorated function possess these specific units (or will + attempt to convert the argument to these units), then will + strip the units before passing the magnitude to the wrapped + function. + + A value of None indicates not to check that argument for units + (suitable for flags and other non-data arguments). + return_value : unit-like or list of unit-like or mapping of hashable to unit-like \ + or list of mapping of hashable to unit-like, optional + The expected units of the returned value(s), either as a + single unit or as a list of units. The decorator will attach + these units to the variables returned from the function. + + A value of None indicates not to attach any units to that + return value (suitable for flags and other non-data results). + **kwargs_units : mapping of hashable to unit-like, optional + Unit to expect for each keyword argument given to func. + + The decorator will first check that arguments passed to the decorated + function possess these specific units (or will attempt to convert the + argument to these units), then will strip the units before passing the + magnitude to the wrapped function. + + A value of None indicates not to check that argument for units (suitable + for flags and other non-data arguments). + + Returns + ------- + return_values : Any + Return values of the wrapped function, either a single value or a tuple + of values. These will be given units according to ``return_value``. + + Raises + ------ + TypeError + If any of the units are not a valid type. + ValueError + If the number of arguments or return values does not match the number of + units specified. Also thrown if any parameter does not have a unit + specified. + + See Also + -------- + pint.wraps + + Examples + -------- + Decorating a function which takes one quantified input, but + returns a non-data value (in this case a boolean). + + >>> @expects("deg C") + ... def above_freezing(temp): + ... return temp > 0 + ... + + Decorating a function which allows any dimensions for the array, but also + accepts an optional `weights` keyword argument, which must be dimensionless. + + >>> @expects(None, weights="dimensionless") + ... def mean(da, weights=None): + ... if weights: + ... return da.weighted(weights=weights).mean() + ... else: + ... return da.mean() + ... + """ + + def outer(func): + signature = inspect.signature(func) + + params_units = signature.bind(*args_units, **kwargs_units) + + missing_params = [ + name + for name, p in signature.parameters.items() + if p.kind not in variable_parameters and name not in params_units.arguments + ] + if missing_params: + raise ValueError( + "Missing units for the following parameters: " + + ", ".join(map(repr, missing_params)) + ) + + n_expected_results = _number_of_results(return_value) + + @functools.wraps(func) + def wrapper(*args, **kwargs): + nonlocal return_value + + params = signature.bind(*args, **kwargs) + # don't apply defaults, as those can't be quantities and thus must + # already be in the correct units + + spec_units = dict( + enumerate( + itertools.chain.from_iterable( + spec.values() if isinstance(spec, dict) else (spec,) + for spec in params_units.arguments.values() + if spec is not None + ) + ) + ) + params_units_ = dict( + enumerate( + itertools.chain.from_iterable( + ( + extract_units(param) + if isinstance(param, (xr.DataArray, xr.Dataset)) + else (param.units,) + ) + for name, param in params.arguments.items() + if isinstance(param, (xr.DataArray, xr.Dataset, pint.Quantity)) + ) + ) + ) + + ureg = get_registry( + None, + dict(spec_units) if spec_units else {}, + dict(params_units_) if params_units else {}, + ) + + errors = [] + for name, (value, units) in zip_mappings( + params.arguments, params_units.arguments + ): + try: + if units is None: + if isinstance(value, pint.Quantity) or ( + isinstance(value, (xr.DataArray, xr.Dataset)) + and value.pint.units + ): + raise TypeError( + "Passed in a quantity where none was expected" + ) + continue + if isinstance(value, pint.Quantity): + params.arguments[name] = value.m_as(units) + elif isinstance(value, (xr.DataArray, xr.Dataset)): + params.arguments[name] = value.pint.to(units).pint.dequantify() + else: + raise TypeError( + f"Attempting to convert non-quantity {value} to {units}." + ) + except ( + TypeError, + pint.errors.UndefinedUnitError, + pint.errors.DimensionalityError, + ) as e: + e.add_note( + f"expects: raised while trying to convert parameter {name}" + ) + errors.append(e) + + if errors: + raise ExceptionGroup("Errors while converting parameters", errors) + + result = func(*params.args, **params.kwargs) + + n_results = _number_of_results(result) + if return_value is not None and ( + (isinstance(result, tuple) ^ isinstance(return_value, tuple)) + or (n_results != n_expected_results) + ): + message = "mismatched number of return values:" + if n_results != n_expected_results: + message += f" expected {n_expected_results} but got {n_results}." + elif isinstance(result, tuple) and not isinstance(return_value, tuple): + message += ( + " expected a single return value but got a 1-sized tuple." + ) + else: + message += ( + " expected a 1-sized tuple but got a single return value." + ) + raise ValueError(message) + + if result is None: + return + + if not isinstance(result, tuple): + result = (result,) + if not isinstance(return_value, tuple): + return_value = (return_value,) + + final_result = [] + errors = [] + for index, (value, units) in enumerate(zip(result, return_value)): + if units is not None: + try: + if isinstance(value, (xr.Dataset, xr.DataArray)): + value = value.pint.quantify(units) + else: + value = ureg.Quantity(value, units) + except Exception as e: + e.add_note( + f"expects: raised while trying to convert return value {index}" + ) + errors.append(e) + + final_result.append(value) + + if errors: + raise ExceptionGroup("Errors while converting return values", errors) + + if n_results == 1: + return final_result[0] + return tuple(final_result) + + return wrapper + + return outer diff --git a/pint_xarray/itertools.py b/pint_xarray/itertools.py new file mode 100644 index 00000000..f03634d0 --- /dev/null +++ b/pint_xarray/itertools.py @@ -0,0 +1,30 @@ +import itertools +from functools import reduce + + +def separate(predicate, iterable): + evaluated = ((predicate(el), el) for el in iterable) + + key = lambda x: x[0] + grouped = itertools.groupby(sorted(evaluated, key=key), key=key) + + groups = {label: [el for _, el in group] for label, group in grouped} + + return groups[False], groups[True] + + +def unique(iterable): + return list(dict.fromkeys(iterable)) + + +def zip_mappings(*mappings): + def common_keys(a, b): + all_keys = unique(itertools.chain(a.keys(), b.keys())) + intersection = set(a.keys()).intersection(b.keys()) + + return [key for key in all_keys if key in intersection] + + keys = list(reduce(common_keys, mappings)) + + for key in keys: + yield key, tuple(m[key] for m in mappings) diff --git a/pint_xarray/tests/test_expects.py b/pint_xarray/tests/test_expects.py new file mode 100644 index 00000000..4d44165c --- /dev/null +++ b/pint_xarray/tests/test_expects.py @@ -0,0 +1,296 @@ +import re + +import pint +import pytest +import xarray as xr + +import pint_xarray +from pint_xarray.testing import assert_units_equal + +ureg = pint_xarray.unit_registry + + +class TestExpects: + @pytest.mark.parametrize( + ["values", "units", "expected"], + ( + ((ureg.Quantity(1, "m"), 2), ("mm", None, None), 500), + ((ureg.Quantity(1, "m"), ureg.Quantity(0.5, "s")), ("mm", "ms", None), 2), + ( + (xr.DataArray(4).pint.quantify("km"), 2), + ("m", None, None), + xr.DataArray(2000), + ), + ( + ( + xr.DataArray([4, 2, 0]).pint.quantify("cm"), + xr.DataArray([4, 2, 1]).pint.quantify("mg"), + ), + ("m", "g", None), + xr.DataArray([10, 10, 0]), + ), + ( + (ureg.Quantity(16, "m"), 2, ureg.Quantity(4, "s")), + ("mm", None, "ms"), + 2, + ), + ), + ) + def test_args(self, values, units, expected): + @pint_xarray.expects(*units) + def func(a, b, c=1): + return a / (b * c) + + actual = func(*values) + + if isinstance(actual, xr.DataArray): + xr.testing.assert_identical(actual, expected) + elif isinstance(actual, pint.Quantity): + pint.testing.assert_equal(actual, expected) + else: + assert actual == expected + + @pytest.mark.parametrize( + ["value", "units", "error", "message", "multiple"], + ( + ( + ureg.Quantity(1, "m"), + (None, None), + TypeError, + "Passed in a quantity where none was expected", + True, + ), + (1, ("m", None), TypeError, "Attempting to convert non-quantity", True), + ( + 1, + (None,), + ValueError, + "Missing units for the following parameters: 'b'", + False, + ), + ( + ureg.Quantity(1, "m"), + ("nonsense_unit", None), + pint.errors.UndefinedUnitError, + "'nonsense_unit' is not defined in the unit registry", + True, + ), + ), + ) + def test_args_error(self, value, units, error, message, multiple): + if multiple: + root_error = ExceptionGroup + root_message = "Errors while converting parameters" + else: + root_error = error + root_message = message + + with pytest.raises(root_error, match=root_message) as excinfo: + + @pint_xarray.expects(*units) + def func(a, b=1): + return a * b + + func(value) + + if not multiple: + return + + group = excinfo.value + assert len(group.exceptions) == 1, f"Found {len(group.exceptions)} exceptions" + exc = group.exceptions[0] + assert isinstance( + exc, error + ), f"Unexpected exception type: {type(exc)}, expected {error}" + if not re.search(message, str(exc)): + raise AssertionError(f"exception {exc!r} did not match pattern {message!r}") + + @pytest.mark.parametrize( + ["values", "units", "expected"], + ( + ( + {"a": ureg.Quantity(1, "m"), "b": 2}, + {"a": "mm", "b": None, "c": None}, + 1000, + ), + ( + {"a": 2, "b": ureg.Quantity(100, "cm")}, + {"a": None, "b": "m", "c": None}, + 4, + ), + ( + {"a": ureg.Quantity(1, "m"), "b": ureg.Quantity(0.5, "s")}, + {"a": "mm", "b": "ms", "c": None}, + 4, + ), + ( + {"a": xr.DataArray(4).pint.quantify("km"), "b": 2}, + {"a": "m", "b": None, "c": None}, + xr.DataArray(4000), + ), + ( + { + "a": xr.DataArray([4, 2, 0]).pint.quantify("cm"), + "b": xr.DataArray([4, 2, 1]).pint.quantify("mg"), + }, + {"a": "m", "b": "g", "c": None}, + xr.DataArray([20, 20, 0]), + ), + ), + ) + def test_kwargs(self, values, units, expected): + @pint_xarray.expects(**units) + def func(a, b, c=2): + return a / b * c + + actual = func(**values) + + if isinstance(actual, xr.DataArray): + xr.testing.assert_identical(actual, expected) + elif isinstance(actual, pint.Quantity): + pint.testing.assert_equal(actual, expected) + else: + assert actual == expected + + @pytest.mark.parametrize( + ["values", "return_value_units", "expected"], + ( + ((1, 2), ("m", "s"), (ureg.Quantity(1, "m"), ureg.Quantity(2, "s"))), + ((1, 2), "m / s", ureg.Quantity(0.5, "m / s")), + ((1, 2), None, 0.5), + ( + (xr.DataArray(2), 2), + ("m", "s"), + (xr.DataArray(2).pint.quantify("m"), ureg.Quantity(2, "s")), + ), + ( + (xr.DataArray(2), 2), + "kg / m^2", + xr.DataArray(1).pint.quantify("kg / m^2"), + ), + ), + ) + def test_return_value(self, values, return_value_units, expected): + multiple = isinstance(return_value_units, tuple) + + @pint_xarray.expects(a=None, b=None, return_value=return_value_units) + def func(a, b): + if multiple: + return a, b + else: + return a / b + + actual = func(*values) + if isinstance(actual, xr.DataArray): + xr.testing.assert_identical(actual, expected) + elif isinstance(actual, pint.Quantity): + pint.testing.assert_equal(actual, expected) + else: + assert actual == expected + + def test_return_value_none(self): + @pint_xarray.expects(None) + def func(a): + return None + + actual = func(1) + assert actual is None + + def test_return_value_none_error(self): + @pint_xarray.expects(return_value="Hz") + def func(): + return None + + with pytest.raises( + ValueError, + match="mismatched number of return values: expected 1 but got 0.", + ): + func() + + @pytest.mark.parametrize( + [ + "return_value_units", + "multiple_units", + "error", + "multiple_errors", + "message", + ], + ( + ( + ("m", "s"), + False, + ValueError, + False, + "mismatched number of return values", + ), + ( + "m", + True, + ValueError, + False, + "mismatched number of return values: expected 1 but got 2", + ), + ( + ("m",), + True, + ValueError, + False, + "mismatched number of return values: expected 1 but got 2", + ), + (1, False, TypeError, True, "units must be of type"), + (("m",), False, ValueError, False, ".*expected a 1-sized tuple.*"), + ("m", 1, ValueError, False, ".*expected a single return value.*"), + ), + ) + def test_return_value_error( + self, return_value_units, multiple_units, error, multiple_errors, message + ): + if multiple_errors: + root_error = ExceptionGroup + root_message = "Errors while converting return values" + else: + root_error = error + root_message = message + + with pytest.raises(root_error, match=root_message) as excinfo: + + @pint_xarray.expects(a=None, b=None, return_value=return_value_units) + def func(a, b): + if not isinstance(multiple_units, bool) and multiple_units == 1: + print("return 1-tuple") + return (a / b,) + elif multiple_units: + return a, b + else: + return a / b + + func(1, 2) + + if not multiple_errors: + return + + group = excinfo.value + assert len(group.exceptions) == 1, f"Found {len(group.exceptions)} exceptions" + exc = group.exceptions[0] + assert isinstance( + exc, error + ), f"Unexpected exception type: {type(exc)}, expected {error}" + if not re.search(message, str(exc)): + raise AssertionError(f"exception {exc!r} did not match pattern {message!r}") + + def test_datasets(self): + @pint_xarray.expects({"m": "kg", "a": "m / s^2"}, return_value={"f": "newtons"}) + def second_law(ds): + f_da = ds["m"] * ds["a"] + return f_da.to_dataset(name="f") + + ds = xr.Dataset({"m": 0.1, "a": 10}).pint.quantify( + {"m": "tons", "a": "feet / second^2"} + ) + + expected = xr.Dataset({"f": ds["m"] * ds["a"]}).pint.to("newtons") + + actual = second_law(ds) + + assert_units_equal(actual, expected) + xr.testing.assert_allclose(actual, expected) diff --git a/pint_xarray/tests/test_itertools.py b/pint_xarray/tests/test_itertools.py new file mode 100644 index 00000000..61c2169b --- /dev/null +++ b/pint_xarray/tests/test_itertools.py @@ -0,0 +1,48 @@ +import pytest + +from pint_xarray.itertools import separate, unique, zip_mappings + + +@pytest.mark.parametrize( + ["predicate", "iterable"], + ( + (lambda x: x % 2 == 0, range(10)), + (lambda x: x in [0, 2, 3, 5], range(10)), + (lambda x: "s" in x, ["ab", "de", "sf", "fs"]), + ), +) +def test_separate(predicate, iterable): + actual_false, actual_true = separate(predicate, iterable) + + expected_true = [el for el in iterable if predicate(el)] + expected_false = [el for el in iterable if not predicate(el)] + + assert actual_true == expected_true + assert actual_false == expected_false + + +@pytest.mark.parametrize( + ["iterable", "expected"], + ( + ([5, 4, 4, 1, 2, 3, 2, 1], [5, 4, 1, 2, 3]), + (list("dadgafffgaefed"), list("dagfe")), + ), +) +def test_unique(iterable, expected): + actual = unique(iterable) + + assert actual == expected + + +@pytest.mark.parametrize( + ["mappings", "expected"], + ( + (({"a": 1, "b": 2}, {"a": 2, "b": 3}), [("a", (1, 2)), ("b", (2, 3))]), + (({"a": 1, "c": 2}, {"a": 2, "b": 0}), [("a", (1, 2))]), + (({"a": 1, "c": 2}, {"c": 2, "b": 0}), [("c", (2, 2))]), + (({"a": 1}, {"c": 2, "b": 0}), []), + ), +) +def test_zip_mappings(mappings, expected): + actual = list(zip_mappings(*mappings)) + assert actual == expected diff --git a/pixi.lock b/pixi.lock index 663d0fbe..161318e2 100644 --- a/pixi.lock +++ b/pixi.lock @@ -134,6 +134,225 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_31.conda - conda: https://conda.anaconda.org/conda-forge/noarch/xarray-2025.7.1-pyhd8ed1ab_0.conda - pypi: ./ + dev: + channels: + - url: https://conda.anaconda.org/conda-forge/ + indexes: + - https://pypi.org/simple + packages: + linux-64: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.10.3-py313h3dea7bd_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/flexcache-0.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/flexparser-0.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipdb-0.13.13-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.4.0-pyhfa0c392_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-34_h59b9bed_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-34_he106b2a_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.1.0-h69a702a_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.1.0-hcea5267_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-34_h7ac8fdf_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.4-h0c1763c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.1.7-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.2-py313hf6604e3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.2-h26f9b46_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-2.3.1-py313h08cd8bf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pickleshare-0.7.5-pyhd8ed1ab_1004.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pint-0.24.4-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.3.8-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.51-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.4.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-6.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.8.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.5-hec9711d_102_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.14.1-h4440ef1_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.14.1-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.13-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/xarray-2025.8.0-pyhd8ed1ab_0.conda + - pypi: ./ + osx-arm64: + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-h99b78c6_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/coverage-7.10.3-py313ha0c97b7_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/flexcache-0.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/flexparser-0.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-75.1-hfee45f7_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipdb-0.13.13-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.4.0-pyhfa0c392_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libblas-3.9.0-34_h10e41b3_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcblas-3.9.0-34_hb3479ef_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-20.1.8-hf598326_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.7.1-hec049ff_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.4.6-h1da3d7d_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran-15.1.0-hfdf1602_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-15.1.0-hb74de2c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblapack-3.9.0-34_hc9a63f6_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.1-h39f12f2_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h5505292_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenblas-0.3.30-openmp_h60d53f8_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.50.4-h4237e3c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-openmp-20.1.8-hbb9b287_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.1.7-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/numpy-2.3.2-py313h674b998_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.5.2-he92f556_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pandas-2.3.1-py313hd1f53c0_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pickleshare-0.7.5-pyhd8ed1ab_1004.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pint-0.24.4-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.3.8-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.51-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.4.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-6.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.8.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.13.5-hf3f3da0_102_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h1d1bf99_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.14.1-h4440ef1_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.14.1-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.13-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/xarray-2025.8.0-pyhd8ed1ab_0.conda + - pypi: ./ + win-64: + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h2466b09_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-h4c7d964_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/coverage-7.10.3-py313hd650c13_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/flexcache-0.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/flexparser-0.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipdb-0.13.13-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.4.0-pyh6be1c34_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.9.0-34_h5709861_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.9.0-34_h2a3cdd5_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.7.1-hac47afa_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.4.6-h537db12_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libhwloc-2.12.1-default_h88281d1_1000.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-hc1393d2_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblapack-3.9.0-34_hf9ab0e9_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.1-h2466b09_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libmpdec-4.0.0-h2466b09_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.50.4-hf5d6505_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libwinpthread-12.0.0.r4.gg4f2fc60ca-h57928b3_9.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-2.13.8-h741aa76_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.1-h2466b09_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/llvm-openmp-20.1.8-hfa2b4ca_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.1.7-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/mkl-2024.2.2-h57928b3_16.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/numpy-2.3.2-py313hce7ae62_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.5.2-h725018a_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pandas-2.3.1-py313hc90dcd4_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pickleshare-0.7.5-pyhd8ed1ab_1004.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pint-0.24.4-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.3.8-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.51-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.4.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-6.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.8.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.13.5-h7de537c_102_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2021.13.0-h18a62a1_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h2c6b04d_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.14.1-h4440ef1_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.14.1-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.22621.0-h57928b3_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_31.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_31.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_31.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.13-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/xarray-2025.8.0-pyhd8ed1ab_0.conda + - pypi: ./ docs: channels: - url: https://conda.anaconda.org/conda-forge/ @@ -935,7 +1154,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/fe/5e/3be305568fe5f34448807976dc82fc151d76c3e0e03958f34770286278c1/flexparser-0.4-py3-none-any.whl - pypi: https://pypi.anaconda.org/scientific-python-nightly-wheels/simple/numpy/2.4.0.dev0/numpy-2.4.0.dev0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://pypi.anaconda.org/scientific-python-nightly-wheels/simple/pandas/3.0.0.dev0+2300.g7863029c30/pandas-3.0.0.dev0+2300.g7863029c30-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl - - pypi: git+https://github.com/hgrecco/pint.git#88f2ff02921b6fae82185c966a865a6fa592ed12 + - pypi: git+https://github.com/hgrecco/pint.git#6e0d03862a863169d18e6f8687472ec256718732 - pypi: https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - pypi: https://pypi.anaconda.org/scientific-python-nightly-wheels/simple/scipy/1.17.0.dev0/scipy-1.17.0.dev0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl @@ -979,7 +1198,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/fe/5e/3be305568fe5f34448807976dc82fc151d76c3e0e03958f34770286278c1/flexparser-0.4-py3-none-any.whl - pypi: https://pypi.anaconda.org/scientific-python-nightly-wheels/simple/numpy/2.4.0.dev0/numpy-2.4.0.dev0-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://pypi.anaconda.org/scientific-python-nightly-wheels/simple/pandas/3.0.0.dev0+2300.g7863029c30/pandas-3.0.0.dev0+2300.g7863029c30-cp313-cp313-macosx_11_0_arm64.whl - - pypi: git+https://github.com/hgrecco/pint.git#88f2ff02921b6fae82185c966a865a6fa592ed12 + - pypi: git+https://github.com/hgrecco/pint.git#6e0d03862a863169d18e6f8687472ec256718732 - pypi: https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - pypi: https://pypi.anaconda.org/scientific-python-nightly-wheels/simple/scipy/1.17.0.dev0/scipy-1.17.0.dev0-cp313-cp313-macosx_12_0_arm64.whl @@ -1024,7 +1243,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/fe/5e/3be305568fe5f34448807976dc82fc151d76c3e0e03958f34770286278c1/flexparser-0.4-py3-none-any.whl - pypi: https://pypi.anaconda.org/scientific-python-nightly-wheels/simple/numpy/2.4.0.dev0/numpy-2.4.0.dev0-cp313-cp313-win_amd64.whl - pypi: https://pypi.anaconda.org/scientific-python-nightly-wheels/simple/pandas/3.0.0.dev0+2300.g7863029c30/pandas-3.0.0.dev0+2300.g7863029c30-cp313-cp313-win_amd64.whl - - pypi: git+https://github.com/hgrecco/pint.git#88f2ff02921b6fae82185c966a865a6fa592ed12 + - pypi: git+https://github.com/hgrecco/pint.git#6e0d03862a863169d18e6f8687472ec256718732 - pypi: https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - pypi: https://pypi.anaconda.org/scientific-python-nightly-wheels/simple/scipy/1.17.0.dev0/scipy-1.17.0.dev0-cp313-cp313-win_amd64.whl @@ -2390,6 +2609,21 @@ packages: - pkg:pypi/coverage?source=hash-mapping size: 386646 timestamp: 1754308490349 + - conda: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.10.3-py313h3dea7bd_0.conda + sha256: f8fd0cb192e4d1a7c98c382d159fb1398f3c1bffee2ba82252f0fc74f5e6e6ef + md5: 8a6c0256d67a5688ba3605a9a0e318b3 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + - tomli + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/coverage?source=hash-mapping + size: 387953 + timestamp: 1754910179600 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/coverage-7.10.2-py311h2fe624c_0.conda sha256: 7fef83c23cb95a4d964a1c360630b8f3d9d27c1eb3ae1ce8b17bcba3bd04ac84 md5: 7f7346c86f66f9fc8e00e3e9adc209bd @@ -2435,6 +2669,21 @@ packages: - pkg:pypi/coverage?source=hash-mapping size: 384709 timestamp: 1754308344502 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/coverage-7.10.3-py313ha0c97b7_0.conda + sha256: fa5e64ec8ae433df8540906548247ba61c0f4d3b59182f40f7bd82954f0e64a6 + md5: 1c865f8cb4a588d394a8979be337d2fd + depends: + - __osx >=11.0 + - python >=3.13,<3.14.0a0 + - python >=3.13,<3.14.0a0 *_cp313 + - python_abi 3.13.* *_cp313 + - tomli + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/coverage?source=hash-mapping + size: 388940 + timestamp: 1754910481803 - conda: https://conda.anaconda.org/conda-forge/win-64/coverage-7.10.2-py311h3f79411_0.conda sha256: 0f991f9ef21897cf27ed03bcfdc7f6dcd5ab01468ee199df46e2c7f047df6c2a md5: 4ffaeacc144145419a2144161dcb4728 @@ -2483,6 +2732,22 @@ packages: - pkg:pypi/coverage?source=hash-mapping size: 411192 timestamp: 1754308378027 + - conda: https://conda.anaconda.org/conda-forge/win-64/coverage-7.10.3-py313hd650c13_0.conda + sha256: ab2cafef225674e7d098661edc389ec434d9ae4ba40f7843ebc27caedaec78a0 + md5: 4b133690bf70e697f8a11fbbef7d1ad5 + depends: + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + - tomli + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/coverage?source=hash-mapping + size: 413476 + timestamp: 1754910297815 - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.5-py313hd8ed1ab_102.conda noarch: generic sha256: 058c8156ff880b1180a36b94307baad91f9130d0e3019ad8c7ade035852016fb @@ -2935,6 +3200,20 @@ packages: - pkg:pypi/iniconfig?source=hash-mapping size: 11474 timestamp: 1733223232820 + - conda: https://conda.anaconda.org/conda-forge/noarch/ipdb-0.13.13-pyhd8ed1ab_1.conda + sha256: 33275d537122e67df200203d541170db8b55886667d30cc7262cc1e463b04406 + md5: 044c5249ad8ea18a414d07baa1f369ea + depends: + - decorator + - ipython + - python >=3.9 + - toml >=0.10.2 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/ipdb?source=hash-mapping + size: 18713 + timestamp: 1734884952029 - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-6.30.1-pyh3521513_0.conda sha256: 3dd6fcdde5e40a3088c9ecd72c29c6e5b1429b99e927f41c8cee944a07062046 md5: 953007d45edeb098522ac860aade4fcf @@ -4023,6 +4302,17 @@ packages: purls: [] size: 638142 timestamp: 1740128665984 + - conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-hc1393d2_2.conda + sha256: 0dcdb1a5f01863ac4e8ba006a8b0dc1a02d2221ec3319b5915a1863254d7efa7 + md5: 64571d1dd6cdcfa25d0664a5950fdaa2 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: LGPL-2.1-only + purls: [] + size: 696926 + timestamp: 1754909290005 - conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.1.0-hb9d3cd8_0.conda sha256: 98b399287e27768bf79d48faba8a99a2289748c65cd342ca21033fab1860d4a4 md5: 9fa334557db9f63da6c9285fd2a48638 @@ -6360,9 +6650,9 @@ packages: - pkg:pypi/pillow?source=hash-mapping size: 42177350 timestamp: 1751482641943 - - pypi: git+https://github.com/hgrecco/pint.git#88f2ff02921b6fae82185c966a865a6fa592ed12 + - pypi: git+https://github.com/hgrecco/pint.git#6e0d03862a863169d18e6f8687472ec256718732 name: pint - version: 0.25rc2 + version: 0.26.dev5+g6e0d038 requires_dist: - flexcache>=0.3 - flexparser>=0.4 @@ -6440,8 +6730,8 @@ packages: timestamp: 1753127340588 - pypi: ./ name: pint-xarray - version: 0.5.2.dev31+gc9306ba.d20250810 - sha256: bc8542ad204c48c38cd0adb20a151662ea2e47cd5d5c7438396cbbe4440ef1d9 + version: 0.5.2.dev32+g68973ed.d20250815 + sha256: 02b9afd4dcdf5ddb45ac0f2dbb2b0c98bdbc132fa73b93271b06ab0c47add6d6 requires_dist: - xarray>=2023.7.0 - numpy>=1.26 @@ -8126,6 +8416,40 @@ packages: - pkg:pypi/xarray?source=hash-mapping size: 883059 timestamp: 1752140533516 + - conda: https://conda.anaconda.org/conda-forge/noarch/xarray-2025.8.0-pyhd8ed1ab_0.conda + sha256: 91c476aab9f878a243b4edb31a3cf6c7bb4e873ff537315f475769b890bbbb29 + md5: a7b1b2ffdbf18922945874ccbe1420aa + depends: + - numpy >=1.26 + - packaging >=24.1 + - pandas >=2.2 + - python >=3.11 + constrains: + - flox >=0.9 + - toolz >=0.12 + - h5netcdf >=1.3 + - dask-core >=2024.6 + - iris >=3.9 + - bottleneck >=1.4 + - hdf5 >=1.14 + - h5py >=3.11 + - cftime >=1.6 + - cartopy >=0.23 + - pint >=0.24 + - sparse >=0.15 + - nc-time-axis >=1.4 + - matplotlib-base >=3.8 + - seaborn-base >=0.13 + - distributed >=2024.6 + - netcdf4 >=1.6.0 + - zarr >=2.18 + - scipy >=1.13 + - numba >=0.60 + license: Apache-2.0 + purls: + - pkg:pypi/xarray?source=compressed-mapping + size: 894173 + timestamp: 1755208520958 - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.12-hb9d3cd8_0.conda sha256: ed10c9283974d311855ae08a16dfd7e56241fac632aec3b92e3cfe73cff31038 md5: f6ebe2cb3f82ba6c057dde5d9debe4f7 diff --git a/pyproject.toml b/pyproject.toml index 4d5d30f1..58107b57 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -173,6 +173,11 @@ build-docs = { cmd = "rm -rf generated/; python -m sphinx -b html -w warnings.lo autobuild-docs = { cmd = "sphinx-autobuild -b html -w warnings.log -W -Tn -j auto . _build", cwd = "docs" } build-docs-rtd = { cmd = "python -m sphinx -b html -W -T -j auto . $READTHEDOCS_OUTPUT/html", cwd = "docs" } +[tool.pixi.feature.dev.dependencies] +ipython = "*" +ipdb = "*" +python = "3.13.*" + [tool.pixi.environments] tests = ["tests"] nightly = { features = ["tests", "nightly"], no-default-feature = true } @@ -181,3 +186,4 @@ tests-py311 = ["tests", "tests-py311"] tests-py312 = ["tests", "tests-py312"] tests-py313 = ["tests", "tests-py313"] doctests = ["tests", "tests-py313"] +dev = ["tests", "dev"]