From 2632f2fd08b469e9c0b27fb6b4113352fbdb18d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?William=20Laur=C3=A9n?= Date: Sat, 25 Jul 2026 10:20:52 +0300 Subject: [PATCH 1/3] fix: resolve holistic review findings for 1.9.3 Guard QuantityFrame.scan_parquet against an upstream polars panic: the Parquet statistics reader has no implementation for extension dtypes, so a pushed-down predicate on a unit-typed column aborts a worker thread. Every unit-typed column is re-wrapped through a metadata-only identity that the optimizer will not push a predicate through, and the upstream bug itself is pinned by a test. Reject non-numeric numpy magnitudes. _cast_array_float now allow-lists the float, signed and unsigned integer dtype kinds instead of relying on an unsafe cast, so a bool mask, a complex array (previously silently losing its imaginary part) and datetime64/timedelta64 (previously silently becoming tick counts) are refused with the same messages the pl.Series path already used. Stop testing dimensionality with an O(n) magnitude pass. pint's quantity-level `dimensionless` converts the whole magnitude to root units; the unit-level check is magnitude-independent and answers identically for every registered unit. Applied in __mul__, __truediv__, __floordiv__ and check_compatibility, with the isinstance test reordered first so `q * 2.0` skips it entirely. Validate CoolProp expression requests eagerly: cp.fluid now checks the output property and the fluid name where the expression is built rather than at collect() time inside a plugin node. The name check moved out of encomp.fluids into encomp.coolprop as public validate_fluid_config (cached) / validate_fluid_name, so both entry points share one implementation. Other fixes: ndim reports 1 for pl.Series and pl.Expr magnitudes; round() works for every magnitude container; units_of degrades per column and warns when a foreign producer wrote unparseable unit metadata; QuantityFrame has a collection-free repr; typed-read and with_units errors name the offending column; the division-by-zero divergence between containers is pinned as deliberate; a dangling ". " is gone. Rename fluids.BackendKind/BACKEND_KIND to NativeInterface/NATIVE_INTERFACE: it selects the native interface (AbstractState vs HAPropsSI), so "backend" now refers only to the CoolProp equation-of-state backend everywhere. Docs: correct two stale claims about bare `import encomp` (a bad .env value and the application-registry takeover both need a submodule import), the wheel-size figure, and note that omitting an INCOMP concentration means 0 % solute; add a scalar-vs-vector performance note, the scan-and-filter limitation, and a getting-started notebook pointer. Upgrade the locked dependencies (polars 1.43.0, ruff 0.16.0, ty 0.0.63) and bump the version to 1.9.3. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 14 +- docs/usage.md | 12 +- encomp/coolprop/README.md | 4 +- encomp/coolprop/__init__.py | 59 +++ encomp/fluids.py | 59 +-- encomp/gases.py | 4 +- encomp/polars.py | 94 ++++- encomp/settings.py | 6 +- encomp/tests/test_coolprop_plugin.py | 16 +- encomp/tests/test_fluids.py | 10 +- encomp/tests/test_polars_units.py | 59 +++ encomp/tests/test_resolve_fluid_spec.py | 20 +- encomp/tests/test_units.py | 99 ++++- encomp/units.py | 83 +++- pyproject.toml | 2 +- uv.lock | 523 +++++++++++++----------- 16 files changed, 723 insertions(+), 341 deletions(-) diff --git a/README.md b/README.md index 7217e28..bc7edbf 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ pip install encomp `encomp` ships as a single per-platform wheel containing its dual-purpose native Rust artifact and one bundled CoolProp shared library, so supported platforms have nothing to build and do not install the Python `CoolProp` package. Wheels are provided for Windows (x86_64), Linux (x86_64 and arm64), and macOS (Apple Silicon only). PyPI does not publish an sdist; unsupported platforms, including Intel Macs, need a build from the git repository; see [Tests](#tests). -On macOS arm64, the native-only package reduces the resolver's combined wheel download from about 19.6 MB (`encomp` plus the separate Python CoolProp wheel) to about 9 MB for `encomp` alone—roughly a 54% reduction. The `encomp` artifact itself grows only slightly for the scalar bridge and bundled license; removing the separate CoolProp wheel is where the installation-size saving comes from. +On macOS arm64, the native-only package reduces the resolver's combined wheel download from about 19.6 MB (`encomp` plus the separate Python CoolProp wheel) to about 10 MB for `encomp` alone—roughly half. The `encomp` artifact itself grows only slightly for the scalar bridge and bundled license; removing the separate CoolProp wheel is where the installation-size saving comes from. ## The `Quantity` class @@ -67,6 +67,8 @@ Q([1, 2, 3], "bar") * 2 # [2.0 4.0 6.0] bar assert Q(0.1) == Q(10, "%") ``` +Give a quantity the whole column: a magnitude may be a `float`, a 1-D NumPy array, a Polars `Series` or a Polars `Expr`, and the dimensionality machinery is paid *per operation*, not per element. A scalar operation carries single-digit-microsecond overhead over raw pint arithmetic, which caps a Python loop over scalar quantities at roughly 10⁴–10⁵ ops/s; the same work on a vector magnitude amortizes to within ~20% of the underlying NumPy or Polars kernel at 10⁶ elements. Port scalar pint loops to array/`Series`/`Expr` magnitudes rather than iterating. + ### `Quantity` type system Each `Quantity` has a `Dimensionality` type parameter, determined at runtime from the unit. @@ -306,7 +308,7 @@ Raw arithmetic on a unit-typed column is refused because Polars cannot delegate The persisted Arrow field contract is `ARROW:extension:name = "encomp.unit"` and `ARROW:extension:metadata = `. Import `encomp.polars`—or `encomp.coolprop`, which registers the same dtype—before reading with Polars 1.x. For readers that do not import encomp, `POLARS_UNKNOWN_EXTENSION_TYPE_BEHAVIOR=load_as_extension` preserves generic extension metadata; otherwise current Polars 1.x warns and loads the numeric storage type. -The Polars extension API is marked unstable upstream, so this integration is experimental even though encomp pins its required behavior in tests. +The Polars extension API is marked unstable upstream, so this integration is experimental even though encomp pins its required behavior in tests. One upstream limitation is worth knowing: Polars' Parquet *statistics* reader has no implementation for extension dtypes, so a predicate that gets pushed into a `pl.scan_parquet(...)` on a unit-typed column aborts with a Rust panic. `QuantityFrame.scan_parquet` shields the scan (at the cost of row-group skipping on those columns), and eager `pl.read_parquet` is unaffected—filter through one of those rather than a bare lazy scan. ## The `Fluid` class @@ -416,6 +418,8 @@ df.select( # composition={species: mole fraction} dict, and a fixed phase via assume_phase='gas' ``` +The fluid name, the output property, the input pair and the input units are all validated where the expression is *built*, so a typo raises there rather than inside a plugin node at `collect()` time. What remains deferred is per-row evaluation: a state CoolProp cannot fix yields `null` for that row (`NaN` on the NumPy path), not an error. + ### Implementation Scalar calls reuse a bounded thread-local cache of native `AbstractState` handles; a handle is never shared between threads. Arrays use the batched C API (`AbstractState_update_and_1_out`), with handle-table locking confined to construction and destruction. The flash loop itself runs lock-free in C++, so independent chunks and property expressions can execute in parallel. @@ -472,14 +476,14 @@ The attributes of `encomp.settings.Settings` are overridden with a file named `. Attribute names are prefixed with `ENCOMP_`. Settings are loaded when `encomp.settings` is imported; for runtime changes to quantity and unit rendering, use `encomp.units.set_quantity_format()`. -Because the `.env` file is resolved relative to the current working directory, a stray `.env` that sets an invalid `ENCOMP_*` value (for example `ENCOMP_UNITS` pointing at a missing file) makes `import encomp` fail with a `pydantic.ValidationError`, even in an unrelated project. Remove or correct the offending value; unrelated keys in the `.env` are ignored. +Because the `.env` file is resolved relative to the current working directory, a stray `.env` that sets an invalid `ENCOMP_*` value (for example `ENCOMP_UNITS` pointing at a missing file) makes importing any `encomp` submodule (such as `encomp.units`) fail with a `pydantic.ValidationError`, even in an unrelated project. The bare `import encomp` still succeeds—it only exposes `__version__`—so the traceback points at the first real import. Remove or correct the offending value; unrelated keys in the `.env` are ignored. Importing `encomp.units` also registers a `typeguard` checker for `Quantity`, so `@typeguard.typechecked` and `encomp.misc.isinstance_types` compare dimensionality and magnitude type rather than falling back to a plain `isinstance`. -`import encomp` also installs `encomp.units.UNIT_REGISTRY` as pint's process-wide *application registry*. This is deliberate: every quantity in the process must come from that registry, or the dimensionality subclasses, the custom `[currency]` / `[normal]` dimensions and `on_redefinition="raise"` would silently not apply. The consequence is that another pint-based library in the same process gets encomp's registry (and its unit definitions, including the `Nm³` reinterpretation) after `import encomp`. Registry options that encomp pins — `force_ndarray`, `force_ndarray_like`, `autoconvert_offset_to_baseunit` — cannot be reassigned; a write that would change one is discarded and logs a warning. +Importing `encomp.units` (directly, or via any encomp submodule that uses it) also installs `encomp.units.UNIT_REGISTRY` as pint's process-wide *application registry*. This is deliberate: every quantity in the process must come from that registry, or the dimensionality subclasses, the custom `[currency]` / `[normal]` dimensions and `on_redefinition="raise"` would silently not apply. The consequence is that another pint-based library in the same process gets encomp's registry (and its unit definitions, including the `Nm³` reinterpretation) from that point on. Registry options that encomp pins — `force_ndarray`, `force_ndarray_like`, `autoconvert_offset_to_baseunit` — cannot be reassigned; a write that would change one is discarded and logs a warning. ## Documentation -The usage guide, example notebooks, and API reference are at [encomp.readthedocs.io](https://encomp.readthedocs.io). +The usage guide, example notebooks, and API reference are at [encomp.readthedocs.io](https://encomp.readthedocs.io). The [getting-started notebook](https://encomp.readthedocs.io/en/latest/notebooks/getting-started.html) is the shortest path from installation to working code. Release notes for each version are published as [GitHub Releases](https://github.com/wlaur/encomp/releases). diff --git a/docs/usage.md b/docs/usage.md index dfd54b0..489e735 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -72,12 +72,12 @@ mf = Q(25, ureg.kg / ureg.h) ``` :::{warning} -`import encomp` installs {py:data}`encomp.units.UNIT_REGISTRY` as `pint`'s process-wide -*application registry*. Every quantity in the process must come from it, or the -dimensionality subclasses, the custom `[currency]` / `[normal]` dimensions and -`on_redefinition="raise"` would silently not apply. Another `pint`-based library in the -same process therefore gets `encomp`'s registry (and its unit definitions) after the -import. The registry options `force_ndarray`, `force_ndarray_like` and +Importing `encomp.units` (directly, or via any `encomp` submodule that uses it) installs +{py:data}`encomp.units.UNIT_REGISTRY` as `pint`'s process-wide *application registry*. +Every quantity in the process must come from it, or the dimensionality subclasses, the +custom `[currency]` / `[normal]` dimensions and `on_redefinition="raise"` would silently +not apply. Another `pint`-based library in the same process therefore gets `encomp`'s +registry (and its unit definitions) from that point on. The registry options `force_ndarray`, `force_ndarray_like` and `autoconvert_offset_to_baseunit` are pinned: a write that would change one is discarded and logs a warning. ::: diff --git a/encomp/coolprop/README.md b/encomp/coolprop/README.md index 7c7d8b0..ab86f51 100644 --- a/encomp/coolprop/README.md +++ b/encomp/coolprop/README.md @@ -17,8 +17,8 @@ from encomp import coolprop as cp df = pl.DataFrame({"P": [50e5, 60e5], "T": [400.0, 450.0], "R": [0.4, 0.6]}) # Pa, K, - df.select( - cp.water("DMASS", "P", "T").alias("rho"), # IF97 water/steam - cp.water("HMASS", "P", "T").alias("h"), # runs in parallel + cp.water("DMASS", "P", "T").alias("rho"), # IF97 water/steam + cp.water("HMASS", "P", "T").alias("h"), # runs in parallel cp.humid_air("W", "P", "T", "R").alias("humidity_ratio"), ) ``` diff --git a/encomp/coolprop/__init__.py b/encomp/coolprop/__init__.py index 334e7ab..ceb093a 100644 --- a/encomp/coolprop/__init__.py +++ b/encomp/coolprop/__init__.py @@ -112,6 +112,8 @@ def handle_counts(self) -> tuple[int, int]: ... "lib_version", "resolve_fluid_spec", "self_check", + "validate_fluid_config", + "validate_fluid_name", "water", ] @@ -528,6 +530,51 @@ def _resolve_pair(name1: str, name2: str) -> tuple[int, bool]: return _native().resolve_input_pair(name1, name2) +@cache +def validate_fluid_config(backend: Backend | str, fluids: str, fractions: tuple[float, ...] | None) -> None: + """Raise unless CoolProp can construct an ``AbstractState`` for a resolved fluid spec. + + Takes the ``(backend, fluids, fractions)`` triple :func:`resolve_fluid_spec` produces; + :func:`validate_fluid_name` is the name-level entry point most callers want. + + ``backend`` is the ``AbstractState`` backend (``"HEOS"``, ``"IF97"``, ``"INCOMP"``, + ``"BICUBIC&HEOS"``, ...) and ``fluids`` the ``&``-joined species (``"Water"``, + ``"CO2&O2"``). The backend is annotated as the :data:`Backend` Literal widened with + ``str``, like :data:`CName`: the Literal is what editors suggest, but CoolProp -- not + encomp -- is the authority on the name, so an unlisted backend must still pass. + + Cached per triple, so the 1000th ``Fluid("Water", ...)`` pays a dict lookup rather than + another CoolProp initialization. Only *resolved* names are remembered: + ``functools.cache`` stores return values, never exceptions, so an invalid name (or a + transient CoolProp error) is re-checked on the next call instead of being cached as + "this fluid does not exist". Use ``validate_fluid_config.cache_clear()`` to drop it. + """ + # Constructing the AbstractState is the only reliable way to ask CoolProp whether a + # name resolves (it covers pure fluids, INCOMP, and mixtures alike). It costs tens of + # microseconds for the usual backends -- and seconds the very first time a tabular + # backend builds its tables, a cost that would otherwise be paid at the first property + # access anyway. + _native().validate_fluid(backend, fluids, None if fractions is None else list(fractions)) + + +def validate_fluid_name(name: CName, composition: Composition | None = None) -> None: + """Raise ``ValueError`` unless ``name`` (+ ``composition``) is a CoolProp fluid. + + The single eager fluid-name check, shared by :class:`encomp.fluids.Fluid` and the + expression builders here: a misspelled name must fail where it is written, not at + ``collect()`` time inside a plugin node. Results are cached by + :func:`validate_fluid_config`. + """ + backend, fluids, fractions = resolve_fluid_spec(name, composition) + + try: + validate_fluid_config(backend, fluids, None if fractions is None else tuple(fractions)) + except Exception as e: + raise ValueError( + f"Fluid '{name}' could not be initialized, ensure that the name is a valid CoolProp fluid name" + ) from e + + @cache def _parameter_index(name: str) -> int: return _native().parameter_index(name) @@ -768,6 +815,17 @@ def fluid( convert first (e.g. ``encomp.polars.quantities`` + ``.to(...)``), or pass raw SI values via ``.ext.storage()``. """ + # The plugin resolves the output name and the fluid inside the query engine, so a typo + # in either would only surface at collect() time, in CoolProp's words and with the + # plugin .so path in the expression dump. Both checks are cheap and cached, and + # encomp.fluids validates eagerly, so do the same here. + try: + _parameter_index(output) + except ValueError as e: + raise ValueError( + f"fluid output must be a CoolProp property name (DMASS, HMASS, T, ...); got {output!r}." + ) from e + name1, name2 = _input_name(input1), _input_name(input2) for nm in (name1, name2): if not is_fluid_input(nm): @@ -784,6 +842,7 @@ def fluid( a, b = (input2, input1) if swap else (input1, input2) # canonical order name_a, name_b = (name2, name1) if swap else (name1, name2) a_expr, b_expr = _as_expr(a), _as_expr(b) + validate_fluid_name(name, composition) backend, fluids, fractions = resolve_fluid_spec(name, composition) phase = _phase_from_assumed(assume_phase) if assume_phase is not None else None diff --git a/encomp/fluids.py b/encomp/fluids.py index 8369c38..2d2db1a 100644 --- a/encomp/fluids.py +++ b/encomp/fluids.py @@ -5,7 +5,6 @@ from abc import ABC, abstractmethod from collections import OrderedDict from collections.abc import Mapping -from functools import cache from threading import Lock from typing import Annotated, Any, ClassVar, Generic, Literal, Self, TypedDict, Unpack, cast @@ -23,11 +22,11 @@ HumidAirParam, _fluid_scalar, # pyright: ignore[reportPrivateUsage] _humid_air_scalar, # pyright: ignore[reportPrivateUsage] - _native, # pyright: ignore[reportPrivateUsage] _resolve_pair, # pyright: ignore[reportPrivateUsage] is_fluid_param, is_humid_air_param, resolve_fluid_spec, + validate_fluid_name, ) from .settings import SETTINGS from .structures import flatten @@ -98,7 +97,11 @@ "Variable", "N/A", ] -BackendKind = Literal["fluid", "humid_air"] +# Which native CoolProp interface evaluates a property: AbstractState (fluid) or +# HAPropsSI (humid air). NOT a CoolProp *backend* -- that word is reserved for the +# equation of state in a fluid name ("HEOS", "IF97", "INCOMP", ...), and encomp has +# exactly one evaluation implementation (the bundled library) to choose between +NativeInterface = Literal["fluid", "humid_air"] class FluidState(TypedDict, Generic[MT], total=False): # noqa: UP046 @@ -215,32 +218,6 @@ def clear_expr_evaluation_cache() -> None: _EXPR_EVALUATION_CACHE.clear() -@cache -def _resolve_fluid_name(backend: str, fluids: str, fractions: tuple[float, ...] | None) -> None: - # Constructing the AbstractState is the only reliable way to ask CoolProp whether a - # name resolves (it covers pure fluids, INCOMP, and mixtures alike). It costs tens of - # microseconds for the usual backends -- and seconds the very first time a tabular - # backend builds its tables, a cost that would otherwise be paid at the first property - # access anyway. Cached per (backend, fluids), so the 1000th Fluid("Water", ...) pays - # a dict lookup rather than another CoolProp initialization. - # - # functools.cache stores return values, never exceptions, so only a resolved name is - # remembered: a failure (an invalid name, or a transient CoolProp error) is re-checked - # on the next call instead of being cached as "this fluid does not exist". - _native().validate_fluid(backend, fluids, None if fractions is None else list(fractions)) - - -def _validate_fluid_name(name: CName, composition: Composition | None = None) -> None: - backend, fluids, fractions = resolve_fluid_spec(name, composition) - - try: - _resolve_fluid_name(backend, fluids, None if fractions is None else tuple(fractions)) - except Exception as e: - raise ValueError( - f"Fluid '{name}' could not be initialized, ensure that the name is a valid CoolProp fluid name" - ) from e - - def _expr_cache_digest(expr: pl.Expr) -> str: serialized = expr.meta.undo_aliases().meta.serialize(format="json") return hashlib.blake2s(serialized.encode(), digest_size=16).hexdigest() @@ -264,7 +241,7 @@ def _get_expr_evaluation_cache_key( fluid_any._assumed_phase, comp_key, output, - type(fluid).BACKEND_KIND, + type(fluid).NATIVE_INTERFACE, tuple((prop, _expr_cache_digest(expr)) for prop, expr in points), ) @@ -290,7 +267,7 @@ class CoolPropFluid(ABC, Generic[MT]): # noqa: UP046 name: CName points: list[tuple[CProperty, Quantity[Any, MT] | Quantity[Any, float]]] - BACKEND_KIND: ClassVar[BackendKind] = "fluid" + NATIVE_INTERFACE: ClassVar[NativeInterface] = "fluid" # Assumed phase passed to both native interfaces. None means CoolProp determines # the phase; a value skips the expensive mixture phase-stability search. @@ -741,7 +718,7 @@ def _plugin_expr(self, output: CProperty, points: tuple[tuple[str, pl.Expr], ... ) names = [p[0] for p in points] exprs = [p[1] for p in points] - if self.BACKEND_KIND == "humid_air": + if self.NATIVE_INTERFACE == "humid_air": n1, n2, n3 = names # the plugin reads each input's property from its (aliased) name expr = _cprust.humid_air( @@ -801,12 +778,12 @@ def evaluate_single(self, output: CProperty, *points: tuple[CProperty, float]) - # Pair resolution is request validation, not a failed state evaluation. Do it # outside the try/NaN normalization so all magnitude containers reject an # unsupported pair consistently. - if self.BACKEND_KIND != "humid_air": + if self.NATIVE_INTERFACE != "humid_air": (name1, _), (name2, _) = points _resolve_pair(name1, name2) try: - if self.BACKEND_KIND == "humid_air": + if self.NATIVE_INTERFACE == "humid_air": (name1, value1), (name2, value2), (name3, value3) = points value = _humid_air_scalar(output, name1, value1, name2, value2, name3, value3) else: @@ -926,11 +903,11 @@ def construct_quantity( qty.ito(ret_unit) if convert_magnitude: - qty = qty.astype(self._mt) # ty: ignore[no-matching-overload] + qty = qty.astype(self._mt) if isinstance(qty.m, pl.Series): # missing values surface as null, never NaN (the library's single sentinel) - qty.m = qty.m.fill_nan(None).cast(self._eager_series_output_dtype()) + qty.m = qty.m.fill_nan(None).cast(self._eager_series_output_dtype()) # ty: ignore[invalid-assignment] return cast("Quantity[Any, MT]", qty) @@ -1081,6 +1058,12 @@ def __init__( Name of the fluid. With ``composition`` this is the backend only (e.g. ``"HEOS"``); otherwise the full CoolProp name, optionally with fixed fractions (e.g. ``"HEOS::CO2[0.5]&O2[0.5]"``). + + For an incompressible solution the concentration is part of the name and is + **not** optional in practice: CoolProp reads a bare ``"INCOMP::MEG"`` as a + 0 % solution, i.e. pure water, without warning (996 kg/m³ at 25 °C, and NaN + at -20 °C where the intended glycol mixture is still liquid). Spell it out: + ``"INCOMP::MEG[0.5]"``. composition : Composition | None Fixed mixture composition as ``{species: mole fraction}`` (like CoolProp's ``"HEOS::CO2[0.5]&O2[0.5]"`` name syntax). Mole fractions must sum to 1. @@ -1104,7 +1087,7 @@ def __init__( else: self.name = name - _validate_fluid_name(self.name, self._composition) + validate_fluid_name(self.name, self._composition) points = self._build_points(kwargs) @@ -1428,7 +1411,7 @@ class HumidAir(CoolPropFluid[MT]): viscosity. This differs from ``Fluid.M``, which returns molar mass. """ - BACKEND_KIND: ClassVar[BackendKind] = "humid_air" + NATIVE_INTERFACE: ClassVar[NativeInterface] = "humid_air" STATE_INPUTS: ClassVar[frozenset[str]] = frozenset(HUMID_AIR_INPUTS) # CoolProp humid-air property names, units, and descriptions diff --git a/encomp/gases.py b/encomp/gases.py index 0db8ab8..80fe922 100644 --- a/encomp/gases.py +++ b/encomp/gases.py @@ -77,7 +77,7 @@ def _n_s_condition(condition: Literal["N", "S"]) -> GasCondition: def _resolve_gas_condition(condition: object, name: str) -> GasCondition: if isinstance(condition, str): if condition in ("N", "S"): - return _n_s_condition(condition) # ty: ignore[invalid-argument-type] + return _n_s_condition(condition) raise ValueError(f"{name} must be 'N', 'S', or a (pressure, temperature) tuple, got {condition!r}") @@ -138,7 +138,7 @@ def ideal_gas_density( # directly from ideal gas law # override the inferred type here since it's sure to be Density - rho = (P * M) / (CONSTANTS.R * T.to("K").unknown()) # ty: ignore[unsupported-operator] + rho = (P * M) / (CONSTANTS.R * T.to("K").unknown()) return rho.to("kg/m³").asdim(Density) diff --git a/encomp/polars.py b/encomp/polars.py index d0500a2..c7f3095 100644 --- a/encomp/polars.py +++ b/encomp/polars.py @@ -25,15 +25,17 @@ from __future__ import annotations +import warnings from collections.abc import Mapping from pathlib import Path from typing import Any, ClassVar, Self, cast, overload import polars as pl +from pint.errors import UndefinedUnitError from . import utypes as _ut from ._polars_dtype import EXTENSION_NAME, UnitDType -from .units import DimensionalityTypeError, Quantity, Unit +from .units import DimensionalityError, DimensionalityTypeError, Quantity, Unit from .utypes import Dimensionality, UnknownDimensionality __all__ = [ @@ -321,6 +323,22 @@ def unit( return Column(probe.u, dimensionality, name=name, _explicit_dimensionality=asdim is not None) +def _shield_extension_pushdown(lf: pl.LazyFrame) -> pl.LazyFrame: + """Re-wrap every unit-typed column so predicates cannot be pushed into the scan. + + Polars' Parquet statistics reader has no implementation for extension dtypes and + aborts the process thread with a Rust ``PanicException`` when a pushed-down predicate + makes it decode statistics for one. The re-wrap is a metadata-only identity + (``storage`` -> same dtype) that the optimizer will not push a predicate through. + """ + unit_columns = [(name, dtype) for name, dtype in lf.collect_schema().items() if isinstance(dtype, UnitDType)] + + if not unit_columns: + return lf + + return lf.with_columns(pl.col(name).ext.storage().ext.to(dtype).alias(name) for name, dtype in unit_columns) + + class QuantityFrame: """Validated, lazy Polars frame with typed quantity-column descriptors.""" @@ -371,7 +389,14 @@ def __init__(self, frame: pl.DataFrame | pl.LazyFrame) -> None: # meaning. Pint permits scale-only conversions such as delta_degC -> K, # so compare semantic dimensionality after converting as well. source = Quantity(pl.col(declaration.name).ext.storage(), dtype.unit) - converted_quantity = source.to(declaration.unit) + try: + converted_quantity = source.to(declaration.unit) + except DimensionalityError as e: + # pint's own message names the two units but never the column, which is + # the one thing the caller needs to act on in a wide frame + raise DimensionalityTypeError( + f"Cannot read column {declaration.name!r} carrying {dtype.unit} as {declaration.unit}: {e}" + ) from e if converted_quantity.dt is not declaration.dimensionality: if not declaration.dimensionality_is_explicit: raise DimensionalityTypeError( @@ -388,6 +413,17 @@ def __init__(self, frame: pl.DataFrame | pl.LazyFrame) -> None: self.lf = lf.with_columns(conversions) + def __repr__(self) -> str: + # a summary that never collects the frame: the declared columns and their units, + # which is what a notebook user evaluating the object wants to see + declarations: list[str] = [] + + for declaration in self._unit_columns.values(): + unit_repr = f"{declaration.unit:~P}" or "dimensionless" + declarations.append(f"{declaration.name} [{unit_repr}]") + + return f"<{type(self).__name__}: {', '.join(declarations) or 'no declared columns'} (lazy)>" + @classmethod def from_untyped(cls, frame: pl.DataFrame | pl.LazyFrame) -> Self: """Assign the declared units to bare numeric columns, then validate.""" @@ -405,8 +441,17 @@ def from_untyped(cls, frame: pl.DataFrame | pl.LazyFrame) -> Self: @classmethod def scan_parquet(cls, source: str | Path) -> Self: - """Lazily scan a Parquet file and validate its persisted unit schema.""" - return cls(pl.scan_parquet(source)) + """Lazily scan a Parquet file and validate its persisted unit schema. + + A predicate on a unit-typed column is deliberately *not* pushed into the Parquet + scan: polars' Parquet statistics reader panics on any extension dtype + (``polars-parquet`` ``statistics.rs``, an upstream limitation, still present in + 1.43). An identity re-wrap of every unit-typed column shields the scan, at the + cost of row-group skipping for those columns. Filtering a bare + ``pl.scan_parquet(...)`` on a unit-typed column is what triggers the panic; go + through this method, or ``pl.read_parquet``, instead. + """ + return cls(_shield_extension_pushdown(pl.scan_parquet(source))) @classmethod def derive( @@ -444,8 +489,31 @@ def units_of(frame: pl.DataFrame | pl.LazyFrame) -> dict[str, Unit[Any]]: Reads only schema metadata: no data pass, and a ``LazyFrame`` (e.g. a ``pl.scan_parquet``) is not collected. Columns without a unit dtype are omitted. + + ``EXTENSION_NAME`` is a cross-process contract, so a file written by another producer + can carry unit metadata Pint cannot parse. Such a column is omitted and reported in a + ``UserWarning`` naming it, rather than making the whole frame uninspectable. """ - return {name: dtype.unit for name, dtype in frame.collect_schema().items() if isinstance(dtype, UnitDType)} + units: dict[str, Unit[Any]] = {} + unparseable: list[str] = [] + + for name, dtype in frame.collect_schema().items(): + if not isinstance(dtype, UnitDType): + continue + + try: + units[name] = dtype.unit + except UndefinedUnitError: + unparseable.append(f"{name!r} ({dtype.ext_metadata()!r})") + + if unparseable: + warnings.warn( + f"ignoring {EXTENSION_NAME!r} columns whose unit metadata is not a known unit: {', '.join(unparseable)}", + UserWarning, + stacklevel=2, + ) + + return units @overload @@ -468,6 +536,16 @@ def with_units(frame: pl.DataFrame | pl.LazyFrame, units: Mapping[str, str | Uni missing = sorted(set(units) - set(schema)) if missing: raise ValueError(f"unit schema keys are not columns of the frame: {missing}") - return frame.with_columns( - pl.col(name).ext.to(UnitDType(unit, storage=schema[name])) for name, unit in units.items() - ) + + dtypes: dict[str, UnitDType] = {} + for name, unit in units.items(): + try: + dtypes[name] = UnitDType(unit, storage=schema[name]) + except (TypeError, UndefinedUnitError) as e: + # the dtype constructor knows the bad storage dtype or unit string, but not + # which column it came from -- which is what the caller has to fix. A note + # adds that without rewriting an exception that renders its own message + e.add_note(f"offending unit schema entry: {name!r}: {unit!r}") + raise + + return frame.with_columns(pl.col(name).ext.to(dtype) for name, dtype in dtypes.items()) diff --git a/encomp/settings.py b/encomp/settings.py index 2de7538..10a123b 100644 --- a/encomp/settings.py +++ b/encomp/settings.py @@ -39,8 +39,10 @@ class Settings(BaseSettings): Because the ``.env``-file is resolved relative to the *current working directory*, a stray ``.env`` containing an invalid ``ENCOMP_*`` value - (e.g. ``ENCOMP_UNITS`` pointing to a missing file) makes ``import encomp`` - fail with a ``ValidationError`` -- even in an unrelated project. + (e.g. ``ENCOMP_UNITS`` pointing to a missing file) makes importing any + ``encomp`` submodule (e.g. ``encomp.units``) fail with a ``ValidationError`` + -- even in an unrelated project. The bare ``import encomp`` succeeds: the + top-level package exposes only ``__version__``. * ``UNITS``: path to a file with unit definitions for ``pint`` * ``TYPESET_SYMBOL_SCRIPTS``: whether to typeset SymPy symbol sub- and superscripts diff --git a/encomp/tests/test_coolprop_plugin.py b/encomp/tests/test_coolprop_plugin.py index 6a51f55..80a8271 100644 --- a/encomp/tests/test_coolprop_plugin.py +++ b/encomp/tests/test_coolprop_plugin.py @@ -342,13 +342,21 @@ def test_invalid_input_pair_raises_before_plugin_execution() -> None: def test_invalid_runtime_names_raise_python_exceptions() -> None: - df = pl.DataFrame({"P": [101325.0], "T": [300.0]}) + # a bad output property or fluid name is rejected where the expression is BUILT, not + # at collect() time from inside a plugin node (which reports CoolProp's own words plus + # the plugin .so path in an expression dump) + with pytest.raises(ValueError, match="must be a CoolProp property name"): + cp.water(cast(Any, "NOT_A_PROPERTY"), "P", "T") - with pytest.raises(Exception, match="unknown parameter"): - df.select(cp.water(cast(Any, "NOT_A_PROPERTY"), "P", "T")) + with pytest.raises(ValueError, match="could not be initialized"): + cp.fluid("DMASS", "P", "T", name=cast(Any, "NotAFluid")) with pytest.raises(Exception, match="interior NUL"): - df.select(cp.fluid("DMASS", "P", "T", name=cast(Any, "Water\0bad"))) + cp.fluid("DMASS", "P", "T", name=cast(Any, "Water\0bad")) + + # a valid name and output still build a plan and evaluate + df = pl.DataFrame({"P": [101325.0], "T": [300.0]}) + assert df.select(cp.water("DMASS", "P", "T")).item() == pytest.approx(996.5, abs=1.0) def test_duplicate_state_inputs_raise() -> None: diff --git a/encomp/tests/test_fluids.py b/encomp/tests/test_fluids.py index 126d684..8f18aab 100644 --- a/encomp/tests/test_fluids.py +++ b/encomp/tests/test_fluids.py @@ -11,6 +11,7 @@ from encomp import coolprop as encomp_coolprop from .. import utypes as ut +from ..coolprop import validate_fluid_config from ..fluids import ( CoolPropFluid, CProperty, @@ -19,7 +20,6 @@ HumidAir, HumidAirState, Water, - _resolve_fluid_name, clear_expr_evaluation_cache, ) from ..settings import SETTINGS @@ -330,25 +330,25 @@ def test_invalid_fluid_name_rejected_at_construction() -> None: def test_fluid_name_validation_is_cached() -> None: # a valid name is resolved by CoolProp once; every later construction is a cache hit - _resolve_fluid_name.cache_clear() + validate_fluid_config.cache_clear() for _ in range(50): Fluid("Water", P=Q(2, "bar"), T=Q(25, "degC")) - info = _resolve_fluid_name.cache_info() + info = validate_fluid_config.cache_info() assert info.misses == 1 assert info.hits == 49 # an invalid name is NOT cached: functools.cache stores return values, not exceptions, # so a transient CoolProp failure can never be remembered as "this fluid is invalid" - _resolve_fluid_name.cache_clear() + validate_fluid_config.cache_clear() for _ in range(3): with pytest.raises(ValueError, match="could not be initialized"): Fluid("Watr", P=Q(2, "bar"), T=Q(25, "degC")) - assert _resolve_fluid_name.cache_info().currsize == 0 + assert validate_fluid_config.cache_info().currsize == 0 def test_invalid_fluid_state_repr_does_not_raise() -> None: diff --git a/encomp/tests/test_polars_units.py b/encomp/tests/test_polars_units.py index dec9007..75998fa 100644 --- a/encomp/tests/test_polars_units.py +++ b/encomp/tests/test_polars_units.py @@ -437,6 +437,65 @@ def test_quantity_frame_derive_validates_assignments() -> None: Report.power.assign(cast(Any, sensors.pressure)) +def test_scan_parquet_survives_predicate_pushdown(tmp_path: Path) -> None: + # polars' parquet statistics reader has no implementation for extension dtypes and + # aborts a worker thread with a Rust panic when a pushed-down predicate makes it decode + # them (upstream; reproduced on 1.42 and 1.43). QuantityFrame.scan_parquet inserts an + # identity re-wrap that the optimizer will not push a predicate through + path = tmp_path / "sensors.parquet" + frame = pl.DataFrame({"pressure": [10.0, 60.0, 100.0], "Volume flow": [1.0, 2.0, 3.0]}) + Sensors.from_untyped(frame).lf.sink_parquet(path) + + scanned = Sensors.scan_parquet(path) + filtered = scanned.lf.filter(pl.col("pressure").ext.storage() > 50.0).collect() + + assert filtered["pressure"].ext.storage().to_list() == [60.0, 100.0] + assert filtered.schema["pressure"] == UnitDType("bar") + + # is_null() decodes statistics too, and a non-declared unit column is shielded as well + assert scanned.lf.filter(pl.col("Volume flow").ext.storage().is_null()).collect().height == 0 + + # pin the upstream bug itself: when it is fixed, this fails and the shield (and the + # note in QuantityFrame.scan_parquet) can be revisited + with raises(BaseException, match=r"not yet implemented|statistics"): + pl.scan_parquet(path).filter(pl.col("pressure").ext.storage() > 50.0).collect() + + +def test_units_of_degrades_per_column_for_unparseable_metadata() -> None: + # EXTENSION_NAME is a cross-process contract: another producer can write metadata pint + # cannot parse. That must not make the well-formed columns of the frame uninspectable + good = with_units(pl.DataFrame({"good": [1.0]}), {"good": "bar"}) + bad_dtype = pl.BaseExtension(EXTENSION_NAME, pl.Float64(), "totally_bogus") + frame = good.with_columns(pl.Series("bad", [2.0]).cast(pl.Float64).ext.to(bad_dtype)) + + with pytest.warns(UserWarning, match="totally_bogus"): + assert units_of(frame) == {"good": Unit("bar")} + + +def test_typed_read_errors_name_the_column() -> None: + stored = with_units(pl.DataFrame({"pressure": [300.0], "Volume flow": [1.0]}), {"pressure": "degC"}) + + with raises(DimensionalityTypeError, match="column 'pressure'"): + Sensors(stored) + + with raises(TypeError, match="float or integer") as excinfo: + with_units(pl.DataFrame({"tag": ["a"]}), {"tag": "bar"}) + + assert any("'tag'" in note for note in excinfo.value.__notes__) + + with raises(UndefinedUnitError) as unit_excinfo: + with_units(pl.DataFrame({"P": [1.0]}), {"P": "bogus_unit"}) + + assert any("'P'" in note for note in unit_excinfo.value.__notes__) + + +def test_quantity_frame_repr_summarizes_declarations() -> None: + sensors = Sensors.from_untyped(pl.DataFrame({"pressure": [1.0], "Volume flow": [10.0]})) + + # a collection-free summary: the repr of a lazy frame must not evaluate it + assert repr(sensors) == "" + + def test_quantity_frame_round_trips_fluid_expression_output(tmp_path: Path) -> None: source_path = tmp_path / "states.parquet" result_path = tmp_path / "properties.parquet" diff --git a/encomp/tests/test_resolve_fluid_spec.py b/encomp/tests/test_resolve_fluid_spec.py index 8e09b99..e2b4d47 100644 --- a/encomp/tests/test_resolve_fluid_spec.py +++ b/encomp/tests/test_resolve_fluid_spec.py @@ -5,7 +5,7 @@ and ``encomp.coolprop.fluid`` delegate to it. """ -from typing import cast +from typing import Any, cast import pytest @@ -135,3 +135,21 @@ def test_is_humid_air_input() -> None: def test_phase_ignoring_backends() -> None: assert "IF97" in cp.PHASE_IGNORING_BACKENDS assert "HEOS" not in cp.PHASE_IGNORING_BACKENDS + + +def test_fluid_expression_validates_the_name_and_output_eagerly() -> None: + # a typo in either must fail where it is written; deferring it to collect() surfaces + # CoolProp's own words plus the plugin .so path in an expression dump + with pytest.raises(ValueError, match="could not be initialized"): + cp.fluid("DMASS", "P", "T", name=cast(Any, "Watr")) + + with pytest.raises(ValueError, match="must be a CoolProp property name"): + cp.water(cast(Any, "DMAS"), "P", "T") + + with pytest.raises(ValueError, match="could not be initialized"): + cp.validate_fluid_name(cast(Any, "HEOS::NotAFluid")) + + # valid names and outputs build a plan without touching the data + assert cp.fluid("DMASS", "P", "T", name="IF97::Water").meta.output_name() == "DMASS" + cp.validate_fluid_name("Water") + cp.validate_fluid_name("INCOMP::MEG[0.5]") diff --git a/encomp/tests/test_units.py b/encomp/tests/test_units.py index 9d88764..d2eb481 100644 --- a/encomp/tests/test_units.py +++ b/encomp/tests/test_units.py @@ -1332,8 +1332,9 @@ def test_indexing() -> None: def test_round() -> None: - # TODO: should this even work? - # type numpy.ndarray doesn't define __round__ method + # numpy does not define __round__ on ndarray and polars spells rounding as a method, + # so every container is routed explicitly -- round() must work identically for all of + # them, like abs() and the arithmetic operators do q = Q(25.12312312312, "kg/s") @@ -1348,8 +1349,21 @@ def test_round() -> None: assert q_r2.m[0] == 25.1 assert q_r2.m[1] == 25.1 - with pytest.raises(TypeError, match="round"): - round(Q([25.12312312312], "kg/s").astype(pl.Series), 1) + q_series = round(Q([25.12312312312], "kg/s").astype(pl.Series), 1) + + assert isinstance(q_series.m, pl.Series) + assert q_series.m.to_list() == [25.1] + assert q_series.u == Unit("kg/s") + + q_expr = round(Q(25.12312312312, "kg/s").astype(pl.Expr), 1) + + assert isinstance(q_expr.m, pl.Expr) + assert pl.select(q_expr.m).item() == 25.1 + + # round() with no digits, for every container + assert round(Q(25.6, "kg/s")).m == 26.0 + assert round(Q([25.6], "kg/s")).m[0] == 26.0 + assert round(Q([25.6], "kg/s").astype(pl.Series)).m.to_list() == [26.0] def test_abs() -> None: @@ -1457,9 +1471,44 @@ class Model(BaseModel): def test_float_cast() -> None: - assert isinstance(Q([False, False]).m[0], float) + # non-float numeric sequences normalize to a float64 magnitude + assert isinstance(Q([1, 2]).m[0], float) + assert Q([1, 2]).m.dtype == np.float64 + assert Q(np.array([1, 2], dtype=np.int8)).m.dtype == np.float64 + assert Q(np.array([1.5], dtype=np.float32)).m.dtype == np.float64 + + +def test_non_numeric_array_magnitudes_are_rejected() -> None: + # a bool magnitude is always a mistake (typically a comparison mask fed back in as + # data), and every other non-numeric dtype numpy would silently coerce is wrong data: + # a complex array loses its imaginary part, a datetime64 becomes its raw tick count. + # the pl.Series magnitude path refuses exactly the same set + for values in ( + [False, True], + np.array([False, True]), + np.array([True], dtype=object), + ): + with pytest.raises(TypeError, match="not a bool"): + Q(cast(Any, values), "m") + + for array in ( + np.array([1 + 2j, 3 + 4j]), + np.array([1, 2], dtype="datetime64[s]"), + np.array([1, 2], dtype="timedelta64[s]"), + ): + with pytest.raises(TypeError, match="must contain real numbers"): + Q(cast(Any, array), "m") + + with pytest.raises(TypeError, match="must contain real numbers"): + Q(cast(Any, [1 + 2j]), "m") + + # the in-place magnitude setter validates identically + qty = Q(np.array([1.0]), "m") - assert (Q([False, True]) == Q(np.array([False, True]))).all() + with pytest.raises(TypeError, match="not a bool"): + qty.m = cast(Any, np.array([True])) + + assert qty.m.tolist() == [1.0] def test_temperature_difference() -> None: @@ -2214,6 +2263,44 @@ def test_floordiv_is_consistent_across_magnitude_containers() -> None: assert np.signbit(negative_infinity.m) +def test_division_by_zero_follows_the_container() -> None: + # this is the one arithmetic divergence between the containers, and it is deliberate: + # a float magnitude follows Python (dividing a float by zero raises, and a Quantity + # must not swallow that), while the vector containers follow IEEE elementwise, which + # is what their kernels do and what makes a per-row zero not abort a whole column + with pytest.raises(ZeroDivisionError): + _ = Q(1.0, "m") / Q(0.0, "s") + + with pytest.raises(ZeroDivisionError): + _ = Q(1.0, "m") // Q(0.0, "m") + + with np.errstate(divide="ignore", invalid="ignore"): + assert np.isinf((Q(np.array([1.0]), "m") // Q(np.array([0.0]), "m")).m).all() + + with np.errstate(divide="ignore", invalid="ignore"): + assert np.isinf((Q(np.array([1.0]), "m") / Q(np.array([0.0]), "s")).m).all() + assert np.isnan((Q(np.array([0.0]), "m") / Q(np.array([0.0]), "s")).m).all() + + series = Q(pl.Series([1.0, 0.0]), "m") / Q(pl.Series([0.0, 0.0]), "s") + assert series.m.to_list() == [float("inf"), pytest.approx(float("nan"), nan_ok=True)] + + expression = Q(pl.lit(1.0), "m") / Q(pl.lit(0.0), "s") + assert pl.select(expression.m).item() == float("inf") + + +def test_ndim_reports_one_for_every_vector_container() -> None: + assert Q(1.0, "m").ndim == 0 + assert Q(np.array([1.0, 2.0]), "m").ndim == 1 + + # neither polars type has an ndim attribute; a Series/Expr magnitude is still a vector + series = Q(pl.Series([1.0, 2.0]), "m") + assert series.ndim == 1 + assert not series.is_scalar + assert len(series) == 2 + + assert Q(pl.col("x"), "m").ndim == 1 + + def test_mixed_container_arithmetic_is_rejected() -> None: array = cast(Any, Q(np.array([1.0, 2.0]), "m")) series = cast(Any, Q(pl.Series([1.0, 2.0]), "m")) diff --git a/encomp/units.py b/encomp/units.py index 0581ec9..870603b 100644 --- a/encomp/units.py +++ b/encomp/units.py @@ -328,8 +328,9 @@ def __init(self) -> None: # pyright: ignore[reportUnusedFunction] # invoked by # Quantity in the process must come from encomp's registry, or the dimensionality # subclasses, the custom dimensions (currency, normal) and on_redefinition="raise" would # silently not apply. The trade-off is documented (README "Settings"): another pint-based -# library in the same process sees encomp's registry after `import encomp`. This is an -# intentional, settled design decision -- not an oversight to be re-flagged. +# library in the same process sees encomp's registry once this module is imported (the +# top-level `encomp` package exposes only __version__ and does NOT trigger this). This is +# an intentional, settled design decision -- not an oversight to be re-flagged. setattr(pint, "_DEFAULT_REGISTRY", UNIT_REGISTRY) # noqa: B010 cast(Any, pint.application_registry).set(UNIT_REGISTRY) @@ -973,17 +974,36 @@ def __reduce__( # pyright: ignore[reportIncompatibleMethodOverride] # pyrefly: @staticmethod def _cast_array_float(inp: np.ndarray) -> Numpy1DArray: - # don't fail in case the array contains unsupported objects, - # cast to float64, matches the Numpy1DArray type definition + # cast to float64, matching the Numpy1DArray type definition. The accepted dtype + # kinds are allow-listed rather than reject-listed, because the cast below is + # deliberately unsafe (float16 / int64 / ... must be accepted) and every other kind + # numpy can force into a float is silently WRONG data: a bool mask becomes 1.0/0.0, + # a complex array loses its imaginary part, and a datetime64/timedelta64 becomes its + # raw tick count. The pl.Series branch of _validate_magnitude refuses exactly the + # same set, so both vector containers agree on what a magnitude is + if inp.dtype.kind == "b": + # same message as the scalar check: a bool magnitude (typically a comparison + # mask fed back in as data) is always a mistake + raise TypeError("magnitude must be a real number, not a bool") + if inp.dtype.kind in {"S", "U"}: raise ValueError("magnitude sequences must contain real numbers, not strings") if inp.dtype.kind == "O": for item in inp: + # bool is an int subclass and passes numbers.Real, so it needs its own check + if isinstance(item, bool): + raise TypeError("magnitude must be a real number, not a bool") + if item is None or isinstance(item, (str, bytes)) or not isinstance(item, numbers.Real): raise ValueError( f"magnitude sequences must contain real numbers; got {type(item).__name__}: {item!r}" ) + elif inp.dtype.kind not in {"f", "i", "u"}: + raise TypeError( + f"magnitude sequences must contain real numbers, got dtype {inp.dtype!r}. " + "Complex, datetime, timedelta and other non-numeric arrays are not valid magnitudes" + ) if inp.dtype == np.float64: return cast("Numpy1DArray", inp) @@ -1986,7 +2006,11 @@ def check_compatibility(self, other: Quantity[Any, Any] | float) -> None: """ if not isinstance(other, Quantity): - if not self.dimensionless: + # self.u.dimensionless, not self.dimensionless: pint's quantity-level property + # converts the whole magnitude to root units (an O(n) pass and a temporary on + # every q + 1.0 / q == 5.0 with a vector magnitude), while the unit-level check + # is magnitude-independent and answers identically for every registered unit + if not self.u.dimensionless: raise DimensionalityTypeError( f"Value {other} ({type(other)}) is not compatible with dimensional quantity {self} ({type(self)})" ) @@ -2021,7 +2045,7 @@ def check_compatibility(self, other: Quantity[Any, Any] | float) -> None: ) raise DimensionalityTypeError( - f"Quantities with different dimensionalities are not compatible: {type(self)} and {type(other)}. " + f"Quantities with different dimensionalities are not compatible: {type(self)} and {type(other)}." ) def is_compatible_with( @@ -2075,12 +2099,19 @@ def __round__(self, ndigits: int | None = None) -> Quantity[DT, MT]: if ndigits is None: ndigits = 0 - if isinstance(self.m, float): + m = self.m + + if isinstance(m, float): return cast("Quantity[DT, MT]", super().__round__(ndigits)) - elif isinstance(self.m, np.ndarray): - return cast("Quantity[DT, MT]", self.__class__(np.round(self.m, ndigits), self.u)) + elif isinstance(m, np.ndarray): + return cast("Quantity[DT, MT]", self.__class__(np.round(cast("Numpy1DArray", m), ndigits), self.u)) + elif isinstance(m, (pl.Series, pl.Expr)): + # every container rounds: polars spells it as a method on the object rather + # than through __round__, so route to it explicitly (a negative ndigits is + # rejected by polars, unlike numpy -- that difference is polars', not ours) + return self._call_subclass(cast("MT", m.round(ndigits)), self.u) else: - raise TypeError(f"round() is not supported for magnitude type {type(self.m)}") + raise TypeError(f"round() is not supported for magnitude type {type(m)}") @property def is_scalar(self) -> bool: @@ -2090,11 +2121,20 @@ def is_scalar(self) -> bool: @property def ndim(self) -> int: - """Number of magnitude dimensions: 0 for a scalar, 1 for a vector magnitude.""" + """Number of magnitude dimensions: 0 for a scalar, 1 for a vector magnitude. + + A ``pl.Expr`` magnitude is a column expression, so it counts as a vector even + though its length is unknown until the plan is collected. + """ if isinstance(self.m, (float, int)): return 0 + # neither pl.Series nor pl.Expr has an ndim attribute, and only 1-dimensional + # magnitudes exist in this library, so the vector containers answer 1 directly + if isinstance(self.m, (pl.Series, pl.Expr)): + return 1 + return getattr(self.m, "ndim", 0) def asdim(self, other: type[DT_] | Quantity[DT_, MT]) -> Quantity[DT_, MT]: @@ -3158,7 +3198,12 @@ def __mul__(self, other: Quantity[Any, Any] | float) -> Quantity[Any, Any]: # preserve the dimensionality for other # it might be a distinct subclass with identical units as another dimensionality - if self.dimensionless and isinstance(other, Quantity): + # NOTE: isinstance first, and self.u.dimensionless rather than self.dimensionless: + # pint's quantity-level property converts the whole magnitude to root units, which + # is an extra O(n) pass plus a temporary on the hottest operator in the library -- + # even for q * 2.0, where this branch can never be taken. The unit-level check is + # magnitude-independent and answers identically for every registered unit + if isinstance(other, Quantity) and self.u.dimensionless: subcls = self.get_subclass(other._dimensionality_type, type(ret.m)) return subcls(ret) @@ -3624,11 +3669,19 @@ def __truediv__(self, other: Quantity[DT_, MT]) -> Quantity[UnknownDimensionalit def __truediv__(self, other: Quantity[Any, Any] | float) -> Quantity[Any, Any]: if isinstance(other, Quantity): self._check_comparable_magnitudes(self.m, other.m, "combine") # ty: ignore[invalid-argument-type] + + # NOTE: division by zero deliberately follows each container's own semantics rather + # than being normalized: a float magnitude raises ZeroDivisionError (Python's rule + # for floats, which a Quantity must not swallow), while ndarray and polars + # magnitudes yield IEEE ±inf / nan elementwise (numpy warns, polars is silent). + # Normalizing either way would mean lying about one container to match the other; + # pinned by test_division_by_zero_follows_the_container ret = cast("Quantity[DT, MT]", self._pint_super.__truediv__(other)) # preserve the dimensionality for other # it might be a distinct subclass with identical units as another dimensionality - if self.dimensionless and isinstance(other, Quantity): + # (see __mul__ for why this is a unit-level check with isinstance first) + if isinstance(other, Quantity) and self.u.dimensionless: subcls = self.get_subclass(other._dimensionality_type, type(ret.m)) return subcls(ret) @@ -3682,7 +3735,9 @@ def __floordiv__(self, other: Quantity[Any, Any] | float) -> Quantity[Any, Any]: if isinstance(other, (float, int)): return self._call_subclass(cast("MT", self._floor_magnitude(self.m / other)), self.u) - if other.dimensionless: + # unit-level check: the quantity-level property would convert other's whole + # magnitude to root units just to answer a question about its unit (see __mul__) + if other.u.dimensionless: magnitude = self._floor_magnitude(self.m / other.to_base_units().m) return self._call_subclass(magnitude, self.u) diff --git a/pyproject.toml b/pyproject.toml index 5e2e730..06841f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "encomp" -version = "1.9.2" +version = "1.9.3" description = "General-purpose library for engineering computations" authors = [{ name = "William Laurén", email = "lauren.william.a@gmail.com" }] requires-python = ">=3.13" diff --git a/uv.lock b/uv.lock index 23fb339..9fa8350 100644 --- a/uv.lock +++ b/uv.lock @@ -49,23 +49,23 @@ wheels = [ [[package]] name = "annotated-types" -version = "0.7.0" +version = "0.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +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/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, + { 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 = "anyio" -version = "4.14.1" +version = "4.14.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3b/72/5562aabb8dd7181e8e860622a38bea08d17842b99ecd4c91f84ac95251b0/anyio-4.14.1.tar.gz", hash = "sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e", size = 254831, upload-time = "2026-06-24T20:56:06.017Z" } +sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl", hash = "sha256:4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72", size = 124875, upload-time = "2026-06-24T20:56:04.413Z" }, + { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, ] [[package]] @@ -79,11 +79,11 @@ wheels = [ [[package]] name = "asttokens" -version = "3.0.1" +version = "3.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" } +sdist = { url = "https://files.pythonhosted.org/packages/25/1e/faf0f247f6f881b98fc4d6d07e14085cb89d13665084e6d6ac1dc2c03d0b/asttokens-3.0.2.tar.gz", hash = "sha256:3ecdbd8f2cc195f53ccada3a613538bb5f9ef6f6869129f13e03c30a677b8fe2", size = 63136, upload-time = "2026-07-12T03:31:49.084Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, + { url = "https://files.pythonhosted.org/packages/d4/2b/04b8a15f3a1c77bc79ddf5c73875327f34b4fa75982df2b76e45e402d364/asttokens-3.0.2-py3-none-any.whl", hash = "sha256:9da13157f5b28becde0bd374fc677dcd3c290614264eff096f167c469cd9f933", size = 28702, upload-time = "2026-07-12T03:31:47.542Z" }, ] [[package]] @@ -156,11 +156,11 @@ wheels = [ [[package]] name = "certifi" -version = "2026.6.17" +version = "2026.7.22" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, ] [[package]] @@ -407,56 +407,56 @@ wheels = [ [[package]] name = "coverage" -version = "7.15.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cc/8b/adeb62ea8951f13c4c7fef2e7a85e1a06b499c8d8237ea589d496029e53f/coverage-7.15.0.tar.gz", hash = "sha256:9ac3fe7a1435986463eaa8ee253ae2f2a268709ba4ae5c7dd1f52a05391ad78f", size = 925362, upload-time = "2026-07-02T13:10:50.535Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/76/04/145a3748098bcc86b631a85408d2c3dc5c104e0bd86d605468239b25b6c4/coverage-7.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5be4caf3b28836f078abe700f8944dac4a65d78f16d6c600c89cb624e5535782", size = 220863, upload-time = "2026-07-02T13:09:25.371Z" }, - { url = "https://files.pythonhosted.org/packages/a4/5c/4ed55708fed2c64b63c9bc5715daef670872202101938869b7fe5d5fbb8f/coverage-7.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dd58ad1404704303ca8d4f4b8a1095e7cbc7040ef17a66df1e6619aa10176430", size = 221230, upload-time = "2026-07-02T13:09:26.897Z" }, - { url = "https://files.pythonhosted.org/packages/7b/19/3a80b97d3b2a5c77a01ae359c6bed20c13738fe3d9380f08616d4fec0281/coverage-7.15.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bbcbb317c2e5ded5b21104af81c29f391be2af98d065693ffbe8d23949b948e5", size = 252227, upload-time = "2026-07-02T13:09:28.543Z" }, - { url = "https://files.pythonhosted.org/packages/a1/fa/b70062750686bd7da454da27927622f48bbac6990ac7a4c4a4653e7b0036/coverage-7.15.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:27f31ecb458da3f859aab3f15ada871eb7a7768807d88df4a9f186bb17737970", size = 254823, upload-time = "2026-07-02T13:09:30.177Z" }, - { url = "https://files.pythonhosted.org/packages/a9/09/dad6a75a2e561b9dc5086a8c5257a7591d584246f67e23e70d2995b89ab6/coverage-7.15.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fb759be317fdc62e0f56bffdf61cfcb45c7761ad6b71e3e583e71a67ae753c", size = 256059, upload-time = "2026-07-02T13:09:31.979Z" }, - { url = "https://files.pythonhosted.org/packages/e6/e7/b5d2941fa9564573d44b693a871ff3156f0c42cbefe977a09fa7fdc59971/coverage-7.15.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d5cf007add5ab4bb8fa9f4c77e3732127c9e6cad501d7db43355fbfafca0be84", size = 258190, upload-time = "2026-07-02T13:09:34.035Z" }, - { url = "https://files.pythonhosted.org/packages/7c/1d/8e895bcde3c57ccd46d896dda5f2b3d5df761a1b0c6c9d450d175dedc632/coverage-7.15.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc78d9843bd576fbe2118248258d485e968dc535f95ed504a7b0867ba9b51389", size = 252456, upload-time = "2026-07-02T13:09:35.765Z" }, - { url = "https://files.pythonhosted.org/packages/14/4c/f6997da343ddeb959be82c3b05322793f92c071ad45f7cb8a96336e2dd5f/coverage-7.15.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a263060f1de0b4b74b4e089c2a70b8003b3781c733329a9c8fd54995328f9950", size = 254192, upload-time = "2026-07-02T13:09:37.445Z" }, - { url = "https://files.pythonhosted.org/packages/17/27/a0bc09d032267b9da89d95a2d874cfbef2a5aebbf0e87cf7aba221d79a99/coverage-7.15.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c48decf16e0dfd5b049c7d5e82200c23c08126719142998d4f172444e3d0529e", size = 252153, upload-time = "2026-07-02T13:09:39.422Z" }, - { url = "https://files.pythonhosted.org/packages/54/c0/77fc233d9fba07b244c40948c53fe27308b8f21732fb3417f87fbd6fd992/coverage-7.15.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:08fb028000ed0aaa0a4cbdfbb98be7cb42f370db973fbbb469733505ab20e13e", size = 256310, upload-time = "2026-07-02T13:09:41.006Z" }, - { url = "https://files.pythonhosted.org/packages/d5/24/601cecfb5825becacb8d45219a018a3b55b9dbaec624efdb0ea249d08be2/coverage-7.15.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb7dc0c3b7d8a1077abea0b8546ebc5e26d6ef6ecefc2f0f5ad2b8a53bdad837", size = 251974, upload-time = "2026-07-02T13:09:42.733Z" }, - { url = "https://files.pythonhosted.org/packages/47/1e/6f45e5a5b3d5484318d368702af6716b5ab8913b0428bec981a562fcf296/coverage-7.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cb3602054ccbe9f0d8c2dc04bbeba90d5719236e2cd06e042ddd6d3fc7b6e37", size = 253745, upload-time = "2026-07-02T13:09:44.376Z" }, - { url = "https://files.pythonhosted.org/packages/8e/db/4df027a77bd11d0e527f44c53557c76e54ad027413d0304252ea3a78d67e/coverage-7.15.0-cp313-cp313-win32.whl", hash = "sha256:0bf781da64326b677be344df505171435b6f58716108606621d5d27d964fff8b", size = 222902, upload-time = "2026-07-02T13:09:46.122Z" }, - { url = "https://files.pythonhosted.org/packages/a0/10/0355894d34e231f2c5449e71287e81a50793a325df2e2b027b7bcd9dfd19/coverage-7.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:2c57a275078ee3fa185f83e400f765bc764a549de66d99b47881645cbd4ea629", size = 223444, upload-time = "2026-07-02T13:09:47.687Z" }, - { url = "https://files.pythonhosted.org/packages/06/ef/bb725f263befaaff851203ab338e68af15e195d7f7b5f323162532d9b6a8/coverage-7.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:3812c61afc6685c7999b39320779ab8f43b7a3081fdb0def39976e56fbdb9a21", size = 222839, upload-time = "2026-07-02T13:09:49.717Z" }, - { url = "https://files.pythonhosted.org/packages/4f/9c/1e3ca54f72a3185ece06c58d871099898c48f0ed6430d17b6ab75f0d180a/coverage-7.15.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:41cb79af843222e11da87127ad0ecbfa878abadd0f770a4a99391a27d3887324", size = 220906, upload-time = "2026-07-02T13:09:51.339Z" }, - { url = "https://files.pythonhosted.org/packages/09/37/f718613d83b274880382f6b67e78f3802549ae39b0b3e65ae5b5974df56e/coverage-7.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7d2008989ef8fe54188d3f3bfa2e3099b025af11e90a6a1b9e7dc433d04263d8", size = 221239, upload-time = "2026-07-02T13:09:53.138Z" }, - { url = "https://files.pythonhosted.org/packages/a7/ce/22bae91e0b75445f68d365c7643ed0aa4880bbf77450ee74ca65bdae53a7/coverage-7.15.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:769e8ece11a596315ebf5aa7ec383aeeed016c091d2bf6363ffb996d41529092", size = 252286, upload-time = "2026-07-02T13:09:54.996Z" }, - { url = "https://files.pythonhosted.org/packages/dd/1e/bec5e32aa508615d9d7a2790effb25fb4dc28606e995816afe400b25ece3/coverage-7.15.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:65a6b6164ee5c39e2f3803f314292d6c61a607ba7fee253d1e03c42dc3903502", size = 254789, upload-time = "2026-07-02T13:09:56.678Z" }, - { url = "https://files.pythonhosted.org/packages/17/29/0e865435b4354e4a7c03b1b7920046d31d0a273d55decefea27e011cb9bf/coverage-7.15.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75128817f95a5c45bb01d65fd2d8b9cb54bbe03d81608fb70e3e14b437ad56c2", size = 256135, upload-time = "2026-07-02T13:09:58.343Z" }, - { url = "https://files.pythonhosted.org/packages/84/ff/33a870b58a13325d62fc0a6c8f01fa0ff667cef60c7498e2382a147dfa18/coverage-7.15.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9887bb428fe2d4cd4bee89bac1a6c9932f484afd5b36fbd4ff6ea5f825bb1f5e", size = 258449, upload-time = "2026-07-02T13:10:00.057Z" }, - { url = "https://files.pythonhosted.org/packages/18/7b/6fffe596bf3ddba8462758d02c5dad730fd91055a6634aa2e4226229181a/coverage-7.15.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0bfc0be1f702042207a93a00523b1065ee1fe951e96edf311581c0bbc2e34888", size = 252313, upload-time = "2026-07-02T13:10:01.946Z" }, - { url = "https://files.pythonhosted.org/packages/58/1b/11468dd6c1676ab831a70cb9a8d4e198e8607fa0b7220ab918b73fe9bfbd/coverage-7.15.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f64627d55def5a43282d70e08396672692f77e4da610a5bb8bb4060b432b6859", size = 254142, upload-time = "2026-07-02T13:10:04.065Z" }, - { url = "https://files.pythonhosted.org/packages/79/41/29328e21d16b1b95092c30dd700e08cf915bd3734f836df8f3bdb0e8fa9f/coverage-7.15.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:2c6f0fa473003905c6d5bac328ee4eba9fbea654f15bc24b8a3274b23363fa99", size = 252108, upload-time = "2026-07-02T13:10:06.11Z" }, - { url = "https://files.pythonhosted.org/packages/9b/de/05ccfb990439655b35afbfd8e0d13fe66677565a7d4eb38c3f5ef2635e1c/coverage-7.15.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2bcf9afaf064172c6ec3c58a325a9957ad1178c05dd934e25f253321776e0676", size = 256385, upload-time = "2026-07-02T13:10:08.141Z" }, - { url = "https://files.pythonhosted.org/packages/51/0e/486828a3d2695ea7a2609f17ff572f6b01905e608379440a11da4b8dffbe/coverage-7.15.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:baf06bc987115d6fb938d403f7eab684a057766c490367999a2b71a6883110c6", size = 251923, upload-time = "2026-07-02T13:10:10.179Z" }, - { url = "https://files.pythonhosted.org/packages/18/c7/03582b6715f078e5e558354c87616d945b9894cda2dace8e4009b17035e4/coverage-7.15.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f0405f2ff97b1c4c0e782cb32e02f32369bcf2e6b618b591d67e1ea754575dfe", size = 253580, upload-time = "2026-07-02T13:10:12.052Z" }, - { url = "https://files.pythonhosted.org/packages/db/dc/9e578bbaf2ecb4959a81b7e7601ad8cca772cba2892e8d144cb749b4a71a/coverage-7.15.0-cp314-cp314-win32.whl", hash = "sha256:ab282853ed5fbd64bbb162f19cb8fcb7087187508a6374b4f9c34ec1577c4e8f", size = 223107, upload-time = "2026-07-02T13:10:13.994Z" }, - { url = "https://files.pythonhosted.org/packages/ae/3e/c8c3b75d8dbe0e35f7b0cc3ff5e949fc59500f70b21d0398813f66740664/coverage-7.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:3bb3040e9f4bbe26fcb0cd7cc85ac63e630d3f3a9c74f027abf4caa27e706663", size = 223597, upload-time = "2026-07-02T13:10:15.906Z" }, - { url = "https://files.pythonhosted.org/packages/cd/bc/3cbc9fb036eb388519bccd521f783499c39b64256013fbc362782f196fe1/coverage-7.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:346771144d34f7fa84ec28386f78e0f31653f33cf35e19d253d5b35f9e8201da", size = 223020, upload-time = "2026-07-02T13:10:17.844Z" }, - { url = "https://files.pythonhosted.org/packages/28/00/199c4a8d656dff63102577a056c0fce2ff6a79e40adac092fc986c49cbf1/coverage-7.15.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d34a010905fb6401324ba016b5da03d574967f7b21ce48ea41e66f0f1f95f641", size = 221638, upload-time = "2026-07-02T13:10:19.703Z" }, - { url = "https://files.pythonhosted.org/packages/ba/8e/9d0092c96a3d3a26951ecc7020826aa57bcb1b119ca81acbba996884ab13/coverage-7.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bb25d825d885ca8036795dacfc3924d33091fc76d71ebc99420c6b79e77d96fa", size = 221903, upload-time = "2026-07-02T13:10:21.514Z" }, - { url = "https://files.pythonhosted.org/packages/6d/b4/c0ca3028f42c9a08e51feb4561ef1192e5de99797cd1db5b04590c215bda/coverage-7.15.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:94c9686bfe8a9a6810297aecbd99beaa3445f9e8dc2f80b1382cca0d86b64461", size = 263267, upload-time = "2026-07-02T13:10:23.261Z" }, - { url = "https://files.pythonhosted.org/packages/5f/aa/a375e3846e5d3c013dc600b2a3231089055c73d77f5393dd2192a8d64da6/coverage-7.15.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9bd671c25f9d85f09d7ec481d0e43d5139f486c06a37139847a7ce569788af72", size = 265390, upload-time = "2026-07-02T13:10:25.152Z" }, - { url = "https://files.pythonhosted.org/packages/92/e1/5783cdabb797305e1c9e4809fea496d31834c51fa772514f73dc148bcfc9/coverage-7.15.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:110cbdf8d2e216577312cf06ccf85539c0e5a5420ef747e4a4719b5e483c88cd", size = 267811, upload-time = "2026-07-02T13:10:27.249Z" }, - { url = "https://files.pythonhosted.org/packages/85/31/96d8bbf58b8e9193bc8389574a91a0db48355ee98feb66aa6bf8d1b32eea/coverage-7.15.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2c5d4619214f1d9993e7b00a8600d14614b7e9d84e89507460b126aa5e6559e5", size = 268928, upload-time = "2026-07-02T13:10:29.242Z" }, - { url = "https://files.pythonhosted.org/packages/5e/7a/5294567e811a1cb7eda93140c628fa050d66189da28da320f93d1d815c73/coverage-7.15.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:781a704516e2d8346fbbd5be6c6f3412dd824785146528b3a01816f26c081007", size = 262378, upload-time = "2026-07-02T13:10:31.107Z" }, - { url = "https://files.pythonhosted.org/packages/69/3f/3f48538421f899f28946f90a3d272136a4686e1abf461cc9249a783ee0f3/coverage-7.15.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd4a1b44bcb65ee29e947ac92bbee04956df3a6bfc6143641bb6cae7ede00fc9", size = 265263, upload-time = "2026-07-02T13:10:32.942Z" }, - { url = "https://files.pythonhosted.org/packages/ce/d3/092df15efcab8a9c1467ee960eb8019bbad3f9300d115d89ea6195f369ff/coverage-7.15.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0e4950c9d6d3e39c64c991814ff315e2d0b9cb8152363594212c9e55208c0a8f", size = 262866, upload-time = "2026-07-02T13:10:35.104Z" }, - { url = "https://files.pythonhosted.org/packages/e5/ab/0254d2b88665efb2c57ad368cc77ab5de3435bd8d5add4729c1b0e79431e/coverage-7.15.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:fe9c87ff42e5472d80d21704972e1f96e104a0a599d77c5e35db5a3c562e2571", size = 266599, upload-time = "2026-07-02T13:10:37.05Z" }, - { url = "https://files.pythonhosted.org/packages/a8/79/1cfa4023e489ce6fbc7be4a5d442dbc375edb4f4fda39a352cedb53263c2/coverage-7.15.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f00d5ae1dd2fe13fb8186e3e7d37bcbd8b25c0d764ff7d1b32cef9be058510a8", size = 261714, upload-time = "2026-07-02T13:10:38.966Z" }, - { url = "https://files.pythonhosted.org/packages/b7/eb/fee5c8665656be63f497418d410484637c438172568688e8ac92e06574e7/coverage-7.15.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:363ab38cc78b615f11c9cac3cf1d7eef950c18b9fdedfb9066f59461dcf84d68", size = 264025, upload-time = "2026-07-02T13:10:40.789Z" }, - { url = "https://files.pythonhosted.org/packages/ab/99/63005db722f91edc81abc16302f9cc2f6228c1679e46e15be9ae144b14d0/coverage-7.15.0-cp314-cp314t-win32.whl", hash = "sha256:54fd9c53a5fafff509195f1b6a3f9be615d8e8362a3629ff1de23d270c03c86b", size = 223413, upload-time = "2026-07-02T13:10:42.597Z" }, - { url = "https://files.pythonhosted.org/packages/c1/e8/2bc6181c4fb06f1a6b981eb85330cc57bfad7e3f710fc9c9d350013ba228/coverage-7.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:87b47553097ba185ed964866078e7e63adea9f5f51b5f39691c34f30afd21080", size = 224245, upload-time = "2026-07-02T13:10:44.47Z" }, - { url = "https://files.pythonhosted.org/packages/79/b8/4d959bf9cc45d0cfed2f4d35cafcab978cdb6ea02eb5100009cd740632a3/coverage-7.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aeefb2dd178fe7eee79f0ad25d75855cb35ee9ed472db2c5ea06f5b4fd00cec5", size = 223558, upload-time = "2026-07-02T13:10:46.368Z" }, - { url = "https://files.pythonhosted.org/packages/52/30/21b2ad45959cd50e909e02ebac1e30b4ceb7162e91c11d4c570223a458b7/coverage-7.15.0-py3-none-any.whl", hash = "sha256:56da6a4cbe8f7e9e80bd072ca9cefe67d7106a440a7ec06519ec6507ac94ad19", size = 212632, upload-time = "2026-07-02T13:10:48.641Z" }, +version = "7.15.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/d0/55fe630f4cf94e3fcba868240fad8c8cdd1f764e2a932f8926347e6ec4cd/coverage-7.15.2.tar.gz", hash = "sha256:3df60dc267f0a2ca23cb7a9ab1109c62b9335ffbf519fcfe167157c28c09b81d", size = 927741, upload-time = "2026-07-15T18:56:19.558Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/d5/f8c838e6b7282976f7c918884b792df7a0c42c5bba5d99c60ad2d221d56d/coverage-7.15.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1121caa19159a38b5463eaae4b1e1fde81e525b15ecc5e000cd5b1a108f743a8", size = 221606, upload-time = "2026-07-15T18:54:45.448Z" }, + { url = "https://files.pythonhosted.org/packages/bf/37/97c926376364f66298cc44893b89cdf17b8bc406376497c4061ae4b8a8ff/coverage-7.15.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a300c6934e0989c327b9e8a1e110329da4641149f872bbe9f70168be66da76c1", size = 221982, upload-time = "2026-07-15T18:54:47.341Z" }, + { url = "https://files.pythonhosted.org/packages/b7/30/a36050a6e83c2135ee0776f452ca3948224befc6d7f26acecc082d0c106a/coverage-7.15.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2617f8799d268fabdeef42a7e89ac3a23e1deee9025427db2df970f99a89a578", size = 252972, upload-time = "2026-07-15T18:54:49.2Z" }, + { url = "https://files.pythonhosted.org/packages/31/d3/06b5f1daf95f0f15ab05bd75f26ba5f3c8b33d0bb72f3aaa3cf41d1bad3a/coverage-7.15.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7dc2950a2992cd676d35c20ae63522836deeb034f08874699d14068710af3dc1", size = 255569, upload-time = "2026-07-15T18:54:51.098Z" }, + { url = "https://files.pythonhosted.org/packages/81/1c/9afb3f8de2b8d36960391c48559a2e3ff96594b58099f115921549ea8d0d/coverage-7.15.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9e36686f7a442185db2400b3df171aac520869faf9deb59df687d28659eda2a6", size = 256806, upload-time = "2026-07-15T18:54:53.145Z" }, + { url = "https://files.pythonhosted.org/packages/64/d8/b989f96061a5e32d82fddd1b1b9ff48a7c8f8ae7606f0e80fd9de54b1e33/coverage-7.15.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d29ca7bd67af6e12e74632d65f026eabc1364da5c254494cd914446a28a3ef7", size = 258936, upload-time = "2026-07-15T18:54:55.015Z" }, + { url = "https://files.pythonhosted.org/packages/b8/fa/f99771f5110457c7b511c1935ca49ddf288218eaa84322e028b9334146ae/coverage-7.15.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:db9c8438057e5b0f6a22a0af99c0c1d26b57fbbdbd1be5861ddb8f897fcc3a2d", size = 253178, upload-time = "2026-07-15T18:54:57.527Z" }, + { url = "https://files.pythonhosted.org/packages/f6/96/c098a6044d119c751ceede7be91035fa8310170ec24a6523aff72f0a5793/coverage-7.15.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:63022c4c8dec1d0342f05c3ede99842fe3d007689acc45e86f123a1746e4a026", size = 254934, upload-time = "2026-07-15T18:54:59.41Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a2/1457b3a7a50c8d77500103b97a046db863e2f59a1cf6d2f814595f349885/coverage-7.15.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6c0be82b4d4aa5b2704e08518e2252f3e3d110164bcca826816801052e48a7aa", size = 252898, upload-time = "2026-07-15T18:55:01.338Z" }, + { url = "https://files.pythonhosted.org/packages/6c/0e/76958874c471ecfcdde0d2b2747bb2c61bdbf34a40636f4ce9db9923e643/coverage-7.15.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4510fb9cdf6bb02dfa6af0be4a534b8102d086e22e4a33f8836df663da3d660d", size = 257056, upload-time = "2026-07-15T18:55:03.243Z" }, + { url = "https://files.pythonhosted.org/packages/7c/7c/3d7c4e3bf58baa40327dc7edc2272b17cf02299366d52763db1b0ca1556a/coverage-7.15.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:42ec3d989421b174a2ab607c1539f24127ad362757b7f1c0c0d7a2993f7eb37b", size = 252718, upload-time = "2026-07-15T18:55:05.029Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b8/1cecffed9ce14fb25be9ba42d37b6bb61485c9a3ddd43cd3dde36b6087d8/coverage-7.15.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e8f91bce78e32343af184c3b7fa28fcf5a9e2641f4b6623d392038f804939188", size = 254490, upload-time = "2026-07-15T18:55:06.889Z" }, + { url = "https://files.pythonhosted.org/packages/6c/2c/42984561bc7f4c045dca67516a0c50ee5ef8d84352dbeb5559dc86c4823e/coverage-7.15.2-cp313-cp313-win32.whl", hash = "sha256:434e68d531858205895eb0d74b73d20b84260de426387d53c422a5acda2cf050", size = 223647, upload-time = "2026-07-15T18:55:08.941Z" }, + { url = "https://files.pythonhosted.org/packages/41/9f/39c7c9245efc583beddf89a87683574e663ed93637f3afb6cd7b88405676/coverage-7.15.2-cp313-cp313-win_amd64.whl", hash = "sha256:26c3b04a6377fd7c09800921fa934e3a17c0020439cd59df73e73ae1d4b6a78c", size = 224190, upload-time = "2026-07-15T18:55:10.789Z" }, + { url = "https://files.pythonhosted.org/packages/c7/de/3a2883cf8a213659280ef4b403059e17a9acaeb7fc7fd4105e1226ff2e6d/coverage-7.15.2-cp313-cp313-win_arm64.whl", hash = "sha256:3ed010aa1b69cda8e827aabfca9866216c980e2dca82ab9a78c5f83689964c8b", size = 223583, upload-time = "2026-07-15T18:55:12.678Z" }, + { url = "https://files.pythonhosted.org/packages/81/5f/aed265fd7a3551a394f36dfe41868aee709b7f95db4052205b4ad1563ac3/coverage-7.15.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:40f633c5c5fc783732f6312280122e859538fa24461235597c13d803ea9a108a", size = 221650, upload-time = "2026-07-15T18:55:14.527Z" }, + { url = "https://files.pythonhosted.org/packages/6b/2c/222ba12a545189017120f8eddfc1a0bd4616b47d5d4a8d99421edb2fe4c6/coverage-7.15.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:075560438765b7a2ef43bf7aa7758661b53d889df47f062a31bda6c1ade553a2", size = 221988, upload-time = "2026-07-15T18:55:16.674Z" }, + { url = "https://files.pythonhosted.org/packages/aa/38/304b5877ab46e6c290b4292cfcf3fe28245f0e5597cad7f6acc91fc7e0a4/coverage-7.15.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:25fd15dd40a0a2c51a500d664ca29053c09c3259d998407bf982b6e114696138", size = 253029, upload-time = "2026-07-15T18:55:18.856Z" }, + { url = "https://files.pythonhosted.org/packages/6c/58/821b533b8db9e44cf1d8a97bd525149ced40dde1d0093da02cb78e715244/coverage-7.15.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b9a6367e4aff723e8ee8190836836124284e8fcd4265e307c844010cfa074f3f", size = 255536, upload-time = "2026-07-15T18:55:21.027Z" }, + { url = "https://files.pythonhosted.org/packages/f1/f2/7aa06604c389d32ea7f0a6a988359a7eafc3cd3f8e7bc2e88cd2fdf0b877/coverage-7.15.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9854ca62c152874b2060772503535be2e8f53f70b8aaa7686b094888d872f984", size = 256881, upload-time = "2026-07-15T18:55:23.125Z" }, + { url = "https://files.pythonhosted.org/packages/a2/4f/1ef342339c7916d0096bc5888cc0f653882cc7bc8f897d5cb89143287c9b/coverage-7.15.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:913b6c56e110da40e035bbd168353bf7aaa2544a5eaccea5d98a4629aac156c7", size = 259196, upload-time = "2026-07-15T18:55:25.099Z" }, + { url = "https://files.pythonhosted.org/packages/fe/f4/7ed055d7a9c5ec13b161773a115a5ccc6b0081d568c31fad830806306cc7/coverage-7.15.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aaccad4129d735a8a4d526f26929894c9a4e8ef7034566f210b176749d6906e3", size = 253036, upload-time = "2026-07-15T18:55:27.018Z" }, + { url = "https://files.pythonhosted.org/packages/14/79/ea82cca18c242a3a38b6c017da39726aa62dcb64aa635abf79b92009975c/coverage-7.15.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a164b50081fc7357331c4024ef4d17b78ba325f8380d05f5a69599a7e05257ee", size = 254887, upload-time = "2026-07-15T18:55:29.084Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ba/a136db3c0d9562b00e10b72540dbf3a33cd3bc5b95060c9308e247494623/coverage-7.15.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:bfd341ccf78128e72c094bc70cc25b3ef309c33c7c2c66ba3ed4309549e02de1", size = 252852, upload-time = "2026-07-15T18:55:31.184Z" }, + { url = "https://files.pythonhosted.org/packages/17/17/ea334246b16b7d059953fad6fdefa11e33c68efbd3fe37b1098120a1fac2/coverage-7.15.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1473b3ba8e7ee0f076117b1a72c23f579a2b9e2bb742f48a8d86ea27ca93f91a", size = 257128, upload-time = "2026-07-15T18:55:33.163Z" }, + { url = "https://files.pythonhosted.org/packages/ed/c3/074fb66d46d607855f710876b117cbda562c5ab08363528e78820449f937/coverage-7.15.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:17c432b5f73ad52ef46fb06019f6fa7c66ce381961cf0f7dfd1d3a4bd3a98145", size = 252668, upload-time = "2026-07-15T18:55:35.063Z" }, + { url = "https://files.pythonhosted.org/packages/e1/c1/f620850ada9b36435921c9a3a8057013422b1d964eb4bf37fe138724d192/coverage-7.15.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:77f0ef5011df53a4bd1b35211ab122287f8d9b8d7aa1c4553e5c2deb24b1d446", size = 254325, upload-time = "2026-07-15T18:55:37.125Z" }, + { url = "https://files.pythonhosted.org/packages/cc/31/a729ca3689404493af82ef8e6ff70bd88bdda8da89aeef6ca9b387aeb2b4/coverage-7.15.2-cp314-cp314-win32.whl", hash = "sha256:f653e5d7248c1191ec988a85c72edeab46c3ff44f90639a4ed4874ec0be90243", size = 223844, upload-time = "2026-07-15T18:55:39.078Z" }, + { url = "https://files.pythonhosted.org/packages/c6/83/5d809dc808fb1698c671f3e372259bb9158e64b7ea526fc6ab7de64de9fe/coverage-7.15.2-cp314-cp314-win_amd64.whl", hash = "sha256:9911f31aad8906abe337c271343485cf20df5e70df5d2f57f9f136e7b55f26bc", size = 224331, upload-time = "2026-07-15T18:55:41.346Z" }, + { url = "https://files.pythonhosted.org/packages/16/4e/35e488548e952795829e129995c4174df33bf432b591d1aa42c8d9e4e7ad/coverage-7.15.2-cp314-cp314-win_arm64.whl", hash = "sha256:e38def96ad59853824c97953fdcd2c320a84ba3ce99b417db78af8bb6c3db635", size = 223760, upload-time = "2026-07-15T18:55:43.518Z" }, + { url = "https://files.pythonhosted.org/packages/ed/49/dd2c86cd6374038f6e415fb5bfb86db5218553209c081384a020369dee79/coverage-7.15.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:835ec4e20b45f0a7f63ed78f94065aca00de033403df8377bfe8b9c6abc0a7be", size = 222384, upload-time = "2026-07-15T18:55:45.569Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/173ff17a1c0808e5a438f549f6f145d5ac7528f2791310b63523e3200ac7/coverage-7.15.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7466cc7ab6dc0db871d264bf99e8779f0917ee63d40730af0552f71535a6e072", size = 222647, upload-time = "2026-07-15T18:55:47.544Z" }, + { url = "https://files.pythonhosted.org/packages/84/f8/b8cba872162356fb44ac79c10309d987206a4461e32072fc29228dad7331/coverage-7.15.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e370c12133095ff18432de8c044962be85a5a96d90c6fcbce8e17e76236d2328", size = 264013, upload-time = "2026-07-15T18:55:49.768Z" }, + { url = "https://files.pythonhosted.org/packages/ee/67/a807a7586d0b8cae485308ddd55756f0806c92f8e0b411bacbf23c48edf3/coverage-7.15.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fe41909c9515c3bfdb5f02c4d1f857dba322d9a9a1178069b91eea77889df63a", size = 266135, upload-time = "2026-07-15T18:55:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/ce/67/cd78771dc985f7e4ebdcc82b1a96d9a932af9e806f01f2f91a89f4c72e80/coverage-7.15.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6aa28cfb6488e5453b5b762d65f73aa586380f6693a04d58078ce228a29b06c0", size = 268555, upload-time = "2026-07-15T18:55:54.065Z" }, + { url = "https://files.pythonhosted.org/packages/18/3e/10134cf81275188c58568f324fc74aedff32c63ca4d5bbc513a91944a6f0/coverage-7.15.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bcc0aae933921d03096f53b0b03eeb702129fd406dee59f08d2efacc68681fa5", size = 269674, upload-time = "2026-07-15T18:55:56.066Z" }, + { url = "https://files.pythonhosted.org/packages/75/4a/771b77de446cba985dc414bbc5844bd21604da05dbc044286df8318a48a7/coverage-7.15.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7c63387e21ab21f512c69c9756a8c7dadd322c7275edb064064433c9a09c3743", size = 263101, upload-time = "2026-07-15T18:55:58.107Z" }, + { url = "https://files.pythonhosted.org/packages/5f/b5/70a7011da15f4071943361183aefa27847f3e3aec4fd335f1cb3d3a622b1/coverage-7.15.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e55510bc98ae943cece9e667a6c0fe94c6a92913720dea34243657a17993d0c", size = 266007, upload-time = "2026-07-15T18:56:00.468Z" }, + { url = "https://files.pythonhosted.org/packages/b4/0d/f9547e804ce7ad49646ffeffac26699510efbe6c0f751b66fdc960c4e825/coverage-7.15.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:2ff08701be2d1556fc78b326c80a3e8042da09352ecb3819105f8e386c8a3071", size = 263611, upload-time = "2026-07-15T18:56:02.615Z" }, + { url = "https://files.pythonhosted.org/packages/ac/59/f576a396659c0efd351f5c1544f67c3560e89c7761cabf7f65e412beeda5/coverage-7.15.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:38c9518b7103826c403a461544e3c2e77151e8676d06eaed85911a97e962584a", size = 267344, upload-time = "2026-07-15T18:56:04.622Z" }, + { url = "https://files.pythonhosted.org/packages/7c/5d/c2e4fce3579c0cb635024293f1a32bbe26df101b3e3a69f22243d1352b6c/coverage-7.15.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:dee88b1ed88587abd8c0269a1fc1f4cc77f7750d1dfde2869e2a123af420e67d", size = 262456, upload-time = "2026-07-15T18:56:06.641Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/956287d69436b66094bc4b57ac2da71e43bfd2a5524e958900b9f582fcf8/coverage-7.15.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2fbeeeecea279727f8ac16c8e1133ddfeee793e985c86ae343d6a5ce744eef8c", size = 264771, upload-time = "2026-07-15T18:56:08.795Z" }, + { url = "https://files.pythonhosted.org/packages/2c/5a/6f979530c2734c575de77cf58f5f28d51f7123a94b5030fd9156fe5f363c/coverage-7.15.2-cp314-cp314t-win32.whl", hash = "sha256:cb0fddaa6884be6aae36ced9544b5e90f7d5f03845a2853bf47a14953a4e8688", size = 224151, upload-time = "2026-07-15T18:56:10.856Z" }, + { url = "https://files.pythonhosted.org/packages/54/7e/27f6b2a74d484742f4017553e710b01e396b23d809df3e95ca0bb9a2824b/coverage-7.15.2-cp314-cp314t-win_amd64.whl", hash = "sha256:77f091ea3a9cc611cd29f433565476bc1936c084ac8eee00ea0e7e70c27e4199", size = 224981, upload-time = "2026-07-15T18:56:12.928Z" }, + { url = "https://files.pythonhosted.org/packages/b1/48/284863423aa474240f6842bd00d680da22f4e6ea2e466618ef7c9c9e69a9/coverage-7.15.2-cp314-cp314t-win_arm64.whl", hash = "sha256:6fc448c377d6eeb00a47c673494bd9bae29280ca53987e1869e67ebedfe20658", size = 224294, upload-time = "2026-07-15T18:56:15.156Z" }, + { url = "https://files.pythonhosted.org/packages/ec/82/32e3bd191d498e64f6f911ad55d14006a0861e54869d2d32452326399e65/coverage-7.15.2-py3-none-any.whl", hash = "sha256:eb6bcae8d1a9d305351ecb108232441d11c5cfe9de840a04388ba5d2db8d735c", size = 213375, upload-time = "2026-07-15T18:56:17.305Z" }, ] [[package]] @@ -514,7 +514,7 @@ wheels = [ [[package]] name = "encomp" -version = "1.9.2" +version = "1.9.3" source = { editable = "." } dependencies = [ { name = "numpy" }, @@ -625,11 +625,11 @@ wheels = [ [[package]] name = "filelock" -version = "3.29.7" +version = "3.32.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/35/94/00f2059e4835eace3ae8fde680b932c496f8ec7bdc99168dfa53fb2e6b79/filelock-3.29.7.tar.gz", hash = "sha256:5b481979797ae69e72f0b389d89a80bdd585c260c5b3f1fb9c0a5ba9bb3f195d", size = 71521, upload-time = "2026-07-08T05:46:58.716Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/80/8232b582c4b318b817cf1274ba74976b07b34d35ef439b3eb948f98645a1/filelock-3.32.0.tar.gz", hash = "sha256:7be2ad23a14607ccc71808e68fe30848aeace7058ace17852f68e2a68e310402", size = 213757, upload-time = "2026-07-21T13:17:42.898Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/60/02/be4a57b60c7149b55b9e3b3c13f609cd8eb5307c751f22bd8fb8d262e75b/filelock-3.29.7-py3-none-any.whl", hash = "sha256:987db6f789a3a2a59f55081801b2b3697cb97e2a736b5f1a9e99b559285fbc51", size = 46036, upload-time = "2026-07-08T05:46:57.53Z" }, + { url = "https://files.pythonhosted.org/packages/06/79/b4c714bef36bc4ec2beeae1e0c124f0223888cd8c6feb1cdc56038116920/filelock-3.32.0-py3-none-any.whl", hash = "sha256:d396bea984af47333ef05e50eae7eff88c84256de6112aea0ec48a233c064fe3", size = 97732, upload-time = "2026-07-21T13:17:41.55Z" }, ] [[package]] @@ -722,34 +722,46 @@ wheels = [ [[package]] name = "hypothesis" -version = "6.156.2" +version = "6.161.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "sortedcontainers" }, ] -wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/f1/40b782791455411d09eb181fd688d615b0f46945f50a2b8ad1525a9aa0e2/hypothesis-6.156.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5ebeddf95d2d60283d079e9e3ee434ec2654868d35deb8d7de01221d86b9bd67", size = 749445, upload-time = "2026-07-07T20:24:33.387Z" }, - { url = "https://files.pythonhosted.org/packages/33/92/e92493ff3903a57d84618d379a180566e6e77b3a8e14fab0e6e15d6958b8/hypothesis-6.156.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:082f81fb0697d42809b9ca9b7d2b01e319363f9b6b368dded0f7cf29574228f8", size = 742078, upload-time = "2026-07-07T20:24:35.178Z" }, - { url = "https://files.pythonhosted.org/packages/0c/fb/3c9ae5cdc9e2c296bbd08a00f6a369df509b4ec19443d099d59258f02738/hypothesis-6.156.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82a23eede8f93ae5768b6161e51511e40fd0b3e6f08f25d94f17e476827a3e3f", size = 1069980, upload-time = "2026-07-07T20:23:49.575Z" }, - { url = "https://files.pythonhosted.org/packages/f8/0d/b9cd9bb75beeda4dc520b81d818db300c67659bb15d4c0d1eaac7b5818dd/hypothesis-6.156.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cc703d7f8d16006f2afb8b5c9de79d145888e1bc917431edef7ab058447910a", size = 1119942, upload-time = "2026-07-07T20:23:42.174Z" }, - { url = "https://files.pythonhosted.org/packages/63/b7/590bda127ac08d5ca03787f530093d9a97746550bded6cbe11c38cc33526/hypothesis-6.156.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8bc6a9801926b34d124628e54166c4f5e466831456349c1b32b3abb4320c5588", size = 1244026, upload-time = "2026-07-07T20:24:47.441Z" }, - { url = "https://files.pythonhosted.org/packages/ba/f2/45416c2fa565a6d9df9f1ee3844123c6332cc2f5444e65d0089cd4250d8b/hypothesis-6.156.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9494e7dfdcbafa1fb0b109929500e1f2ee71e58cc531919cc7721acb3ea0e0a9", size = 1286715, upload-time = "2026-07-07T20:24:38.136Z" }, - { url = "https://files.pythonhosted.org/packages/cf/15/c973adc18512e74da8e9deba457019b19b142f6e6b0fae432637ed7c3ce2/hypothesis-6.156.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1778a7446a7aff9420367241740d021bc9093f6f38f39ebdd45c3737fa7fcb4", size = 637936, upload-time = "2026-07-07T20:23:44.498Z" }, - { url = "https://files.pythonhosted.org/packages/24/ec/6446a111a1bc74a3dc41e4d0efc196fc79d93c9976f5fda06e81577a36b4/hypothesis-6.156.2-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d9ad1b6bcfc0d7e89be08ac8607fb75ebac7a95dc549ad0364fad141e9b06154", size = 749477, upload-time = "2026-07-07T20:24:05.453Z" }, - { url = "https://files.pythonhosted.org/packages/3b/f6/68422861dd20b71596cca2e26567dba64c59bec243db34909f571137571a/hypothesis-6.156.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7320f3725f694151bfb5e5c61ba62585fbe326b1ef7e9c7d7ba2ec8671ff9b70", size = 742301, upload-time = "2026-07-07T20:23:57.514Z" }, - { url = "https://files.pythonhosted.org/packages/b1/78/151738ee8606b9d3a838a0acb704a8f92e2b21ac5177f92be27fe959df73/hypothesis-6.156.2-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1f355b2923c0f93b77837a50d6d34da01e0ffc7d368b0cf1784756b7148894d", size = 1070153, upload-time = "2026-07-07T20:23:56.195Z" }, - { url = "https://files.pythonhosted.org/packages/7c/5b/86e2799b2cd736f72a4d866544511ec6fa040c624022e57e36881991dabc/hypothesis-6.156.2-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd042841fc0dfa62ce34db75cb49435ff77e70bf600aeee46c06152ba48e7c37", size = 1120175, upload-time = "2026-07-07T20:24:15.573Z" }, - { url = "https://files.pythonhosted.org/packages/8f/b8/078a73d85368902f00d1b3428afd8a98805e27485c1aaf0dad793507acb8/hypothesis-6.156.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5773eb13c8a1fc387119d7fcb5020881df7b3d87cdcb2a46b9d7ed64cf8b7bb5", size = 1244298, upload-time = "2026-07-07T20:24:21.232Z" }, - { url = "https://files.pythonhosted.org/packages/92/5a/fbc263393e2ed6ae16fa9a6dda4c28abbb5306ebcb93546d8cf5e496d39f/hypothesis-6.156.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e0422a383b8e6b249f0daa44c43126afb6e268064902917707db92e51a15d11f", size = 1287226, upload-time = "2026-07-07T20:23:48.296Z" }, - { url = "https://files.pythonhosted.org/packages/92/66/50297b7dd2275d6bd3fe55c07e195cc2cc7f266f2055932c85a37ec9e9f6/hypothesis-6.156.2-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:c3b020a7baada24c5f6e11516a9391a3dc87ee414acf9a2a07d98f7d5bcb119b", size = 586422, upload-time = "2026-07-07T20:24:22.751Z" }, - { url = "https://files.pythonhosted.org/packages/c0/4c/4deded2dbc5b6e372fbbabc97b95f4c74c3370cdd3efae08ebb26718b03b/hypothesis-6.156.2-cp314-cp314-win_amd64.whl", hash = "sha256:ca3ee647b8795e13ebb47ecacf6318a6ac31c2d431d9d08f92b9d6af2676fd0d", size = 637747, upload-time = "2026-07-07T20:24:01.473Z" }, - { url = "https://files.pythonhosted.org/packages/f0/19/3681202960f66d22ce154d45ba0a0865160f85cc64de958a1a8a06356735/hypothesis-6.156.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:b39c53e2eac55181dd9c2d701d1369575b4cb2a672e098515016ab3cdbab1cf1", size = 747519, upload-time = "2026-07-07T20:24:04.124Z" }, - { url = "https://files.pythonhosted.org/packages/83/b8/d08016b2f5db927b16b8437b79a3fb4b2b2d14c3e04b492ef634a32b42a5/hypothesis-6.156.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:41dd8d0305ee482ee5244c23190e7b6bb886836098dc6d2b7f5c9dde4047062c", size = 740868, upload-time = "2026-07-07T20:24:27.389Z" }, - { url = "https://files.pythonhosted.org/packages/69/2c/c130cf4e7e8390b05fd27a395db73e31fadb6ffd3139cc82590dc149c7cb/hypothesis-6.156.2-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52d0b106f4f0925955cfb0ceb485ac4208b6494320d3be3171e5dbe4f0a0fcb3", size = 1069173, upload-time = "2026-07-07T20:23:45.744Z" }, - { url = "https://files.pythonhosted.org/packages/b7/8f/2f2998931c9c3a4440e100856607019ecae5c04ab93d0e2fb1f9406326c4/hypothesis-6.156.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf23a2746085ecc874f6fc8d709498f1b20c9d8077532802b3e73e1ee65454a7", size = 1119250, upload-time = "2026-07-07T20:24:17.178Z" }, - { url = "https://files.pythonhosted.org/packages/d3/54/daf1e6dd20c0378fdf9d96bcf88ed6b5de34f97724e81ccd480265c43238/hypothesis-6.156.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:73644c1a403ea5864f87db073412985029dad019cec3c9350fc387623f4c999d", size = 1243065, upload-time = "2026-07-07T20:24:00.111Z" }, - { url = "https://files.pythonhosted.org/packages/41/36/8ba904da1928287757c009eb51bdd021d03b4e5def66ee6701c0ac94ac98/hypothesis-6.156.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:de71493b174e6857b1939240a8c9efa1486edd8e30aa524ed67ea6b3e79c05a7", size = 1285470, upload-time = "2026-07-07T20:24:18.662Z" }, - { url = "https://files.pythonhosted.org/packages/3b/ff/968df32637fdb4a8867b3102a4a1c43c2f07106a47caa0439e59f985804d/hypothesis-6.156.2-cp314-cp314t-win_amd64.whl", hash = "sha256:0d7ab28a7de1f316598037862f4accc12597cc86b48d1c9c63a4d95484be4e92", size = 637839, upload-time = "2026-07-07T20:24:52.058Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/f7/5b/98162d7cf48866e18b59d12e975229af411575a20d9a75c303345cc3380b/hypothesis-6.161.2.tar.gz", hash = "sha256:e25f1e6f8a2ea4cadfeaeba18cb8676e5510f52fefd5c9bc165e3eb81e1ef497", size = 486148, upload-time = "2026-07-24T06:43:23.774Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/8f/a565e6ab67de327eee310ef306d715d8f5143e729835450715dde121e912/hypothesis-6.161.2-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c61b868484dbcd9d2d6d25662f60fa2cee464d18061315efd997efe721250f14", size = 766523, upload-time = "2026-07-24T06:42:57.381Z" }, + { url = "https://files.pythonhosted.org/packages/92/d7/5deb1e38c8c253c879bec75a95fe0ff01d60366d7bd33c59012a8d23a8a1/hypothesis-6.161.2-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:b22012a92b8d2f70f7e7a6a4761166b920edf92900de458d6c9d272057dd48d8", size = 762160, upload-time = "2026-07-24T06:42:21.945Z" }, + { url = "https://files.pythonhosted.org/packages/dd/21/1f03b88df69b9b22429ad608c7a4778e01e9bb10656142e0426dbf0865eb/hypothesis-6.161.2-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d59687ed88b290e450505d71a5950422d39791dbaf48a4159b876634976e7898", size = 1091358, upload-time = "2026-07-24T06:42:03.947Z" }, + { url = "https://files.pythonhosted.org/packages/70/9f/5c761fdb60ed5c547b6803620822bca14486fda32d785a31416f1bbab970/hypothesis-6.161.2-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7af888ad7fe38c33112fde21756621b0d8f69167bbb01a5d66569aa90d6c692c", size = 1140836, upload-time = "2026-07-24T06:43:22.104Z" }, + { url = "https://files.pythonhosted.org/packages/1e/d5/1eb85756989ea885c325442ef2ebe5bd7347cbd4f99a907c384d10b343e5/hypothesis-6.161.2-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:9bd07a407addd5a9318305bc3cf78215662c6970b699a2df610955bf41750abc", size = 1096169, upload-time = "2026-07-24T06:42:09.992Z" }, + { url = "https://files.pythonhosted.org/packages/08/0e/c65f8c76f4d9d54203cbe5df6cf33e9b918629150b526d5df0ca5350b9fe/hypothesis-6.161.2-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:140e154d7317e8fcb538a8ff1c7b8ee52b67c9ab53647078e11cb950d2264c11", size = 1132927, upload-time = "2026-07-24T06:42:49.536Z" }, + { url = "https://files.pythonhosted.org/packages/37/da/be099e47ec288a8ce7a9030c47882166694b62c0fcd5d468f46559333848/hypothesis-6.161.2-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:febed60ec1ce744040f8f1e94a994a567a61b3f4bd82be4c3883b93ac76ec58a", size = 1265174, upload-time = "2026-07-24T06:42:34.36Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7c/e591135729eee9dd84cf68a539612ee9571b449f7a32ed9070802ccae4c4/hypothesis-6.161.2-cp310-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:d01840d49772e3a29117b936dd5b71c13a9a82b39a739ac49f228af9601c40e8", size = 1265785, upload-time = "2026-07-24T06:42:18.135Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e3/67a5f389fa1be4f4a68b3c65c15f3273d4fb5910cbc4c99754260ae52112/hypothesis-6.161.2-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4179f4db70daec64dc43d3fa9b23851d78791ca5d84605b3794df2dfc3150c61", size = 1307846, upload-time = "2026-07-24T06:43:05.511Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d8/45ffe9a9f069853fabb7b91fe72d46f9b2f08508550f87f3567fd1d46775/hypothesis-6.161.2-cp310-abi3-win32.whl", hash = "sha256:fabc7595dd1e0c66936e9777843dbef3c2e3f31983a669bda6b635909878176e", size = 652400, upload-time = "2026-07-24T06:42:54.175Z" }, + { url = "https://files.pythonhosted.org/packages/6a/14/81af65ce429671ee4e4b9fb6924a02564dd9e430c543da9ac9449dd07042/hypothesis-6.161.2-cp310-abi3-win_amd64.whl", hash = "sha256:425a35e9240761a2e71743424b193fd799dfa3117bad21982bbe8110637256b2", size = 658534, upload-time = "2026-07-24T06:43:10.538Z" }, + { url = "https://files.pythonhosted.org/packages/8e/98/3d4849d78b7eeafac335d55a8cbc647602eca41cce9fe5b99f28025d6f3b/hypothesis-6.161.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:c77580949be61ce4b946f8dfba38ed652ce28deb6302167550f85720f08c1864", size = 767990, upload-time = "2026-07-24T06:42:43.432Z" }, + { url = "https://files.pythonhosted.org/packages/50/eb/096d58e24d727a69e3ad3fb3887f4b3750d0e41287bb3a6ebfb9f20b6b8d/hypothesis-6.161.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ed59ea708743da8e91f975d3848d2ed0a46548d833c8703b53fa854bbb8f3ebd", size = 759677, upload-time = "2026-07-24T06:42:07.612Z" }, + { url = "https://files.pythonhosted.org/packages/e3/11/511ff00654fdc9a52316e1a772cecc6ef0a0c24f1138a20aeab2f632ae5b/hypothesis-6.161.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73fa1136acb5f554c6b958f8a9d33b2d7f6816909ccf07b22e17351df9bec146", size = 1090072, upload-time = "2026-07-24T06:43:03.917Z" }, + { url = "https://files.pythonhosted.org/packages/99/b8/2c833554fb3ba22d2841888b3409b634d6059420d1f5269f7cf3294358ed/hypothesis-6.161.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:584b1d91b5f3fa200b35bc0533e30e9fea81c852bb150eedbf0a7fc46d7639be", size = 1140009, upload-time = "2026-07-24T06:42:58.975Z" }, + { url = "https://files.pythonhosted.org/packages/35/36/c351c14155225b6290ce94fbd5610a690231312d5aa2121ad436888e3dfe/hypothesis-6.161.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e1f3d2f1b8324259cc1a149286b8856ed07447f9f28f1338452eec73708cd7e5", size = 1263030, upload-time = "2026-07-24T06:42:23.475Z" }, + { url = "https://files.pythonhosted.org/packages/13/f7/1dcbafa23a2fc07aede597e0c9ea2093378a1c05a858dfbbb3627a1a7e11/hypothesis-6.161.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c1c6c7d30c9aaa11b3e79c40a9a7ac1f559fce44853292d5383c0b1d11623658", size = 1306906, upload-time = "2026-07-24T06:42:06.2Z" }, + { url = "https://files.pythonhosted.org/packages/26/93/371966030a92748fcdef978f4ecfd69aa7b0d92e4bcdc398f5938941ef2c/hypothesis-6.161.2-cp313-cp313-win_amd64.whl", hash = "sha256:176d4f1a58f808891eec2a448889a24522002cb952df99c6fe4ab150f81a0eae", size = 655650, upload-time = "2026-07-24T06:42:51.08Z" }, + { url = "https://files.pythonhosted.org/packages/96/bf/fc502b4e92361e0ded96f77f3f40bcc6b16d0ab0511a54afb1110133d18a/hypothesis-6.161.2-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:0101112993070b1f3ba00b44ea3913e06d7b14c0c658f61e19f196aa7a36c2ce", size = 768219, upload-time = "2026-07-24T06:43:20.437Z" }, + { url = "https://files.pythonhosted.org/packages/33/f0/aa051525002a853914fd5e14ea0921ccdd02081d715cb52473f35fcb2325/hypothesis-6.161.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a8323ffde6b3c01103c69af857e085dc4d74cbde07e8f8dac297d81870f0b8dc", size = 759825, upload-time = "2026-07-24T06:42:44.901Z" }, + { url = "https://files.pythonhosted.org/packages/32/24/88b22af1f34b773542eb8c465d3609bc6e35ddf22e4598fbed9b3aa12956/hypothesis-6.161.2-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d05e9343fbf172dc409fbf2b5c8894d2fde6d0748a852b9b8ef2366292240ff", size = 1090594, upload-time = "2026-07-24T06:42:47.918Z" }, + { url = "https://files.pythonhosted.org/packages/bc/4d/b800afacd75cb89e9663178e22393e9b69289ad43ef1cc868a8d0a243482/hypothesis-6.161.2-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84f5f15959b16356534007ba4a0cc95184576639b839a311eb953b07947c9e8c", size = 1140179, upload-time = "2026-07-24T06:42:41.644Z" }, + { url = "https://files.pythonhosted.org/packages/56/89/c33695781141b78b84ce888beb6a0e6865f0fc45b1395fa4d7039d31e29a/hypothesis-6.161.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2c5e898aaf65476e4c3e2953dfb71b16112537704c9cb083e7920c2525a96d97", size = 1263351, upload-time = "2026-07-24T06:42:26.369Z" }, + { url = "https://files.pythonhosted.org/packages/98/9e/8e508005a821a46e230704c69790ab6631d6e94404c7275d5bf7f34aa8d7/hypothesis-6.161.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f733deeeae110c490610eba1c72adbf2b56f358bed3750e1a07d4c42a5ddaffa", size = 1307209, upload-time = "2026-07-24T06:42:19.493Z" }, + { url = "https://files.pythonhosted.org/packages/23/03/62301eab71ce45cf7e9c84e52a4cfcd2d51abcc81a9f733e89a171474395/hypothesis-6.161.2-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:7870df5cc41377aa76a42650be68b1cab87fd7b7a1ae1a7a83b06fc49809b6f1", size = 599729, upload-time = "2026-07-24T06:41:59.905Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a8/4e1479e2136b051adc2a141b2c26b8c8a18deda9d7c1713e816eac6b374b/hypothesis-6.161.2-cp314-cp314-win_amd64.whl", hash = "sha256:cc6bc6e1c6228ac32e7c22110eb663dc5abb844ed83eb2199f00fac004327d6d", size = 655563, upload-time = "2026-07-24T06:42:05.089Z" }, + { url = "https://files.pythonhosted.org/packages/22/8c/9c0f53d4d244cb8ad243a5dbfd1e733eaf5617f1e867c45bac15dfaa88c7/hypothesis-6.161.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f91ffa32ef1597158da5696e68f1d99ef0c23dbe286a6d4ea5d94e4f856614d5", size = 766796, upload-time = "2026-07-24T06:42:16.803Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0c/3aa7877ebb5f333ff864e1465d579cbad85eb87ab45e9d532093ebd88dd0/hypothesis-6.161.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:825feec7f819c44e02a047b7c1c6672793e584b7a659cf2fad107fa031722515", size = 758288, upload-time = "2026-07-24T06:43:16.998Z" }, + { url = "https://files.pythonhosted.org/packages/05/f0/452289f386654e22f11d9cef03a8d2931fede39f028e679ebd40b2dd9513/hypothesis-6.161.2-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8aeecadd8d0df819ccde1d00d8f2f8e5cd5d0ac196967d959b789b111be54cb1", size = 1089175, upload-time = "2026-07-24T06:42:32.859Z" }, + { url = "https://files.pythonhosted.org/packages/85/d9/9e70c36b84392b0e13ead6601032e06fdbac6898d67515140a123242a081/hypothesis-6.161.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:526663659adf7408a2e550440d16020a2f49e4e3e69ccea0a6aa23ed5ff6f6b8", size = 1139093, upload-time = "2026-07-24T06:42:08.771Z" }, + { url = "https://files.pythonhosted.org/packages/98/e5/464b33a0eab6cd8df301ec45bea759c6f66a331f59f4a2d9c600c8e0a07f/hypothesis-6.161.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:96d3609972a89cb28794014a67aab4354dfb30e65c3ea92ce5318044d69f0f47", size = 1261590, upload-time = "2026-07-24T06:42:24.876Z" }, + { url = "https://files.pythonhosted.org/packages/58/b3/e37438facec5f3c3021e2d49610d803ccbf88b594090194fc4fa7f89c49b/hypothesis-6.161.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:adc49e00f4e3245ae5372c602bab929fec10be922fdb5766c73f936a616c0948", size = 1305967, upload-time = "2026-07-24T06:42:15.575Z" }, + { url = "https://files.pythonhosted.org/packages/5b/43/6ac7a850101db2ddaf351ee9e2d649a1a2a10bdc9ce1d83a60cbb0ddc565/hypothesis-6.161.2-cp314-cp314t-win_amd64.whl", hash = "sha256:e368ebca6616dd17cd4d00ea1e03705aac850bcc4a8de322c4942a1d63a80c88", size = 655715, upload-time = "2026-07-24T06:42:40.252Z" }, ] [[package]] @@ -1082,11 +1094,11 @@ source = { git = "https://github.com/microsoft/python-type-stubs.git#d23acef5204 [[package]] name = "mistune" -version = "3.3.2" +version = "3.3.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/04/5f/007786743f962224423753b78f7d7acb0f2ade46d1604f2e0fa2bedf9020/mistune-3.3.2.tar.gz", hash = "sha256:e12ee4f1e74336e91aa1141e35f913b337c40bdf7c0cc49f21fb853a27e8b62f", size = 111284, upload-time = "2026-06-23T00:29:28.568Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/92/328a294a6de83bacb95bed01f04e0eaff4e3616ee359fc821a5dfc539b02/mistune-3.3.4.tar.gz", hash = "sha256:58b5c96d6fcb61190dfe5fae498d2b2065f99cf61e9649418fd54cf1ada86dfe", size = 121426, upload-time = "2026-07-22T05:22:30.89Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/44/43/894c2cbbcbdf53b57d1257a249811abe2ee9ab7ef76af301b40f1c054533/mistune-3.3.2-py3-none-any.whl", hash = "sha256:a678a56387d487db7368ede4647cb2ba1deff22ce61f92343e4ebe0ddfce4f2d", size = 61554, upload-time = "2026-06-23T00:29:27.088Z" }, + { url = "https://files.pythonhosted.org/packages/77/e4/288365afae98953bc01de09f686f40d8ee84578135aa7767d5d4e60b5278/mistune-3.3.4-py3-none-any.whl", hash = "sha256:ee015381e955e370962968befe1d729ab60fafb6a715ac6751763fbce38c8d4a", size = 66862, upload-time = "2026-07-22T05:22:29.419Z" }, ] [[package]] @@ -1117,11 +1129,11 @@ wheels = [ [[package]] name = "narwhals" -version = "2.23.0" +version = "2.24.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e8/ac/66ed1fc6e38a0c0f330627ec5c5d597990d6159b6712b82af0ad2c65f06c/narwhals-2.23.0.tar.gz", hash = "sha256:13e7ff5b4bb4a2f77b907c2e4d8a76e273dfc1323a3c997440a2f9fd26aed408", size = 656209, upload-time = "2026-07-01T11:21:53.278Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/1d/58946e5aab18393e793bd4add6985b95d0e01c3a2d832f38f54468b10dcd/narwhals-2.24.0.tar.gz", hash = "sha256:b5c0f684ccd9d7475b564111e319a4964abcf2baf79d3cf6b1003d06ac9b828d", size = 661143, upload-time = "2026-07-13T10:49:19.086Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/4e/afc8c31605cb8be1d3bb4438c4d979daa104dab6306cd2b87abe9c3a7299/narwhals-2.23.0-py3-none-any.whl", hash = "sha256:769e7b9ab102c93d8fa019f6b4cd1a657909b04a20bf6210e5a35aae06814ae9", size = 458938, upload-time = "2026-07-01T11:21:51.677Z" }, + { url = "https://files.pythonhosted.org/packages/7e/85/a5bfaebfd305ac18b57b0854d74e37e586809061a91fda62f0bd50c8518e/narwhals-2.24.0-py3-none-any.whl", hash = "sha256:42fdedf44e5b2ca7505630d45b4ac3058f38d8485cba9fe1652ca23152df7489", size = 461030, upload-time = "2026-07-13T10:49:17.571Z" }, ] [[package]] @@ -1311,46 +1323,39 @@ wheels = [ [[package]] name = "pandas" -version = "3.0.3" +version = "3.0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, { name = "python-dateutil" }, { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f8/87/4341c6252d1c47b08768c3d25ac487362bf403f0313ddae4a2a26c9b1b4c/pandas-3.0.3.tar.gz", hash = "sha256:696a4a00a2a2a35d4e5deb3fc946641b96c944f02230e4f76137fe35d806c4fc", size = 4651414, upload-time = "2026-05-11T18:54:29.21Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/90/62d8302883c44308c477e222c3daf7c813a34c8e96985882fbd53d964352/pandas-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:67b3b64c11910cfa29f4e94a14d3bff9ee693b6fc76055e7cad549cee0aec5fa", size = 10331071, upload-time = "2026-05-11T18:52:58.838Z" }, - { url = "https://files.pythonhosted.org/packages/7f/ae/6a6493c783a101f165e4356953ba3c74d6f77f0042fa7d753da9dfbb640c/pandas-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:39436b377d56d2a2e52d0395bdbee171f01068e99af5250509aceeb929f765c7", size = 9875690, upload-time = "2026-05-11T18:53:01.431Z" }, - { url = "https://files.pythonhosted.org/packages/62/7c/5df8e9f56c69a2769fbe9382a5ef8f2658c007e376434e1e2cbb57ad895f/pandas-3.0.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4be06d68f9ddcfc645b87534911da79a8fbffc7573c80e0edcf42a5020624d8", size = 10381634, upload-time = "2026-05-11T18:53:04.393Z" }, - { url = "https://files.pythonhosted.org/packages/99/68/1237369725aa617bb358263d535803e3053fdbc593513ec5ed9c9896b5b6/pandas-3.0.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a4eeb6830daf35a71cc09649bd823e2b542dac246cdee9614c6e4bd65028cd6a", size = 10891243, upload-time = "2026-05-11T18:53:07.643Z" }, - { url = "https://files.pythonhosted.org/packages/25/93/77d108e8af7222b4a503ebde0e30215b1c2e4f8e53a526431890f22d5586/pandas-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1928e07221f82db493cd4af1e23c1bfca524a19a4699887975bff68f49a72bfb", size = 11388659, upload-time = "2026-05-11T18:53:10.634Z" }, - { url = "https://files.pythonhosted.org/packages/d0/bd/eff5b4399f332ac386c853f6cd2bd3fa2ca0061b9f36ecd9c4d7c4265649/pandas-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51b1fe551acb77dac643c6fda86084d8d446c10fe64b06a9cc29c4cc8540e7f2", size = 11942880, upload-time = "2026-05-11T18:53:13.536Z" }, - { url = "https://files.pythonhosted.org/packages/2c/20/559ace4200982c3887d0b86bfd0d856a2143ef8ddab63cc07934951a964c/pandas-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:a82d532a3351d435432cd913edbccaf8b8e01d4dd0e5ced5a8d2e8ecd94c7e44", size = 9757091, upload-time = "2026-05-11T18:53:16.306Z" }, - { url = "https://files.pythonhosted.org/packages/3a/66/69055a09fe200f29f922a3eeec4804611900b95f52d932ece3393c3c0c19/pandas-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:275c14e0fce14a2ec20eee474aecd305478ea3c1e6f6a9d8fe219a165542717e", size = 9057282, upload-time = "2026-05-11T18:53:18.768Z" }, - { url = "https://files.pythonhosted.org/packages/57/0e/efe801b0e6811e8e650cd21b7f2608e30f08a7067e2bf6e8752b0d56ee3c/pandas-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:46997386d528eb40376ecd6b033cf4a8a1e5282580f68f43de875b78cba2199d", size = 10767016, upload-time = "2026-05-11T18:53:21.227Z" }, - { url = "https://files.pythonhosted.org/packages/ea/dc/eb55135a1d5f0f0519f28da1f609a206d2cad1f9c35c32d51e38dd7261ae/pandas-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:261e308dfb22448384b7580cf719d2f998fe2966c92893c3e77d14008af1f066", size = 10420210, upload-time = "2026-05-11T18:53:23.982Z" }, - { url = "https://files.pythonhosted.org/packages/c6/3e/b1d5d955ce33ffecb407465a60bc32769d74fcf68224b7ae67ae11d4dea4/pandas-3.0.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd1a5d1def6a46002e964510bdc67c368aa0951df5d1d9f8365336f5a1f490cd", size = 10336126, upload-time = "2026-05-11T18:53:26.731Z" }, - { url = "https://files.pythonhosted.org/packages/f5/76/a01261711ab60a22d71b862f0de20e4c504bf80457270ad8cb42110f6abc/pandas-3.0.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d72828c20c6d6e83e1e22a6a3b47b326b71664112fa9705dcbccfd7a39b62085", size = 10728051, upload-time = "2026-05-11T18:53:29.125Z" }, - { url = "https://files.pythonhosted.org/packages/e9/21/ea191195e587b18cf682e97f433f81b2d0fbe341380e80a3e0d6e4403c8e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d26cbe1fcfc12e8fd900e2454163e466b2d3af84f7c75481df7683ffc073d870", size = 11350796, upload-time = "2026-05-11T18:53:32.056Z" }, - { url = "https://files.pythonhosted.org/packages/64/69/f0eaaf54939f0e8c6768fd06be9af2cef9b36048b96dfb9e1b2c685a807e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e91cec1879ada0624fc3dc9953c5cbd60208e59c0db28f540c5d6d47502422f", size = 11799741, upload-time = "2026-05-11T18:53:34.985Z" }, - { url = "https://files.pythonhosted.org/packages/45/a4/865e0e510cae5fc2194de4db28be638952de942571ba9125934fd9c01d47/pandas-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:08d789b41f87e0905880e293cedf6197ce71fe67cc081358b1e148a491b9bd13", size = 10499958, upload-time = "2026-05-11T18:53:37.857Z" }, - { url = "https://files.pythonhosted.org/packages/86/54/effdcc3c0ff7a08037889200e148ebe94c16c4f653be078c7b3675955df1/pandas-3.0.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3650109c0f22879df8bd6179ab9ee3d7f1d1d4e7e0094a3f0032d9f51e2e64ac", size = 10336065, upload-time = "2026-05-11T18:53:41.099Z" }, - { url = "https://files.pythonhosted.org/packages/68/10/bf2d6738d72748b961a3751ab89522d58c54efc36a8e1a12161216cd45cf/pandas-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bab900348131a7db1f69a7309ef141fd5680f1487094193bcbbb61791573bf8f", size = 9926101, upload-time = "2026-05-11T18:53:43.515Z" }, - { url = "https://files.pythonhosted.org/packages/ae/e9/e35cf11c8a136e757b956f5f0efdcaa50aecde85ea055f1898dfc68262f3/pandas-3.0.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba7e08b9ac1d54569cd1e256e3668975ed624d6826f7b68df0342b012007bddb", size = 10457553, upload-time = "2026-05-11T18:53:46.394Z" }, - { url = "https://files.pythonhosted.org/packages/58/3b/1cdec6772bdbaf7b25dab360c59f03cadf05492dd724c6540af905389b07/pandas-3.0.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d71c63ae4ebdbf70209742096f1fc46a83a0613c99d4b23766cced9ff8cd62a", size = 10914065, upload-time = "2026-05-11T18:53:49.134Z" }, - { url = "https://files.pythonhosted.org/packages/c4/c2/1ef644445fcd72e3627bceec77e3560636f87ddce4ed841afe76b83b5bf9/pandas-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e3a2ec42c98ffa2565a67e08e218d06d72576d758d90facb7c00805194d8f360", size = 11459188, upload-time = "2026-05-11T18:53:52.527Z" }, - { url = "https://files.pythonhosted.org/packages/7e/49/4d8d4f42cbc9c4adc7a1870f269c02cbd6cd40d059622c06fb298addcbad/pandas-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:335f62418ed562cfc3c49e9e196375c28b729dcef8543abf4f9438e381bf3c76", size = 11982966, upload-time = "2026-05-11T18:53:55.043Z" }, - { url = "https://files.pythonhosted.org/packages/38/55/792619469bab9882d8bbd5865d45a72f6478762d04a9af4bf0d08c503e95/pandas-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:3c20a521bbb85902f79f7270c80a59e1b5452d96d170c034f207181870f97ac5", size = 9876755, upload-time = "2026-05-11T18:53:58.067Z" }, - { url = "https://files.pythonhosted.org/packages/2a/af/33c469653b0ba03b50c3a98192d4c07f0c75c66b263ceb097fce0ee97d31/pandas-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:a2d2dff8a04f3917b55ab3910c32990f8ddf7eceba114947838cefa976a68977", size = 9198658, upload-time = "2026-05-11T18:54:00.733Z" }, - { url = "https://files.pythonhosted.org/packages/a2/fa/b8c257bd76b8bd060c3a9151c1fca05e9b9c5e3af5d0f549c0356f6d143d/pandas-3.0.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0d589105b3c14645af1738ff279b2995102d8f7a03b0a66dc8d95550eb513e04", size = 10787242, upload-time = "2026-05-11T18:54:03.564Z" }, - { url = "https://files.pythonhosted.org/packages/54/eb/f19206ffb0bf1919002969aa448b4702c6594845156a6f8050674855aac3/pandas-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:13fc1e853d9e04743d11ba75a985ccbc2a317fe07d8af61e445a6fd24dacd6a6", size = 10436369, upload-time = "2026-05-11T18:54:06.311Z" }, - { url = "https://files.pythonhosted.org/packages/fd/24/c7c39fb4fe22b71a0c2d78bf0c585c600092d85f94f086d2b3b2f6ca27e2/pandas-3.0.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:819959dab7bbd0049c15623fbac4e29a191b9528160a61fb1032242d8ced2d9c", size = 10358306, upload-time = "2026-05-11T18:54:09.085Z" }, - { url = "https://files.pythonhosted.org/packages/16/ec/dd2a9eb7fa1204df88c0864164e35b228ac581062ac612ba0a67fd812e4c/pandas-3.0.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:60ae316d3fd75d1858d450d0db0103ea2be3e7d4a95ec2f064f7e2ae63f7b028", size = 10758394, upload-time = "2026-05-11T18:54:11.956Z" }, - { url = "https://files.pythonhosted.org/packages/95/6e/00c61ea8e85b4f6d8d35e11852a1a4998fc7fafc91c6a602d1cc9c972d64/pandas-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd3a518890b400d32f9023722dc9a9a5c969f00b415419a3c06c043f09bb5d7d", size = 11375717, upload-time = "2026-05-11T18:54:14.539Z" }, - { url = "https://files.pythonhosted.org/packages/31/89/8fc1c268969fac43688d65fd92e67df24bd128d53cb4d2eee534cd307399/pandas-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c39be2d709d01fa972a0cabc522389fceca4f3969332ba25a7d6c5802cf976a", size = 11828897, upload-time = "2026-05-11T18:54:17.146Z" }, - { url = "https://files.pythonhosted.org/packages/56/3b/e7d20dea247a3e6dc0bd8a6953854afbedc03951def4e7371e05e7263e25/pandas-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4db8c527972a821cf5286b40ccc57642a39bc62e62022b42f99f8a67fca8c3a1", size = 10900855, upload-time = "2026-05-11T18:54:19.72Z" }, - { url = "https://files.pythonhosted.org/packages/0f/54/68a0978d1ef8502b8492099beaa6e7a0c1b32e3b5d4f677f5810cb08711c/pandas-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b2c95f8bfc1ee412bf482605d7bfd30c12d1d26bd59fdd91efeef1d4718decb1", size = 9466464, upload-time = "2026-05-11T18:54:22.754Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/be/4f/5f3422a2afec5ffc46308b79e53291365a93748b498ac2e58bead0197916/pandas-3.0.5.tar.gz", hash = "sha256:dca3734d6ab7c906e6730f0788b0a1dbb9f2467731f9711f77995c8e9d62d712", size = 4658219, upload-time = "2026-07-22T22:19:28.819Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/09/7b95c4a0025227d6f118c4039b423412ac6a982db02864166185d812fbc7/pandas-3.0.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c1c05a767fe8e5b4fe9e1c29806829c582052eaedb9120a3da83ba3f69e24a5b", size = 10385742, upload-time = "2026-07-22T22:18:29.346Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0c/dc78fd8c4da477b4b5e8ad37295af352190d21ef63a9ee1bc071753074cc/pandas-3.0.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b86765f268b56f7e665b93bce9d5df69dee7f99e595cf8fb839483ab315942a3", size = 9932067, upload-time = "2026-07-22T22:18:31.833Z" }, + { url = "https://files.pythonhosted.org/packages/3e/71/3592c055cf44df9808550f9368ceda80ff2b224d355ef73fe251dcda1802/pandas-3.0.5-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c597ecf5616b5c420372c1d4d4c00dbbfba7398bea857dcc984347e1ea48417b", size = 10466756, upload-time = "2026-07-22T22:18:34.195Z" }, + { url = "https://files.pythonhosted.org/packages/e3/70/4363150359f95b4cb4bcbb34ca23572bb5495749a621a8f3d5a1ddfd293c/pandas-3.0.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b11c36e218331d0387cbe3a0a5f75162357a1d92d57b2b08a336ff94b19b2be", size = 10938525, upload-time = "2026-07-22T22:18:36.81Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d0/317e7a0c67c0e69fa905a0161409397a7dc2d46ff611f6ca4803352c042b/pandas-3.0.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cf52e1f61d229496da17dc7ab54acdee627357e7008fd4fecba3d0ba2937fa58", size = 11489303, upload-time = "2026-07-22T22:18:39.287Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8d/36dade89b49e4f9d5cbdbe863772581f98c0c6d78fc39ad4c557f6f2e17e/pandas-3.0.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:db172144bb56422bd157812f3b021eacc255451470b31e2c633c349490a1cfee", size = 11989004, upload-time = "2026-07-22T22:18:42.208Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ba/18c4ec8a746e177da05a9e7a7963781d8ea195780724f854601b6ebd6b78/pandas-3.0.5-cp313-cp313-win_amd64.whl", hash = "sha256:0d298e951f23016ce4699951d044ae6418dbc91bf68cefca0f77666fcbb4e5c6", size = 9826896, upload-time = "2026-07-22T22:18:44.539Z" }, + { url = "https://files.pythonhosted.org/packages/de/ec/28a57266b753799a87b8bc79e7887ac6fd981b8c6d2978a0b7e7b6bd708c/pandas-3.0.5-cp313-cp313-win_arm64.whl", hash = "sha256:66266d3442a5e8b3c90274c2b8b230bee42dd1c286bc822cc2f9f2c7e12b883e", size = 9094790, upload-time = "2026-07-22T22:18:47.468Z" }, + { url = "https://files.pythonhosted.org/packages/51/2f/cf6aae281264f4463f0875bcbb15fd2bb6d291cc535187dad1732475e4a9/pandas-3.0.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2f264fc46911cc8131a7322a16199bbf8e353d27c10bb211f5bd0c814324dc36", size = 10390034, upload-time = "2026-07-22T22:18:49.818Z" }, + { url = "https://files.pythonhosted.org/packages/06/ec/5189518c7a7659c4bdcc6b1eb32c46c6f3c86b0661ffd84143d1112c7732/pandas-3.0.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:53730687fcd161883b24e10411c06d6a4c0f2275d2faf3bb2bc25deb4ba8007c", size = 9980065, upload-time = "2026-07-22T22:18:52.249Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f1/598503ce8d7e3c35601e0747ba288c7864baae66380725bc12f13f884dfe/pandas-3.0.5-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:960d3ebcf249f75206899fcd2c6de53f736b7265759ced0d3e559df0b8b709b0", size = 10545532, upload-time = "2026-07-22T22:18:54.813Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/ceae2adf7034e07e9910299fe412e1819c4f0dd520700a888bcb03625448/pandas-3.0.5-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e94c2c5ca43bd3ca32bf64d32308887b65e5f9bfd8023ea52755107a999f93b", size = 10963120, upload-time = "2026-07-22T22:18:57.42Z" }, + { url = "https://files.pythonhosted.org/packages/66/25/86e0f4451874eb79e688deeebe3c451fec4557f8952005818d800ee8ac7e/pandas-3.0.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e819dd5f62966b481a8cb649d3299ebd886a1ea91ed5a99bf7ce77c98d18ab94", size = 11563178, upload-time = "2026-07-22T22:18:59.729Z" }, + { url = "https://files.pythonhosted.org/packages/f3/45/8643daa3b4147e433adfcccefdd0380d3aad79d86b15d8999730fe1944d5/pandas-3.0.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3c5ed2e7c06e91d340dfd091d7934f9bc82e4a36b95f647f090b9d1c9ac649da", size = 12028708, upload-time = "2026-07-22T22:19:02.164Z" }, + { url = "https://files.pythonhosted.org/packages/96/58/ad979ae617615576e8aafd569c9d4b62f1191d896e38f51d66ba06f3b89a/pandas-3.0.5-cp314-cp314-win_amd64.whl", hash = "sha256:cd8f7c6dc98527058ee6264219343f5392240a6f1bfa654fc5d79023020d0c92", size = 9951806, upload-time = "2026-07-22T22:19:04.596Z" }, + { url = "https://files.pythonhosted.org/packages/69/32/7ac03886b304049a9d2625ee88f59af760d8a93bd30ed9239bce7b9869a8/pandas-3.0.5-cp314-cp314-win_arm64.whl", hash = "sha256:5183427f5a8156d480f30333777bc978be93650a49a7c01db26adffe95b31e85", size = 9238297, upload-time = "2026-07-22T22:19:06.836Z" }, + { url = "https://files.pythonhosted.org/packages/be/ed/1d1f2ee5547d5167face2376d11c8b2a4c7bfff5a416ee7a9046891fab1e/pandas-3.0.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:303da736987d481074ca720ada325f8bd80c64ebc2d45ed79b29df3aaa4a26ca", size = 10849690, upload-time = "2026-07-22T22:19:09.391Z" }, + { url = "https://files.pythonhosted.org/packages/57/55/17e17152e98fbb0c4b1e562bc65387a2f20a80db0f4a86bf8d3a0e4248d4/pandas-3.0.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3b2801bbb049d0136f6c213eae02b5fca969384fc2064dd728d8620552aa49da", size = 10509945, upload-time = "2026-07-22T22:19:11.773Z" }, + { url = "https://files.pythonhosted.org/packages/88/90/817d44dbf83facf9556f33576d9af0a241981e7bb5c00606c0bcb5df8dda/pandas-3.0.5-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cce3a9d11d2b1f82c69a27ec1f4948a170e2c403c4bbfa8cca62e3fdebe2ef3a", size = 10392197, upload-time = "2026-07-22T22:19:14.024Z" }, + { url = "https://files.pythonhosted.org/packages/f1/da/889f00c0a6f5aa1545add70abbf01502dff87ab577adb855bd631c54d2f2/pandas-3.0.5-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef01af4d8dc6cd2c8d6c7736f149574ef93fe043811eeb5e445f2647154b5040", size = 10862726, upload-time = "2026-07-22T22:19:16.351Z" }, + { url = "https://files.pythonhosted.org/packages/bc/98/f1e934fb3c98fce859c6147c6785816c7b5b9ab7821115c5d8c4de9842b9/pandas-3.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e2759e890db96dfcffdbd9b86c3c2cb6afaf58def482820317e06163ec1066cd", size = 11414864, upload-time = "2026-07-22T22:19:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/fe/be/d448af7d657d82e1888dd8551f79c6d6fb161080b5b9752d84d910ec2319/pandas-3.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b58b1b39d46a5862e3fb18f50d1a201398619d16a0f9f73f57eea5583cf0e63c", size = 11925105, upload-time = "2026-07-22T22:19:21.515Z" }, + { url = "https://files.pythonhosted.org/packages/29/c1/ccb4238212c8c4f496c584f3044d94e0c030ed8e1d68999db46c91c2242f/pandas-3.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:1c10461f6eeb35d8f05b6184c65c8b9991663b66c46b1d559b682cb34ae7c6ea", size = 10387612, upload-time = "2026-07-22T22:19:24.257Z" }, + { url = "https://files.pythonhosted.org/packages/d2/cf/6a51b2c38980e04c279fd2fa908a1b0982064e860444acfca4ec2e2c8359/pandas-3.0.5-cp314-cp314t-win_arm64.whl", hash = "sha256:3c5015fd1730fbf883647e88068176c839c102cea883ba1769a6f4593bfc1f8c", size = 9509776, upload-time = "2026-07-22T22:19:26.694Z" }, ] [[package]] @@ -1390,16 +1395,16 @@ wheels = [ [[package]] name = "panel-material-ui" -version = "0.13.0" +version = "0.14.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "bokeh" }, { name = "packaging" }, { name = "panel" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/20/c0/9be7012b9d0c9961c1af3a916d7a2c82231ef685f483ce1296c89d1d2d92/panel_material_ui-0.13.0.tar.gz", hash = "sha256:acbe510cbe1e5b11b313d51337dc8e3ce44d6161c0272e4587dfb2a4e3b98cb6", size = 3991602, upload-time = "2026-07-01T14:27:41.36Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/18/2056a59f07ab2de3775adfba4fda807ac451482cd6765c566d7aa1c22151/panel_material_ui-0.14.0.tar.gz", hash = "sha256:486f1e163aa62c2221a5a0003208feb143b135ff563f418d1df30ce2647db665", size = 3986513, upload-time = "2026-07-10T10:43:17.743Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/e3/bc943f1139a2397d1561afea7e7fa9385907699ec34bbfd6e46032eb462c/panel_material_ui-0.13.0-py3-none-any.whl", hash = "sha256:af7e5e70b769bffc4ed02e4e40f2dc1c4156cccba1a6a0eb31b987d47c8b82c1", size = 1741599, upload-time = "2026-07-01T14:27:39.238Z" }, + { url = "https://files.pythonhosted.org/packages/36/f2/4dac7c73673dd2b824aa9c539cbcbec3fadb5b602009ac3fa634f8d00438/panel_material_ui-0.14.0-py3-none-any.whl", hash = "sha256:152e09fc0b903e37d3b45bf8d3018cc3360b9112588a80c1254c3da3f3c2f22d", size = 1732403, upload-time = "2026-07-10T10:43:15.609Z" }, ] [[package]] @@ -1511,11 +1516,11 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.10.0" +version = "4.11.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", size = 31224, upload-time = "2026-05-28T03:32:53.587Z" } +sdist = { url = "https://files.pythonhosted.org/packages/78/9b/560e4be8e26f6fd133a03630a8df0c663b9e8d61b4ade152b72005aec83b/platformdirs-4.11.0.tar.gz", hash = "sha256:0555d18370482847566ffabcaa53ad7c6c1c29f195989ae1ed634a05f76ea1e0", size = 31953, upload-time = "2026-07-21T13:09:36.565Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743, upload-time = "2026-05-28T03:32:52.175Z" }, + { url = "https://files.pythonhosted.org/packages/7d/68/d8d58938dfb1370b266a1a729e6d77a985be23689a0496498ee17b2cbf90/platformdirs-4.11.0-py3-none-any.whl", hash = "sha256:360ccded2b7fce0af0ff80cc8f5942a1c5d99b0e856033acb030bfc634709e74", size = 23247, upload-time = "2026-07-21T13:09:35.422Z" }, ] [[package]] @@ -1529,35 +1534,35 @@ wheels = [ [[package]] name = "polars" -version = "1.42.1" +version = "1.43.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "polars-runtime-32" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/27/99/fe77f10a13a778705ef05b499fc708c9a0b0a3680d9eb6bc6e1b6a6b9914/polars-1.42.1.tar.gz", hash = "sha256:2fe94f3059334650bd850ae19a9c165dcd5d9cb12cd95ea04de2201662e70e8a", size = 741532, upload-time = "2026-06-30T04:57:51.504Z" } +sdist = { url = "https://files.pythonhosted.org/packages/66/5b/5d0f0aa53c6e9a8ecbc99ff502edcf9584e5d08ab34ea407c086999103d5/polars-1.43.0.tar.gz", hash = "sha256:bb2c67553e4968c18dfe268a88ff9a5790d5c2e0b7ea7efe97640b9a90438c88", size = 749537, upload-time = "2026-07-21T04:30:25.966Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/be/6a/edd939cc6fa04b6415aaa9bf19720fc74ead81234b3d38542e0005816d4d/polars-1.42.1-py3-none-any.whl", hash = "sha256:3c0c65cdfa21a621650c4bdcbbccf93964d052fd766c3e70e84a55d961c259fd", size = 837622, upload-time = "2026-06-30T04:56:34.686Z" }, + { url = "https://files.pythonhosted.org/packages/76/28/a8eac2c1d1b2d2a4ba2eb745921616d863185d94b1afd91cbb07af9ef21a/polars-1.43.0-py3-none-any.whl", hash = "sha256:c49078b14e2d6b8ff5cc5b78b6d9638603ea5dffafb889d9204818822f55b813", size = 846493, upload-time = "2026-07-21T04:29:06.68Z" }, ] [[package]] name = "polars-runtime-32" -version = "1.42.1" +version = "1.43.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1f/59/15bcc4dac380c6d63efa5446d8317f22671cbd6c9dadd576bd17a334c45a/polars_runtime_32-1.42.1.tar.gz", hash = "sha256:4d4809e1c1b9a6611f6944f27b24abea902b5159e6b6fa262fd716e947af5afd", size = 3045460, upload-time = "2026-06-30T04:57:52.866Z" } +sdist = { url = "https://files.pythonhosted.org/packages/86/96/7e714cad082e9e6aaebb8886fcb1b0220d5c35149d4c8d3466bd7e7d581e/polars_runtime_32-1.43.0.tar.gz", hash = "sha256:5fb47a3a883402e62eab2fde5922f78c531d037aeece3640c15225f39228621e", size = 3090044, upload-time = "2026-07-21T04:30:27.185Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/62/29/16ff6e4e91d71e530d3581f45e342a9cc35072ac6b31dcbc2fa33de2569e/polars_runtime_32-1.42.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:bbdc26d68ee5b23b0ce227fa0599220aa35b77c826b6b0a6b2d8e7f6c1c36974", size = 53117325, upload-time = "2026-06-30T04:56:37.972Z" }, - { url = "https://files.pythonhosted.org/packages/04/8e/4f8296fcfd1347f1351342fecf13bf2430d7efbae2f1f45964ec7930a99e/polars_runtime_32-1.42.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:f6c0288be940b607dc4a7476c01e67fb6bbee93f5f1dd42c64970274c71008ba", size = 47446251, upload-time = "2026-06-30T04:56:41.459Z" }, - { url = "https://files.pythonhosted.org/packages/88/2e/0d66a7deadc453b890c3391034ca8ab4b05d0beaebbb92a7d65199fba61b/polars_runtime_32-1.42.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:635d9dbcae2302ae223afb395d5cd220bffa61a53d0ab6871d17c8bc830101cf", size = 51359402, upload-time = "2026-06-30T04:56:44.595Z" }, - { url = "https://files.pythonhosted.org/packages/5d/10/ffb85fa380bc9c9000dc35f40f44954dde49023018501c54faab94b3a39e/polars_runtime_32-1.42.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d059e8e53cc114ff82f9bd791fd341dc53534a2c745e6f6aa37594c3a93f01fe", size = 57302723, upload-time = "2026-06-30T04:56:47.609Z" }, - { url = "https://files.pythonhosted.org/packages/c2/63/ca50adc62e44224ca5c622a842ba6f35ee87d1d40ef0df7ea2ed6c6edb08/polars_runtime_32-1.42.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f91f0b13588324905682809d270e1de5f1990c908721c8527657d77a044c9919", size = 51515673, upload-time = "2026-06-30T04:56:50.88Z" }, - { url = "https://files.pythonhosted.org/packages/63/c3/08fbbf38deaa17bf34a601d327cb7451074098673c78b7c1a8538dde9794/polars_runtime_32-1.42.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8b8bf972d99d48aaaa2582e2bce966a6f43bc815bd8725d15f5cab9e2fb15d17", size = 55217259, upload-time = "2026-06-30T04:56:53.834Z" }, - { url = "https://files.pythonhosted.org/packages/d1/0e/51db89361668fe077a835fc579277f824ba526e7daf7b94d23d25439e0d0/polars_runtime_32-1.42.1-cp310-abi3-win_amd64.whl", hash = "sha256:e9364c26da389a8b7339e4d29e20a3d12af730247e6ed3b7804bddce2477f428", size = 52715432, upload-time = "2026-06-30T04:56:57.109Z" }, - { url = "https://files.pythonhosted.org/packages/76/c5/2fb8592d691bd114de25d9c84300b23541dca7060eac11d7b4bed0327786/polars_runtime_32-1.42.1-cp310-abi3-win_arm64.whl", hash = "sha256:7051226e6b42ffc395a7a9190377cd28649fbfb991b8f85c6271f4e1cfb736fb", size = 46718300, upload-time = "2026-06-30T04:56:59.855Z" }, + { url = "https://files.pythonhosted.org/packages/0f/14/9b1f5eb1c5104ba1ceb380a7308ffcd979f3ce1df86e76293062698f2ff1/polars_runtime_32-1.43.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:6707193d30a7135bce0424304f76d8145270527444097548e794ad8d26823b70", size = 53059463, upload-time = "2026-07-21T04:29:09.194Z" }, + { url = "https://files.pythonhosted.org/packages/ca/14/73d77d1c0c928eb599d9516d874af0cd2b6225201e1327a6c4857e6776d0/polars_runtime_32-1.43.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:78ca2f97740b2a6beb36eb112749280b5e08750c60f53c08d2feffebdba9d35a", size = 47499586, upload-time = "2026-07-21T04:29:13.229Z" }, + { url = "https://files.pythonhosted.org/packages/80/b1/98278fa796f93d0975fd3fe1d4ab4031707d4a9f1da44c21c29996b62c73/polars_runtime_32-1.43.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffa99bb2c7ee0a9392ae50b350af0ed17acf0519d15c75fe223021798566174", size = 51326702, upload-time = "2026-07-21T04:29:16.084Z" }, + { url = "https://files.pythonhosted.org/packages/ab/7d/24ae73389aac03296925973c4e2cbe2e4982e859b9fad69eb1a72b9026fa/polars_runtime_32-1.43.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ecc8feaf04de5989a29245885921db612ef1ce9065e5cb6ec37495acfa55bba", size = 57266705, upload-time = "2026-07-21T04:29:19.288Z" }, + { url = "https://files.pythonhosted.org/packages/5c/49/2026be1f7b51242ad62e08b20728462558e161fb84b564e5b986f2b38664/polars_runtime_32-1.43.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:01e1471a5ee161a969c7a96991d8e5d20b97a0b7fe075df9c7a01f52a49c5ac2", size = 51484129, upload-time = "2026-07-21T04:29:22.639Z" }, + { url = "https://files.pythonhosted.org/packages/db/e2/047c7695f08a9b18614c0e1ec5e70a754455542a21a6d52955f7f05c6268/polars_runtime_32-1.43.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9cd8b813afe67d59e87027dedcaf5c6a06fe602472fd74e1a49f4bafea47259c", size = 55169401, upload-time = "2026-07-21T04:29:25.902Z" }, + { url = "https://files.pythonhosted.org/packages/92/dc/bfd2533c487563c7a21ab7d7af3d78f820b0236e14dc5ee63d46188cd275/polars_runtime_32-1.43.0-cp310-abi3-win_amd64.whl", hash = "sha256:41a75fb3cb4cc574eb21801383578f75cfc374597c22322ef457ab7bac8a3301", size = 52541527, upload-time = "2026-07-21T04:29:29.162Z" }, + { url = "https://files.pythonhosted.org/packages/46/d7/5c47f1bf57479d1671669af9c421f9abf4986b2ddaa64c177244ff811de0/polars_runtime_32-1.43.0-cp310-abi3-win_arm64.whl", hash = "sha256:c285e598dd91e08560e519275b8b8108adbafb438d218a175ebe073dbc2027fb", size = 46552281, upload-time = "2026-07-21T04:29:32.224Z" }, ] [[package]] name = "pre-commit" -version = "4.6.0" +version = "4.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cfgv" }, @@ -1566,9 +1571,9 @@ dependencies = [ { name = "pyyaml" }, { name = "virtualenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8e/22/2de9408ac81acbb8a7d05d4cc064a152ccf33b3d480ebe0cd292153db239/pre_commit-4.6.0.tar.gz", hash = "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9", size = 198525, upload-time = "2026-04-21T20:31:41.613Z" } +sdist = { url = "https://files.pythonhosted.org/packages/25/3a/ddb78f32a0814e66b18a099377a106a2dcdce92d86a034d69d65df9b256e/pre_commit-4.6.1.tar.gz", hash = "sha256:03e809865c7d178b9979d06c761fcbfe6808fdaded8581a745bb110e52050421", size = 198646, upload-time = "2026-07-21T20:56:58.225Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", size = 226472, upload-time = "2026-04-21T20:31:40.092Z" }, + { url = "https://files.pythonhosted.org/packages/fb/49/bc925106abcdac498074f2cbe6137e94e09f418dd2b7775df5b577dc0313/pre_commit-4.6.1-py2.py3-none-any.whl", hash = "sha256:0e3b2942510d1fb34eec167a3ec57331bf8442122f1153a9fb8b58f5c49b2717", size = 226186, upload-time = "2026-07-21T20:56:57.064Z" }, ] [[package]] @@ -1864,15 +1869,15 @@ wheels = [ [[package]] name = "python-discovery" -version = "1.4.3" +version = "1.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, { name = "platformdirs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/26/8b004cc36f430345136f6f00fa1aa9ed596c8ed1e8504625fa79522ff39c/python_discovery-1.4.3.tar.gz", hash = "sha256:ad57d7045a862460d4a235986c33f13ed707d3aeb9153fa47eb7dfd0d4673289", size = 70438, upload-time = "2026-07-03T13:21:51.621Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/51/276f964496a5714ab9f320896195639086881c2b39c03b5ad13de84acbb8/python_discovery-1.5.0.tar.gz", hash = "sha256:3e014c6327154d3dda27939a9a0dc9c5c000439f1906d3f303b48f984bd2ecef", size = 72483, upload-time = "2026-07-21T13:14:14.641Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/28/78/9b77ecb4644d1bbea94d29abf78f21c47eca6eb79e9745b702ec0bed2e19/python_discovery-1.4.3-py3-none-any.whl", hash = "sha256:b6e1e4a7d9e3f6948c39746ffe8218225162d738ba39d05ab1d2f6c1cac4878c", size = 33885, upload-time = "2026-07-03T13:21:50.174Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7b/14882602ddee241d7984a742fcb423cb4a30fb0d6efc546ac3129fba475a/python_discovery-1.5.0-py3-none-any.whl", hash = "sha256:70c4fc61b4e7404e44f01d6fc44a715c4d685ca6cea83d295922f05891877c98", size = 34205, upload-time = "2026-07-21T13:14:13.398Z" }, ] [[package]] @@ -2095,27 +2100,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.20" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/dc/35b341fc554ba02f217fc10da57d1a75168cfbcf75b0ef2202176d4c4f2d/ruff-0.15.20.tar.gz", hash = "sha256:1416eb04349192646b54de98f146c4f59afe37d0decfc02c3cbbf396f3a28566", size = 4755489, upload-time = "2026-06-25T17:20:37.578Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/94/d9/2d5014f0253ba541d2061d9fa7193f48e941c8b21bb88a7ff9bbe0bd0596/ruff-0.15.20-py3-none-linux_armv6l.whl", hash = "sha256:00e188c53e499c3c1637f73c91dcf2fb56d576cab76ce1be50a27c4e80e37078", size = 10839665, upload-time = "2026-06-25T17:19:44.702Z" }, - { url = "https://files.pythonhosted.org/packages/c6/d3/ac1798ba64f670698867fcfc591d50e7e421bef137db564858f619a30fcf/ruff-0.15.20-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9ebd1fd9b9c95fc0bd7b2761aebec1f030013d2e193a2901b224af68fe47251b", size = 11208649, upload-time = "2026-06-25T17:19:48.787Z" }, - { url = "https://files.pythonhosted.org/packages/47/47/d3ac899991202095dfcf3d5176be4272642be3cf981a2f1a30f72a2afb95/ruff-0.15.20-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5b16cdd67ca108185cd36dce98c576350c03b1660a751de725fb049193a0632", size = 10622638, upload-time = "2026-06-25T17:19:51.354Z" }, - { url = "https://files.pythonhosted.org/packages/33/13/4e043fe30aa94d4ff5213a9881fc296d12960f5971b234a5263fdc225312/ruff-0.15.20-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3413bb3c3d2ca6a8208f1f4809cd2dca3c6de6d0b491c0e70847672bde6e6efd", size = 10984227, upload-time = "2026-06-25T17:19:54.044Z" }, - { url = "https://files.pythonhosted.org/packages/76/e6/92e7bf40388bc5800073b96564f56264f7e48bfd1a498f5ced6ae6d5a769/ruff-0.15.20-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd7ec42b3bb3da066488db093308a69c4ac5ee6d2af333a86ba6e2eb2e7dd44b", size = 10622882, upload-time = "2026-06-25T17:19:57.037Z" }, - { url = "https://files.pythonhosted.org/packages/13/7a/43460be3f24495a3aa46d4b16873e2c4941b3b5f0b00cf88c03b7b94b339/ruff-0.15.20-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1a36ad0eb77fba9aabfb69ede54de6f376d04ac18ebea022847046d340a8267", size = 11474808, upload-time = "2026-06-25T17:20:00.357Z" }, - { url = "https://files.pythonhosted.org/packages/27/a0/f37077884873221c6b33b4ab49eb18f9f88e54a16a25a5bca59bef46dd66/ruff-0.15.20-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b6df3b1e4610432f0386dba04d853b5f08cbbc903410c6fcc02f620f05aff53c", size = 12293094, upload-time = "2026-06-25T17:20:03.446Z" }, - { url = "https://files.pythonhosted.org/packages/a6/74/165545b60256a9704c21ac0ec4a0d07933b320812f9584836c9f4aca4292/ruff-0.15.20-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e89f198a1ea6ef0d727c1cf16088bc91a6cb0ab947dedc966715691647186eae", size = 11526176, upload-time = "2026-06-25T17:20:06.301Z" }, - { url = "https://files.pythonhosted.org/packages/86/b1/a976a136d40ade83ce743578399865f57001003a409acadc0ecbb3051082/ruff-0.15.20-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309809086c2acb67624950a3c8133e80f32d0d3e27106c0cd60ff26657c9f24b", size = 11520767, upload-time = "2026-06-25T17:20:09.191Z" }, - { url = "https://files.pythonhosted.org/packages/19/0f/f032696cb01c9b54c0263fa393474d7758f1cdc021a01b04e3cbc2500999/ruff-0.15.20-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2d2374caa2f2c2f9e2b7da0a50802cfb8b79f55a9b5e49379f564544fbf56487", size = 11500132, upload-time = "2026-06-25T17:20:13.602Z" }, - { url = "https://files.pythonhosted.org/packages/4b/f4/51b1a14bc69e8c224b15dab9cce8e99b425e0455d462caa2b3c9be2b6a8e/ruff-0.15.20-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a1ed17b65293e0c2f22fc387bc13198a5de94bf4429589b0ff6946b0feaf21a3", size = 10943828, upload-time = "2026-06-25T17:20:16.635Z" }, - { url = "https://files.pythonhosted.org/packages/71/4b/fe267640783cd02bf6c5cc290b1df1051be2ec294c678b5c15fe19e52343/ruff-0.15.20-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f701305e66b38ea6c91882490eb73459796808e4c6362a1b765255e0cdcd4053", size = 10645418, upload-time = "2026-06-25T17:20:19.4Z" }, - { url = "https://files.pythonhosted.org/packages/b0/c0/a65aa4ec2f5e87a1df32dc3ec1fede434fe3dfd5cbcf3b503cafc676ab54/ruff-0.15.20-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b9c0c367ad8e5d0d5b5b8537864c469a0a0e55417aadfbeca41fa61333be9f4", size = 11211770, upload-time = "2026-06-25T17:20:22.033Z" }, - { url = "https://files.pythonhosted.org/packages/5a/a4/0caa331d954ae2723d729d351c989cb4ca8b6077d5c6c2cb6de75e98c041/ruff-0.15.20-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:01cc00dd58f0df339d0e902219dd53990ea99996a0344e5d9cc8d45d5307e460", size = 11618698, upload-time = "2026-06-25T17:20:25.259Z" }, - { url = "https://files.pythonhosted.org/packages/10/9b/5f14927848d2fd4aa891fd88d883788c5a7baba561c7874732364045708c/ruff-0.15.20-py3-none-win32.whl", hash = "sha256:ed65ef510e43a137207e0f01cfcf998aeddb1aeeda5c9d35023e910284d7cf21", size = 10857322, upload-time = "2026-06-25T17:20:28.612Z" }, - { url = "https://files.pythonhosted.org/packages/fa/f0/fe47c501f9dea92a26d788ff98bb5d92ed4cb4c88792c5c88af6b697dc8e/ruff-0.15.20-py3-none-win_amd64.whl", hash = "sha256:a525c81c70fb0380344dd1d8745d8cc1c890b7fc94a58d5a07bd8eb9557b8415", size = 11993274, upload-time = "2026-06-25T17:20:31.871Z" }, - { url = "https://files.pythonhosted.org/packages/d7/2b/9555445e1201d92b3195f45cdb153a0b68f24e0a4273f6e3d5ab46e212bb/ruff-0.15.20-py3-none-win_arm64.whl", hash = "sha256:2f5b2a6d614e8700388806a14996c40fab2c47b819ef57d790a34878858ed9ca", size = 11343498, upload-time = "2026-06-25T17:20:35.03Z" }, +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/94/1e5e4967626faf12fa56999cd6222dff6992ceb086ad7945756baf70c7a7/ruff-0.16.0.tar.gz", hash = "sha256:e460aafd5495ec89efaa6ced2e4a9a581116451e1c88b9d37ef497e0f8e93982", size = 4790557, upload-time = "2026-07-23T19:11:30.981Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/81/1c8818fee7ce1a04cd7d1b3172e0a8f8e4f1dc4feb7fc390e16daa8af323/ruff-0.16.0-py3-none-linux_armv6l.whl", hash = "sha256:e5115729eb08c585e5121978ba5d5b60caeae394ce21b9fb5e6cd33a1c6c9b1e", size = 10754633, upload-time = "2026-07-23T19:10:46.415Z" }, + { url = "https://files.pythonhosted.org/packages/23/df/beaf59c09d68db84304d555f188b276a77132a5d5b0b67a5c762aa143628/ruff-0.16.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3c954b1d580bfa035b41654f7858cc7e71d5fc3ac5b723dd62bd9133830ed522", size = 10969164, upload-time = "2026-07-23T19:10:50.271Z" }, + { url = "https://files.pythonhosted.org/packages/42/ce/741cd197496a1abbf51352710fd15ed995d2a2be87189c1da26a450d6e83/ruff-0.16.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e01c21d10eb1b29f47b7454e1f4056db9a3f0260c646aa88457c610291db9f81", size = 10488846, upload-time = "2026-07-23T19:10:52.639Z" }, + { url = "https://files.pythonhosted.org/packages/52/2a/a2db8e88cade358f5cdcb05674a917751074109315d014eb6352d9a893f7/ruff-0.16.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e364e5ed22ed8dc05082fd78e35308618260907ac2d3c1d637b2e682415b6c9", size = 10889729, upload-time = "2026-07-23T19:10:54.89Z" }, + { url = "https://files.pythonhosted.org/packages/42/65/62a771694ebd63029dc953e27dbad40e1588bd4860ff9fe881018fddaa49/ruff-0.16.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d327b8fc113a1d4421a04f3839d3752057c8dd1ee320223a6f3f52d04ada462a", size = 10568275, upload-time = "2026-07-23T19:10:56.993Z" }, + { url = "https://files.pythonhosted.org/packages/3f/e2/ced249fe8af5f086c5c58cc21cc3356d50f32f7401c5df87050c999620a7/ruff-0.16.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9b50c55e263103586b3dcf5f73d479eb8cb5fdb6098fec59a62891dab653717", size = 11385112, upload-time = "2026-07-23T19:10:59.615Z" }, + { url = "https://files.pythonhosted.org/packages/87/0b/05154977a8fd69eeb6c103271f55403bfd8711f5c0f8ed07489d95a504e7/ruff-0.16.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ff4a79ce3ec0172f3241943835de1c4cb4e2dcd07f0f8c2d02603dbbbee4b17", size = 12207008, upload-time = "2026-07-23T19:11:02.154Z" }, + { url = "https://files.pythonhosted.org/packages/fb/29/98225831a3a1eab0e02f4acc6ca6559a98611dcc68b6965ff4b7234627c1/ruff-0.16.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e95c448fca1fb2a18372a9440926c5a6ee789639bb975c72e7ae6d0b04218ab4", size = 11650842, upload-time = "2026-07-23T19:11:04.557Z" }, + { url = "https://files.pythonhosted.org/packages/91/66/6bd3cf90500653d55dc0ffc8507aa8300bd49d0214b2e8cb4d3fef2943ba/ruff-0.16.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f11a8d11010301d0a398a2fdef67691feca7294da6aef55e2150e8fa2cd520b", size = 11400718, upload-time = "2026-07-23T19:11:09.233Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a2/a54eb4eae05d66364050a5d3b8a9c5ef88196531b3cbe7109d873f87f819/ruff-0.16.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:48044c678e9cb8698246c99b14aaccfa6601dea7379eb48a6f8f73f7a6d86cd0", size = 11426177, upload-time = "2026-07-23T19:11:11.994Z" }, + { url = "https://files.pythonhosted.org/packages/1a/be/16e3eea4b2a478a496919f5e36f17c4559e54620bd3bbac5d6affa068006/ruff-0.16.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7aa0959bad8eb8bef50340154fc9b58678dae31fa4293afa38b44b6e552c0213", size = 10856126, upload-time = "2026-07-23T19:11:14.221Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/252eb8b868a16eec7257c14f504f77537e734b2d69c762e639e588e304a3/ruff-0.16.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:28ea2b7df8ebf7f9da6b7d47b230ab48f387c0a29be3b474c4d0740e197bb9af", size = 10571208, upload-time = "2026-07-23T19:11:16.378Z" }, + { url = "https://files.pythonhosted.org/packages/21/09/817a482f542f7570cbb4554b26e896610c7114f539b1d9e2d2145bf6bef6/ruff-0.16.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:33a3dfac8c35f81498dea9181bccc2f4c4bc8f1521a1dd9406e77643e0f0fb09", size = 11063329, upload-time = "2026-07-23T19:11:19.173Z" }, + { url = "https://files.pythonhosted.org/packages/2e/23/9403c180ca1cb9b1f7335f5c3e5305c09d49ea5b345196682a36028bde4a/ruff-0.16.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a5237a0bda500d30d81b8e07a6973a5cbc772864cbf746ae2f4e8a2e01c9f4ed", size = 11489751, upload-time = "2026-07-23T19:11:21.74Z" }, + { url = "https://files.pythonhosted.org/packages/b2/1d/1b2ef7bcde851c78d7f17f1cca13fd6dc695fc4b3d6197941e72cae5b132/ruff-0.16.0-py3-none-win32.whl", hash = "sha256:7fab76fa065c873f41ff744347c6e77bcc3dfec4bcc754dc26b63d23c0f7f5fb", size = 10785885, upload-time = "2026-07-23T19:11:23.947Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a3/d5e4ef7a56be3f928ffb90b94c25ba7d3cb9c7fe0736aeaaedf361770712/ruff-0.16.0-py3-none-win_amd64.whl", hash = "sha256:429c117f022bf481fabd9d551e7a3952b24c65e6ef44337ea09d90bebef14472", size = 11923141, upload-time = "2026-07-23T19:11:26.409Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9a/8415f2657cbe200f41a4531ccededf135505a92d4a012229121f885b26f9/ruff-0.16.0-py3-none-win_arm64.whl", hash = "sha256:14296fedcd2705c77ab8235439278bbb38f285cf7da5528b00b3e330c3d4872d", size = 11273407, upload-time = "2026-07-23T19:11:28.705Z" }, ] [[package]] @@ -2147,11 +2152,11 @@ wheels = [ [[package]] name = "soupsieve" -version = "2.8.4" +version = "2.9.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/47/2c/0a5f6f8ee0d5589e48c7640213ed5175d52cf540a06725b628cc1a45d6ce/soupsieve-2.8.4.tar.gz", hash = "sha256:e121fd02e975c695e4e9e8774a5ee35d74714b59307868dcc5319ad2d9e3328e", size = 121110, upload-time = "2026-05-24T13:55:57.154Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d9/38/e12680bbe6b4f8f3d17adcaf38d26850aa756c85cf4a80e79fc12a018fe8/soupsieve-2.9.1.tar.gz", hash = "sha256:c33e6605bbc71dd628b00c632d58ae607c22bade247e52553928f83bbb75b4ba", size = 122261, upload-time = "2026-07-21T16:57:17.452Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl", hash = "sha256:e7e6b0769c8f51ed59acab6e994b00621096cfb1c640a7509295987388fbaf65", size = 37304, upload-time = "2026-05-24T13:55:55.406Z" }, + { url = "https://files.pythonhosted.org/packages/0f/2c/437fe806897c2d6cfdc3ee43a18da8bf8e568530a4ae9bac781541ca9896/soupsieve-2.9.1-py3-none-any.whl", hash = "sha256:4f4477399246b7a0c720a88ca2454b11cd6bb9ae4c9d170140786e916776c14c", size = 37404, upload-time = "2026-07-21T16:57:16.421Z" }, ] [[package]] @@ -2201,14 +2206,14 @@ wheels = [ [[package]] name = "sphinx-autodoc-typehints" -version = "3.12.1" +version = "3.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "sphinx" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/de/ae/8ad5e79a0f7beac5e5810b2a8d0a1c6c63c795ba265a81970070abf15f32/sphinx_autodoc_typehints-3.12.1.tar.gz", hash = "sha256:4bbf6f6b907586d3b2a3ae2b23e0f86be939f19a7449e917d304df57ff9674d1", size = 84421, upload-time = "2026-07-03T13:52:09.056Z" } +sdist = { url = "https://files.pythonhosted.org/packages/22/99/e5a23a7bc3f9b6b799dbf72ab3955cdc0b2382803b5d11eaafcc76621662/sphinx_autodoc_typehints-3.13.0.tar.gz", hash = "sha256:59eb4fe26f71ccd7a8f10caadddcb3c3b31df7016f91bda546da2a333bae4e12", size = 85285, upload-time = "2026-07-21T13:10:44.981Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/a2/d5964523f0c98e39ef608a09f3de23032aa32fbe3e98a262e2112c39f985/sphinx_autodoc_typehints-3.12.1-py3-none-any.whl", hash = "sha256:932472c9cc160e6a0dc34eca13d3d6a9823ed6e6dc505d7c12f6963c983cf8f6", size = 42950, upload-time = "2026-07-03T13:52:07.696Z" }, + { url = "https://files.pythonhosted.org/packages/39/1c/4b3c9b1a3130b9d717f517351ce8325a62fad7c4f846cc324025c22a43c6/sphinx_autodoc_typehints-3.13.0-py3-none-any.whl", hash = "sha256:d71c388c865f3bedc1f32416febb0f8886b3051a4cd8bd8677e64b8b65c9b475", size = 43440, upload-time = "2026-07-21T13:10:43.793Z" }, ] [[package]] @@ -2383,14 +2388,14 @@ wheels = [ [[package]] name = "tqdm" -version = "4.68.4" +version = "4.69.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ae/5f/57ff8b434839e70dab45601284ea413e947a63799891b7553e5960a793a8/tqdm-4.68.4.tar.gz", hash = "sha256:19829c9673638f2a0b8617da4cdcb927e831cd88bcfcb6e78d42a4d1af131520", size = 792418, upload-time = "2026-07-07T09:58:18.369Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dd/84/da0e5038228fa34dfd77c5026b173ed035d2a3ba31f1077590c013de2bff/tqdm-4.69.1.tar.gz", hash = "sha256:2be21080a0ce17e902c2f1baeb6a74bf551b67bbdfa4bc0109fad471d0b4cb0d", size = 793046, upload-time = "2026-07-24T14:22:02.08Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/22/2a/5e5e750890ada51017d18d0d4c30da696e5b5bd3180947729927628fc3cb/tqdm-4.68.4-py3-none-any.whl", hash = "sha256:5168118b2368f48c561afda8020fd79195b1bdb0bdf8086b88442c267a315dc2", size = 676612, upload-time = "2026-07-07T09:58:16.256Z" }, + { url = "https://files.pythonhosted.org/packages/01/50/5817619a0fca56aff06383dbfde7ae017b3ca383915b3f1e4713164273cf/tqdm-4.69.1-py3-none-any.whl", hash = "sha256:0a654b96f7a2660cceb615b56f307ec2bef96c515409014a429a561981ab52b4", size = 675452, upload-time = "2026-07-24T14:22:00.048Z" }, ] [[package]] @@ -2404,27 +2409,27 @@ wheels = [ [[package]] name = "ty" -version = "0.0.56" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/55/07/fb29aea5235b0aa8ecfc4d1cc6ddf9fba8b863d67d96c6d345694d644c43/ty-0.0.56.tar.gz", hash = "sha256:84d114dc3796361c0fc72945016eabd74d46b9ee64f198cb0e485719704681e5", size = 6050123, upload-time = "2026-07-01T16:44:56.036Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/48/bce79e7ca5c1cc529d3e0d37ddd1121aea4b68a4f749974ad1cc77161871/ty-0.0.56-py3-none-linux_armv6l.whl", hash = "sha256:186d4a53e15747c947e1ec3d7eec8e345d8e40a1ca10e634c585db52497e87dd", size = 11643066, upload-time = "2026-07-01T16:44:18.374Z" }, - { url = "https://files.pythonhosted.org/packages/80/d1/22555d8a1d719661f10050f3865d877bbf497da908961c75fe22217dd18a/ty-0.0.56-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aae1a980fd9535da0469b7ba2b2e1b54a907743a5e0f442dd57eee9f5bfd034c", size = 11407487, upload-time = "2026-07-01T16:44:20.956Z" }, - { url = "https://files.pythonhosted.org/packages/cf/2d/b3b7a74ce8bc59ef48843ad80179bb0d9598bbd6cfc0d11d519bdf6b1352/ty-0.0.56-py3-none-macosx_11_0_arm64.whl", hash = "sha256:afd3058c0a6c5f241e814734f133008c93ee805f61c9cf4ce7412b8822b5d9ad", size = 10962270, upload-time = "2026-07-01T16:44:22.959Z" }, - { url = "https://files.pythonhosted.org/packages/64/ac/6c2fd7de0304a8a7218a756af74f7e62a5e8540fdb175e0a869e51042345/ty-0.0.56-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:058b52f7a823ac13aae3cae30809dd6b5145794b64d8478f9ef38c75d79b4483", size = 11471406, upload-time = "2026-07-01T16:44:25.327Z" }, - { url = "https://files.pythonhosted.org/packages/50/b6/11d861156861c03c7726b74558f9a0e0092661aff83a4fda1279df28c425/ty-0.0.56-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2c66e00c1522add1f2bbdd2e45828c953b35c306b7bef03ec9169c75a63699a0", size = 11445612, upload-time = "2026-07-01T16:44:27.531Z" }, - { url = "https://files.pythonhosted.org/packages/fb/ba/09df108582090f3c0770ec4bc8675affed60248f6793a78d909be16211d9/ty-0.0.56-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40903d71c669a30691b5a5d5728056c7877a1bd6be4f233a38883a8b28cf34d7", size = 12093889, upload-time = "2026-07-01T16:44:29.548Z" }, - { url = "https://files.pythonhosted.org/packages/d7/f7/dbb4b4ccb69cd64c209ae55b1ab788ace8222c2bc1f6845be9e7cbedbf25/ty-0.0.56-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:63fe3947fe0c46c69a7d950e6832ee70a9ec17321fefbff3d2e3c20baf9e5bd0", size = 12666337, upload-time = "2026-07-01T16:44:31.586Z" }, - { url = "https://files.pythonhosted.org/packages/86/e9/73f903fe4a3d9ea02f26f57c1eb07e3b1029ec92b0e8c2364718893440e3/ty-0.0.56-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71a0c1a72f9854532e710e119b6871ffe4542c8a65146f1f65dcd78fecd885b4", size = 12280247, upload-time = "2026-07-01T16:44:33.637Z" }, - { url = "https://files.pythonhosted.org/packages/d6/90/cebd222495832f1a00dcd321ba25f3cab804221a4991b992c2178bec68ee/ty-0.0.56-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70d1665596494e24d8ebd198438872b5a56ec3cae5f2bcf6c673be797acc4e3c", size = 11991107, upload-time = "2026-07-01T16:44:36.122Z" }, - { url = "https://files.pythonhosted.org/packages/b7/07/8f7337a07250f42d975cdb6decf47fc5b421e6c7da5e3e7be1e85f63a7e5/ty-0.0.56-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:778f99e51558afc1dbbe48ee38ab6aae7b31390ed8c1a1ef1499b295e9f1e82f", size = 12298970, upload-time = "2026-07-01T16:44:38.243Z" }, - { url = "https://files.pythonhosted.org/packages/3c/b9/a52cd59034a48f5f18c6b155cc2cc36861d874b6d0af204b12c898024c3d/ty-0.0.56-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:867bc5708e0066bb4ff6c7db524bd5deea2676c62bfe71d3303138b3be850af0", size = 11425683, upload-time = "2026-07-01T16:44:40.473Z" }, - { url = "https://files.pythonhosted.org/packages/1d/2e/48e42d33357d52eefb695c0c3fcfc96879b73668a7447d1d1e0ad774fedc/ty-0.0.56-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a6012f4189c928edb330a37deb9930f982380bd4aa7c4b8e0428eec9651c7551", size = 11469258, upload-time = "2026-07-01T16:44:42.513Z" }, - { url = "https://files.pythonhosted.org/packages/d5/01/ad1b4138be1e3fa97863af3925aa2134f17a593240c35dc38c3429fb5ad1/ty-0.0.56-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8ee83de1a7ff4cc32837ec06134ce391d441bc5b35ecd8d3cfe053f120f3e4c1", size = 11758736, upload-time = "2026-07-01T16:44:44.567Z" }, - { url = "https://files.pythonhosted.org/packages/09/34/9d81967ff240eaa57e9249728ef7b7790747cf6d3c9a98ec86b2cfdcc8ee/ty-0.0.56-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:62619b3b0e2c6248ef30d3f0e2f2217ae9893040585be07f32324242f197cd6f", size = 12100242, upload-time = "2026-07-01T16:44:46.584Z" }, - { url = "https://files.pythonhosted.org/packages/c3/36/f51d4666d2de6cf33c1f3a1fcc4bb6b70b197dd6ceaa491eef71d78fe8e8/ty-0.0.56-py3-none-win32.whl", hash = "sha256:b30687bb5cd9729d34c889a289edf32770388d9bb05243e534e723fb45e0381b", size = 11093759, upload-time = "2026-07-01T16:44:49.171Z" }, - { url = "https://files.pythonhosted.org/packages/5e/b4/8fb5d4acfa4afb152245b20fa263069a7547bd1f8e4bfca4eda280c897d7/ty-0.0.56-py3-none-win_amd64.whl", hash = "sha256:ad4c8c47b6f4e3f9ed3fc0b1a5d650088d229e17dd8f63c1826d6bbe94cc3235", size = 12100327, upload-time = "2026-07-01T16:44:51.26Z" }, - { url = "https://files.pythonhosted.org/packages/b8/fc/6a183e71edde90d0c35c2303f23f7a45b6891d1a2c45daf7b8f869831e19/ty-0.0.56-py3-none-win_arm64.whl", hash = "sha256:57538f273d444a5f1293fa7860e967178afe3917611fc5eff16b64e1204fe0d6", size = 11538780, upload-time = "2026-07-01T16:44:53.8Z" }, +version = "0.0.63" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/ce/cbeaa5c7576fec643609dfbf200d59493523b1cc0481d4e7a5effcbf0630/ty-0.0.63.tar.gz", hash = "sha256:c2f66439393b3acac69306c117d4ae44638ce5fffa4a20c21046e85bd473359f", size = 6280695, upload-time = "2026-07-23T11:41:39.845Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/e4/d17a8e113ab15c692fe6fb9c422112d4bee7e829d80ced35826b45d98d98/ty-0.0.63-py3-none-linux_armv6l.whl", hash = "sha256:9a4ef7782e3af314fb63d006bec5ac3025bd70fee774b4dcfb5e44e8564f1994", size = 12056302, upload-time = "2026-07-23T11:41:03.5Z" }, + { url = "https://files.pythonhosted.org/packages/be/0b/357234c815dc4bfcc88c3f860aa0983fe4228c8aa2a5bb16c35ee08a94af/ty-0.0.63-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:01eab0ab70d51ad10298aa2d4b058b387a1fe93e5ee52d2a1ee23e9c69ba8354", size = 11737674, upload-time = "2026-07-23T11:41:05.862Z" }, + { url = "https://files.pythonhosted.org/packages/1c/13/193d9aeeb6774690351cff9fafabd3ae9b54cc225d125e07fa004ce23bdc/ty-0.0.63-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a671b61eaad16178389e05b9c108c9cb75ae8d84968fe55906484f55d6268338", size = 11264191, upload-time = "2026-07-23T11:41:07.963Z" }, + { url = "https://files.pythonhosted.org/packages/4f/b7/c5843e16759a2b25fc8a7197473474038e585ac9fdaa6fa16aeb6384bf6a/ty-0.0.63-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b255cc83d95c51bb9ee4931303fdbece8cb1d6d7c655eb77a3f2c8f349fb64d6", size = 11818890, upload-time = "2026-07-23T11:41:09.885Z" }, + { url = "https://files.pythonhosted.org/packages/06/20/adf83ae1fc570d9bb449605e1eeeaa523ad2329ca838a636698b5c135d11/ty-0.0.63-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0da1e8183aa4f893421a173904478021498f542ee41273660538da6649a9631c", size = 11853141, upload-time = "2026-07-23T11:41:12.151Z" }, + { url = "https://files.pythonhosted.org/packages/ea/90/f8effd846e3ee13486ea08257c13094d58b9f188c5641bf609d6a7d5c09f/ty-0.0.63-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:128f38eb5a67199e3811426386f7ec96f41251ddf45e04ddc0bcbb29d4853245", size = 12545184, upload-time = "2026-07-23T11:41:14.4Z" }, + { url = "https://files.pythonhosted.org/packages/b8/76/aac3a30d40431eb1d329acea016b248f5b6255e30ef3d28e19447e344be2/ty-0.0.63-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bedf34aff8b0557f2a7119b314a68a9c9e58c1d21554d0e2748f2c5fe6a1f638", size = 13062375, upload-time = "2026-07-23T11:41:16.441Z" }, + { url = "https://files.pythonhosted.org/packages/62/3d/0158733932893e17f6008dadd74c8d7aed422fefc66e47fe77888014fe8c/ty-0.0.63-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7328d63c34587606dce02935a25404f54cd0161bfe413b8f7fc21d174aead612", size = 12619461, upload-time = "2026-07-23T11:41:18.538Z" }, + { url = "https://files.pythonhosted.org/packages/f5/be/e280ad095b050778f16f493d49a073fa5f6d8f301d3e2e59be6a672ba05c/ty-0.0.63-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:504c4457f3a62afe836c1f26a2c9a12549299095f9cc4146778558df51a7515c", size = 12376402, upload-time = "2026-07-23T11:41:20.482Z" }, + { url = "https://files.pythonhosted.org/packages/57/57/619788bf335cb86b090470b557ae1eed33613dc24e12142505d32b462e58/ty-0.0.63-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:7394ed39424b027c89d5f0c818871c791e33958b88f5bb13e14bd4a12a3ca631", size = 12671377, upload-time = "2026-07-23T11:41:22.489Z" }, + { url = "https://files.pythonhosted.org/packages/07/a2/0aff2cdd3c98e2c4729337542b8da0ce1980790e1600e0c4a717a1f46293/ty-0.0.63-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:94c3f155230490f8fb505911655e940c630c25cc0c35a76690cb413254fb4437", size = 11768090, upload-time = "2026-07-23T11:41:24.558Z" }, + { url = "https://files.pythonhosted.org/packages/43/4a/cdb5f1d26154144dfee08e1bd671daf827238170f7cee9dad43990bb2016/ty-0.0.63-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:c16e1d8b4f0ae99106d5f13a894034a202baf6cdab61e2bb1a239de82904f839", size = 11867747, upload-time = "2026-07-23T11:41:26.794Z" }, + { url = "https://files.pythonhosted.org/packages/7e/26/ecc09ecb70bc9fbfdf42fa57ff29568f173e8200df575a0d72dc2b8486f9/ty-0.0.63-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b66293dad89eaed4b9fbf3b661614dbeb85b59ba037178a3f9a0adedf264da5c", size = 12120000, upload-time = "2026-07-23T11:41:28.731Z" }, + { url = "https://files.pythonhosted.org/packages/14/07/862822f9c2c397785b69b25cf79b4dfc3c0d55684b9adf11ac194450f8e1/ty-0.0.63-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8b29cc832e2ec73502c97dd7325d6ae0e085b34a33529a0f785192317d9862fc", size = 12477862, upload-time = "2026-07-23T11:41:30.847Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c2/50b08b641578d6f48e8aa28a91d0de32c91aa24dd926ed199e8afd2d37ea/ty-0.0.63-py3-none-win32.whl", hash = "sha256:f0a8fbfd1f990c0c5d85cce018d438969ac79e49247e2192c9745394bb7d17ab", size = 11433155, upload-time = "2026-07-23T11:41:33.28Z" }, + { url = "https://files.pythonhosted.org/packages/92/2d/d422a5568f0d1f317186be22241288dc6e08b71dc24aca223f22038ac2d2/ty-0.0.63-py3-none-win_amd64.whl", hash = "sha256:2aa2370bdd6f42e9f37518c812379bef70b9162f9e5aad511495229b0b71cb93", size = 12450036, upload-time = "2026-07-23T11:41:35.559Z" }, + { url = "https://files.pythonhosted.org/packages/35/97/2c9748e28ead0650c7ad3e5f74f178832ceabd7cb5c272a882f29eb32ee4/ty-0.0.63-py3-none-win_arm64.whl", hash = "sha256:95ac1a62162c3c7ac204731e95ebf766d62a46a7bfa238a6acbe923fcb772cb1", size = 11826243, upload-time = "2026-07-23T11:41:37.749Z" }, ] [[package]] @@ -2462,11 +2467,11 @@ wheels = [ [[package]] name = "tzdata" -version = "2026.2" +version = "2026.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" } +sdist = { url = "https://files.pythonhosted.org/packages/92/ff/5a28bdfd8c3ebec42564ac7d0e54ca3db65044a9314a97f9564fa7a1e926/tzdata-2026.3.tar.gz", hash = "sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415", size = 198674, upload-time = "2026-07-10T08:50:37.887Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, + { url = "https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931", size = 348168, upload-time = "2026-07-10T08:50:36.46Z" }, ] [[package]] @@ -2498,20 +2503,20 @@ wheels = [ [[package]] name = "uvicorn" -version = "0.50.2" +version = "0.51.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9f/f6/cc9aadc0e481344a42095d222bfa764122fb8cfba708d1922917bd8bfb01/uvicorn-0.50.2.tar.gz", hash = "sha256:b92bf03509b82bcb9d49e7335b4fd364518ad021c2dc18b4e6a2fec8c955a0bb", size = 93716, upload-time = "2026-07-06T10:38:31.984Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/65/b7c6c443ccc58678c91e1e973bbe2a878591538655d6e1d47f24ba1c51f3/uvicorn-0.51.0.tar.gz", hash = "sha256:f6f4b69b657c312f516dd2d268ab9ae6f254b11e4bac504f37b2ab58b24dd0b0", size = 94412, upload-time = "2026-07-08T10:59:05.962Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/f0/7c228ee10c7ab8fd3a21d06579a6f7c6075c6ce72594a20fb5d2f206ff24/uvicorn-0.50.2-py3-none-any.whl", hash = "sha256:4ae72a385630bcc17a0adb8290f26c993865e0b43a2114c2aab96420172c056a", size = 72846, upload-time = "2026-07-06T10:38:30.543Z" }, + { url = "https://files.pythonhosted.org/packages/45/ec/dbb7e5a6b91f86bfb9eb7d2988a2730907b6a729875b949c7f022e8b88fa/uvicorn-0.51.0-py3-none-any.whl", hash = "sha256:5d38af6cd620f2ae3849fb44fd4879e0890aa1febe8d47eb355fb45d93fe6a5b", size = 73219, upload-time = "2026-07-08T10:59:04.44Z" }, ] [[package]] name = "virtualenv" -version = "21.6.0" +version = "21.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, @@ -2519,9 +2524,9 @@ dependencies = [ { name = "platformdirs" }, { name = "python-discovery" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/74/65/ec1d92091671e6407d3e7c1f5801413bb7b2b57630a50cae7750456ba0ed/virtualenv-21.6.0.tar.gz", hash = "sha256:e18a4d750f2b64dea73e72ffde3922f3c52365fabdc8157ebd3da20d031c4734", size = 5526111, upload-time = "2026-07-06T22:49:56.972Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/25/e367a7229b0914772ca8d81b41fde012d9feda68523b52644a571bb21ce8/virtualenv-21.7.0.tar.gz", hash = "sha256:7f9519b9432ff11b6e1a3e94061664efc2ff99ea21780e3cf4f6bd0a5da8b37c", size = 5527510, upload-time = "2026-07-21T13:12:14.109Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/e7/2fbd0cc1653c53eed8f10670538bb547de2b3e37aacad283faa82a71094b/virtualenv-21.6.0-py3-none-any.whl", hash = "sha256:bce9d097950fef9d81129b333babfb7767072850c2f1acce0ec536708401bfd1", size = 5506216, upload-time = "2026-07-06T22:49:54.941Z" }, + { url = "https://files.pythonhosted.org/packages/a5/7a/ae29312b1e88a22e81f5d21fc11526d2a114089776c2550d2b205b6c2a47/virtualenv-21.7.0-py3-none-any.whl", hash = "sha256:a8370c1c5530fbabf955e40b8fbbc68a431648b10f9433faa587db30a06e51dd", size = 5507078, upload-time = "2026-07-21T13:12:12.136Z" }, ] [[package]] @@ -2616,38 +2621,62 @@ wheels = [ [[package]] name = "websockets" -version = "16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, - { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, - { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, - { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, - { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, - { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, - { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, - { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, - { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, - { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, - { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, - { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, - { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, - { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, - { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, - { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, - { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, - { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, - { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, - { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, - { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, - { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, - { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, - { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, - { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, - { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, - { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +version = "16.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/f7/bc3a25c5ec26ce62ce487690becc2f3710bbc7b33338f005ad390db0b986/websockets-16.1.1.tar.gz", hash = "sha256:db234eda965dcce15df96bb9709f587cd87d4d52aaf0e80e2f34ec04c7670c57", size = 182204, upload-time = "2026-07-17T22:51:05.858Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/fd/6ec6c6d2850aea25b1b2aa9901a016980bb87d01e89b3eb00470b1b5d471/websockets-16.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ab59169ace05dcb49a1d4118f0bde139557adf45091bd85747e36bf5de984dd1", size = 179587, upload-time = "2026-07-17T22:49:38.959Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d8/1d299d2dd34087db39831a34cc645ef8a6f89d78efada6983093513cd81c/websockets-16.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5e3b7d601f6f84156b08cc4a5e541c2b50ad7b36cfc302b657a12477c904a5df", size = 177272, upload-time = "2026-07-17T22:49:40.293Z" }, + { url = "https://files.pythonhosted.org/packages/3d/86/0a70d3ae2f0f2256bb41302d9804dbca65d4360281e7feb3e1f94102ac46/websockets-16.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cd2ca96a082a36964aca83e992f72abeb61b7306c1a6cba4c7d06a7b93750cac", size = 177530, upload-time = "2026-07-17T22:49:41.786Z" }, + { url = "https://files.pythonhosted.org/packages/b5/c2/c676c69444d9db448b3f0a55a98dcc534affce0bce961d9d2f0b8499b10a/websockets-16.1.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f5d497865f05bb222cab7016c6034542e84e5f29f49c6fd3f4939cda7197b5b8", size = 187197, upload-time = "2026-07-17T22:49:43.658Z" }, + { url = "https://files.pythonhosted.org/packages/0b/13/88137fbaf726ebe29d62c1117fa11fa2bbb6209dc79d4ad738efbe36a2aa/websockets-16.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bae954c382e013d5ea5b190d2830526bfa45ad121c326da0049b8c769f185db6", size = 188433, upload-time = "2026-07-17T22:49:45.147Z" }, + { url = "https://files.pythonhosted.org/packages/01/6d/46c2f2ce6751cb26f39293e1ecbf8544cb01321397cd476c2756b98c216d/websockets-16.1.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e09f753a169951eb4f28c2c774f71069304f66e7277e0f5a2892423599cfa854", size = 189868, upload-time = "2026-07-17T22:49:46.581Z" }, + { url = "https://files.pythonhosted.org/packages/29/2b/170a9e8097636cfde4dc3c592b6e00b18a44a2f5407606d96ca542dd5838/websockets-16.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:024193f8551a2b0eafbdd160911012c4e6c228c28430c84433253299a9e42d6a", size = 189059, upload-time = "2026-07-17T22:49:47.972Z" }, + { url = "https://files.pythonhosted.org/packages/a7/48/f0d4ebc9ab4b473b8861b9e20fdb663d515d42f7befdf62cdb60fee7a1ec/websockets-16.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:aabe464bfd13bd25f4821faf111da6fefdc389f870265a53105580e45b0a2e49", size = 187814, upload-time = "2026-07-17T22:49:49.344Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ba/39a41d3ae8e72696a9492581900611c5a91e2b07563b0bcd2523adea9854/websockets-16.1.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a28fcbc9b6baf54a2e23f8655f308e4ccc6afdd7266f8fe7954f320dcda0f785", size = 185229, upload-time = "2026-07-17T22:49:50.787Z" }, + { url = "https://files.pythonhosted.org/packages/3c/36/ac15b604f850d1907f0a85ed721cefe47cd45034b3620069b829746cccbe/websockets-16.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79eace538c6a97e96d0d03d4f9d314f9677f5ed85a8a984992ffd90b13cb8a56", size = 187874, upload-time = "2026-07-17T22:49:52.228Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f3/3fbd5d71d59299c3770faa5884d4f45070236ca5a35ab3a61830812c409a/websockets-16.1.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:496af849a472b531f758dbd4d61338f5000538cb1a7b3d20d9d32a264517f509", size = 186469, upload-time = "2026-07-17T22:49:53.776Z" }, + { url = "https://files.pythonhosted.org/packages/b4/fc/dd90349bba58af2a53ef2ddd9c32716c81eb6d59a0687939fff561860878/websockets-16.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5283810d2646741a0d8da2aa733d6aefa0545809afccb2a5d105a26bc45125f1", size = 188347, upload-time = "2026-07-17T22:49:55.202Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f3/f73ba86427682da59b78c11d77ba56d5b801c32e84afe79b274bbd6a9bb2/websockets-16.1.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4e3b680b1e0a27457e727a0d572fd81dffa87b6dbf8b228ab57da64f7d85aead", size = 185903, upload-time = "2026-07-17T22:49:56.75Z" }, + { url = "https://files.pythonhosted.org/packages/34/7c/f95eb20e80104173b3a0a092291f89ea4047ef6e608e0a57ca06eb14eecb/websockets-16.1.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:69159730a823dde3ea8d08783e8d47ef135a6d7e8d44eb127e32b321c9db8e3e", size = 186855, upload-time = "2026-07-17T22:49:58.467Z" }, + { url = "https://files.pythonhosted.org/packages/b0/35/dd875b3e050ff232d60fa377707f890e369f74d134f1be32e8f68879747c/websockets-16.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ed5bb271084b46530ee2ddc0410537a9961152c5ccba2fc98c5276d992ccba87", size = 187140, upload-time = "2026-07-17T22:50:00.016Z" }, + { url = "https://files.pythonhosted.org/packages/e8/dc/5cbfcb41824502f6af93b8f3943a4d06c67c23c7d2e31eb18748c4a5b2a7/websockets-16.1.1-cp313-cp313-win32.whl", hash = "sha256:cfb70b4eb56cac4da0a83588f3ad50d46beb0690391082f3d4e2d488c70b68ea", size = 179928, upload-time = "2026-07-17T22:50:01.685Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c1/71e5deb5b7f8f226997ab64908c184ac3105c0155ce2d486f318e5dd08a8/websockets-16.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:d9531d9cbeac99af6f038fb1bc351403531f7d634a2c2e10e2f7c854c6ed5b68", size = 180242, upload-time = "2026-07-17T22:50:03.117Z" }, + { url = "https://files.pythonhosted.org/packages/73/a2/ba78a164eeea4620df4a4df4bd2ed6017438c4655cc0f36f2c0bc0432355/websockets-16.1.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:443aefe96b7fdb132e2a70806cca1f2af49bb3f28e47abcd7c2e9dcf4d8fa1b8", size = 179635, upload-time = "2026-07-17T22:50:05.001Z" }, + { url = "https://files.pythonhosted.org/packages/b9/08/d26d7a7628cd4ac34cbbdb63ac80914ca842ed8e42938c40a53567806df3/websockets-16.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6456ff333092d509127d75a638cb411afae8ff17f092635015d1902efec8a293", size = 177320, upload-time = "2026-07-17T22:50:06.427Z" }, + { url = "https://files.pythonhosted.org/packages/0f/45/ebec83e6269536aa5932533c67b0af5c781f3e73fdbcd68672dcf43f4f44/websockets-16.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fce6c48559c86d1ac3632ccb1bebc7d5442fbe79bd9bb0e40379ee54be2a4051", size = 177544, upload-time = "2026-07-17T22:50:07.834Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d5/abc614d2297f6c1c3e01e61260364457a47c25cc1cf6a879038902bc6aa8/websockets-16.1.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:92b820d345f7a3fc7b8163949ee92df910f290c3fc517b3d5301c78065adafe1", size = 187270, upload-time = "2026-07-17T22:50:09.275Z" }, + { url = "https://files.pythonhosted.org/packages/52/71/4c99af3b87dff1b2927981f6876607d4acb45338c665242168d3982f7758/websockets-16.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a606d9c24035242a3e256e9d5b77ed9cd6bccfcb7cf993e5ca3c0f6f68fb6a7", size = 188509, upload-time = "2026-07-17T22:50:10.722Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b4/5c8ca14b0df7eb84ed0524165c5359150210140817a3312aee57bf62a1cf/websockets-16.1.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:414e596c75f74e0994084694189d7dc9229fb278e33064d6784b73ffbba3ca31", size = 189882, upload-time = "2026-07-17T22:50:12.293Z" }, + { url = "https://files.pythonhosted.org/packages/25/c1/bedfba9e70557129cb8083748d167bdcc01483dedf0f0df143676df05cbe/websockets-16.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:536676848fc5961aca9d20389951f59169508f765637a172403dc5434d722fa0", size = 189114, upload-time = "2026-07-17T22:50:13.789Z" }, + { url = "https://files.pythonhosted.org/packages/df/09/aa835b2787835aebd839114be5de51b797cb480b63ba42b26d34dfe147cb/websockets-16.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:97fd3a0e8b53efa41970ac1dff3d8cf0d2884cadeb4caaf95db7ad1526926ee3", size = 187861, upload-time = "2026-07-17T22:50:15.179Z" }, + { url = "https://files.pythonhosted.org/packages/20/26/f6408330694dbc9830857d9d23bc14ac4f6875127a480cfdda8d5ca21198/websockets-16.1.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7b1b19636af86a3c7995d4d028dbe376f39b4bf31541146f9c123582a6c94562", size = 185286, upload-time = "2026-07-17T22:50:16.741Z" }, + { url = "https://files.pythonhosted.org/packages/17/9a/e0675e70dd8a80762cf35bb18799d3f290a4890ffe6439bc51d222796083/websockets-16.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:41c8e77f17294c0ac18008a7309b99b34ee72247ef10b6dff4c3f8b5ac29896b", size = 187935, upload-time = "2026-07-17T22:50:18.213Z" }, + { url = "https://files.pythonhosted.org/packages/33/c1/3234cfb86afde01b81e9bddcc6e534c440975d60a13991259e833069ab3e/websockets-16.1.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:9f63bcef7f4b02b06b35fc01c93b96c43b5e88e1e8868676caacf493d5a31f3a", size = 186444, upload-time = "2026-07-17T22:50:19.67Z" }, + { url = "https://files.pythonhosted.org/packages/89/87/9c15206e1d778923d8daa9657de07aa62ea815e13448319c98458c37b281/websockets-16.1.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dab9eb87869da2d6ed3af3f3adf28414baae6ec9d4df355ffc18889132f3436c", size = 188409, upload-time = "2026-07-17T22:50:21.28Z" }, + { url = "https://files.pythonhosted.org/packages/f2/00/cf5de5c67676de2d3eef8b2a518f168f6796595447a5b7161ba0d012915c/websockets-16.1.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:43e3a9fdd7cbf7ba6040c31fae0faf84ca1474fef777c4e37912f1540f854499", size = 185958, upload-time = "2026-07-17T22:50:22.719Z" }, + { url = "https://files.pythonhosted.org/packages/62/c0/731b6ddede2e4136912ec4cff2cffbda35af73546be4762c3d7bd3bd79af/websockets-16.1.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:056ae37939ed7e9974f364f5864e76e49182622d8f9751ac1903c0d09b013985", size = 186911, upload-time = "2026-07-17T22:50:24.108Z" }, + { url = "https://files.pythonhosted.org/packages/8c/7f/39c634472c4469a24a7c09cecddffb08fac6d0e74f73881a94ee8a40a196/websockets-16.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a0eadbbf2c30f01efa58e1f110eb6fa293261f6b0b1aa38f7f48707107690af9", size = 187204, upload-time = "2026-07-17T22:50:25.548Z" }, + { url = "https://files.pythonhosted.org/packages/26/89/9667c256c256dafcc62d21328ce7a40067da857969b68ee9af375b0aaf72/websockets-16.1.1-cp314-cp314-win32.whl", hash = "sha256:195c978b065fa40910582464f99d6b15c8b314c68e0546549a55ed83f4735328", size = 179603, upload-time = "2026-07-17T22:50:27.086Z" }, + { url = "https://files.pythonhosted.org/packages/bd/dd/1c099d6c0fc5deb6b46ccdbb6981fdb4b12c917869cb3952408409dc18db/websockets-16.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:4e8d01cc3bcae7bbf8167f944aeafefed590fae5693552bba9794a9df68371cc", size = 179948, upload-time = "2026-07-17T22:50:28.521Z" }, + { url = "https://files.pythonhosted.org/packages/35/25/9956b2d5e0529d5d23924f21bba1440d4c5c88a562e4f08550871ffa97a7/websockets-16.1.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0ffd3031ea8bda8d61762e84220186105ba3b748b3c8da2ae4f7816fac03e573", size = 179963, upload-time = "2026-07-17T22:50:29.982Z" }, + { url = "https://files.pythonhosted.org/packages/17/06/55ffc976c488b6aee9ea05761ff7c4e88e7c1fd82818c8ca7b556ad2f90c/websockets-16.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:84a2cef8deffbd9ab8ee0ea546a2a6a7030c28f44e6cdd4547dbfeb489eb8999", size = 177497, upload-time = "2026-07-17T22:50:31.396Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e8/f7dac2e980bacc92bdc26cebae4ae4d50cae5380732c50980598fc0bbae4/websockets-16.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3df13f73af9b3b38ab1195eb299ecb67a4330c911c97ae04043ff74085728abe", size = 177698, upload-time = "2026-07-17T22:50:32.829Z" }, + { url = "https://files.pythonhosted.org/packages/b2/39/26762f734113e22da2b942c3aca85798e0c0405d64c256549540ff31e5a1/websockets-16.1.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:23253dd5bcae3f9aaee0a1d30967a8dbd52e5d3cff93a2e5b84df57b77d4750d", size = 187561, upload-time = "2026-07-17T22:50:34.24Z" }, + { url = "https://files.pythonhosted.org/packages/11/94/c3f330851806b9b02138b774d593478323e73c99238681b4b93efe64e02d/websockets-16.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1c5705e314449e3308872fe084b8571ce078ee4fc55a98a769bdefe5917392", size = 188732, upload-time = "2026-07-17T22:50:36.088Z" }, + { url = "https://files.pythonhosted.org/packages/d1/f2/eb2c450f052de334ae33cf200ece6e87b0e14d186807074e4eb1cd2cdea2/websockets-16.1.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:69e52d175a0a7d1e13b4b67ad41c560b7d98e8c6f6126eb0bda496c784faf8c7", size = 190872, upload-time = "2026-07-17T22:50:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/70/31/2ac8cecf3a74f7fed9132129fc3d90b3998a1554570c11a69b2a8c20332d/websockets-16.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1f79c89b5eb034d1722938a891916582f8f7f503f58ca22518a63c3f2cd18499", size = 189305, upload-time = "2026-07-17T22:50:39.53Z" }, + { url = "https://files.pythonhosted.org/packages/6a/cf/8ab19650d3c0d4562c92e70ab47c257c4aa5c6a713ed87fe63766b31fefc/websockets-16.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:39f2a024af5c345ffe8fcf1ee18c049c024c94df393bb09b044a6917c77bde43", size = 188033, upload-time = "2026-07-17T22:50:40.912Z" }, + { url = "https://files.pythonhosted.org/packages/66/d7/a49a38a6127a4acb134fb1912b215d900cc657605cff32445bf519f3acc4/websockets-16.1.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:952303a7318d4cbe1011400839bb2051c9f84fa0a35923267f5daba34b15d458", size = 185748, upload-time = "2026-07-17T22:50:42.559Z" }, + { url = "https://files.pythonhosted.org/packages/95/3e/ad1fa40388c7f2e0bb2c7930d0090b6c5498594bd1cdaec18864df3d9e97/websockets-16.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:249116b4a76063d930a46391ad56e135c286e4562a18309029fc2c73f4ed4c62", size = 188285, upload-time = "2026-07-17T22:50:43.974Z" }, + { url = "https://files.pythonhosted.org/packages/35/b8/d5db28ca264b9104f82196f92dc8843e35fd391f763d42e4ad358f5bc97e/websockets-16.1.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:61922544a0587a13fd3f53e4c0e5e606510c7b0d9d22c8444e5fae22a06b38cb", size = 186777, upload-time = "2026-07-17T22:50:45.474Z" }, + { url = "https://files.pythonhosted.org/packages/42/9c/726cb39d0cc43ae848dce4aa2acb04eecc6738b1264ec6d700bf6bcfb9f8/websockets-16.1.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:46dcaa042cd1de6c59e7d9269fa63ff7572b6df40510600b678f0826b3c7af51", size = 188682, upload-time = "2026-07-17T22:50:46.973Z" }, + { url = "https://files.pythonhosted.org/packages/be/c7/1168704de8c2dd483edabe4a22cbe4465dd8be8dd95561d214f9fe092871/websockets-16.1.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:38565aca3e01ea8734e578fb2118dade0ecb0250533f29e22b8d1a7a196cf4d0", size = 186377, upload-time = "2026-07-17T22:50:48.413Z" }, + { url = "https://files.pythonhosted.org/packages/ca/40/f9ff2d630ffce4e7dfea0b2288e1caf9ebbf9ff8a9ec9396136ce8b94935/websockets-16.1.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:42f599f4d48c7e1a3338fdaac3acd075be3b3cf02d4b274f3bf2767aedd3d217", size = 187148, upload-time = "2026-07-17T22:50:49.845Z" }, + { url = "https://files.pythonhosted.org/packages/b5/71/e177c8299f78d7cbe2d14df228643c10c70c0e86e108e092056bbcc16e46/websockets-16.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dcc04fedf83effaeb9cce98abc9469bb1b42ef85f03e01c8c1f4438ef7555737", size = 187578, upload-time = "2026-07-17T22:50:51.619Z" }, + { url = "https://files.pythonhosted.org/packages/49/b2/b6987faf330f5af5c787a2610124c2e8403d51724f9001ec4fff6311fe7a/websockets-16.1.1-cp314-cp314t-win32.whl", hash = "sha256:8483c2096363120eea8b07c06ae7304d520f686665fffd4811fad423930a65d7", size = 179729, upload-time = "2026-07-17T22:50:53.269Z" }, + { url = "https://files.pythonhosted.org/packages/a2/6e/fbac6ed878dd362fbad7d415fa4f84d38e3e33fed8cde45c64e783acf826/websockets-16.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:bcce07e23e5769375158f5efdcdafa8d5cd014b93c6683865b840ed65b96f231", size = 180072, upload-time = "2026-07-17T22:50:54.969Z" }, + { url = "https://files.pythonhosted.org/packages/be/4d/2d0d67834092e354d2b0498f014a41249a89556bc406cf86f3e1557bb463/websockets-16.1.1-py3-none-any.whl", hash = "sha256:6abbd3e82c731c8e531714466acd5d87b5e88ac3243465337ba71d68e23ae7e3", size = 173814, upload-time = "2026-07-17T22:51:04.184Z" }, ] [[package]] From b3803828213894f3a50ee7b1a3f708891e908656 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?William=20Laur=C3=A9n?= Date: Sat, 25 Jul 2026 10:45:28 +0300 Subject: [PATCH 2/3] fix: make the missing-value and temperature-reading rules consistent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Missing values: polars distinguishes null (absent) from NaN (an ordinary float value), numpy has only NaN for both, and the two conventions met without a stated rule. The rule is now explicit and implemented: encomp never produces a NaN in a polars magnitude (a CoolProp state that cannot be fixed is null, as before), a NaN the caller puts in a Series is kept verbatim because it is data rather than a missing marker, and astype -- the boundary between the two worlds -- translates the sentinel. astype("pl.Series") previously carried a numpy NaN straight into polars, where it poisons mean() and is invisible to drop_nulls(); it now becomes null, and the reverse direction already rendered null as NaN. Temperature vs TemperatureDifference: a Quantity[Temperature] could carry a delta unit, so its unit and its dimensionality stated different physical meanings (Q[Temperature, float](5.0, "delta_degC"), and asdim(Temperature) on a difference, both produced 5 Δ°C typed as an absolute temperature). That pair is now re-resolved from the unit, exactly as every other mismatched subclass hint is (Q[Mass](1, "m") is a Length) -- it cannot raise, because pint itself builds T - T as self.__class__(magnitude, delta_unit) on the absolute class. asdim() gained the mirror of the existing rewrite, so reinterpreting a difference as absolute moves delta_degC back to degC; only the genuinely ambiguous multiplicative units (K, degR, ...) keep their spelling under both readings. A difference also converts to K by scale alone, which let it fix a CoolProp state: 300 ΔK silently flashed as 300 K, and 5 Δ°C returned NaN instead of an error. No CoolProp state input is a difference, so to_numeric_correct_unit (the single place state-input dimensionality is validated) refuses the whole class, as does ideal_gas_density -- its sibling _resolve_gas_condition already did. Both rules are documented in the usage guide and the Quantity docstring, and pinned by tests. Co-Authored-By: Claude Opus 5 (1M context) --- docs/usage.md | 51 +++++++++++++++++++++++++++++ encomp/fluids.py | 12 +++++++ encomp/gases.py | 11 ++++++- encomp/tests/test_fluids.py | 26 +++++++++++++++ encomp/tests/test_gases.py | 18 +++++++++++ encomp/tests/test_units.py | 64 +++++++++++++++++++++++++++++++++++++ encomp/units.py | 64 +++++++++++++++++++++++++++++++++++-- 7 files changed, 243 insertions(+), 3 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index 489e735..1c4b8f3 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -291,6 +291,29 @@ Q(arr, "bar") # [0.0 0.2 0.4 0.6000000000000001 0.8 1.0] bar ``` +Missing values follow the convention of whichever container holds them: `NaN` in the NumPy +world, `null` in the Polars world (where `NaN` is an ordinary float value, not a missing +marker — `mean()` propagates it while it skips `null`). `encomp` never *produces* a `NaN` +in a Polars magnitude: a CoolProp state that cannot be fixed yields `null` for that row, +and the corresponding NumPy result is `NaN`. {py:meth}`encomp.units.Quantity.astype` +translates between the two spellings at the boundary: + +```python +import numpy as np +import polars as pl + +from encomp.units import Quantity as Q + +# crossing into the Polars world turns the NumPy missing marker into null +assert Q(np.array([1.0, np.nan]), "bar").astype("pl.Series").m.to_list() == [1.0, None] + +# and back again, since NumPy has no other spelling for it +assert np.isnan(Q(pl.Series([1.0, None]), "bar").astype("ndarray").m[1]) + +# a NaN you put in a Series is kept as-is: it is data, not a missing marker +assert Q(pl.Series([1.0, float("nan")]), "bar").m.null_count() == 0 +``` + ### Quantities with expression magnitudes Polars Expressions can be used as magnitude: @@ -454,6 +477,34 @@ Q(4.19, "kJ/kg/K") * Q(5, "K") # ≈ 20.95 kJ/kg The environment variable `ENCOMP_AUTOCONVERT_OFFSET_TO_BASEUNIT` can be set to `True` to disable this error (this is not recommended). ::: +Only the *multiplicative* temperature units (`K`, `degR`, `mK`, ...) are ambiguous between +the two dimensionalities; `degC`/`degF` mean absolute and `delta_degC`/`delta_degF` mean a +difference. The unit is therefore what decides the dimensionality, and +{py:meth}`encomp.units.Quantity.asdim` — the explicit reinterpretation — rewrites the unit +so it can never contradict the dimensionality it is paired with: + +```python +from encomp.units import Quantity as Q +from encomp.utypes import Temperature, TemperatureDifference + +# K expresses both readings, so the unit is unchanged by the reinterpretation +assert Q(300, "K").dt is Temperature +assert Q(300, "K").asdim(TemperatureDifference).u == Q.get_unit("K") + +# an offset or delta unit is not ambiguous, so asdim() moves it onto the matching scale +assert Q(5, "delta_degC").asdim(Temperature).u == Q.get_unit("degC") +assert Q(5, "degC").asdim(TemperatureDifference).u == Q.get_unit("delta_degC") + +# a Quantity[...] annotation that disagrees with the unit is re-resolved from the unit +assert Q[Temperature, float](5.0, "delta_degC").dt is TemperatureDifference +``` + +A temperature difference is refused wherever an absolute temperature is required, even +though it converts to `K` by scale alone: {py:class}`encomp.fluids.Fluid` state inputs, +{py:func}`encomp.gases.ideal_gas_density` and the gas-condition tuples all raise +{py:class}`encomp.units.ExpectedDimensionalityError` rather than flash a difference as an +absolute state. + ### Currency units The dimensionality {py:class}`encomp.utypes.Currency` represents an arbitrary currency. diff --git a/encomp/fluids.py b/encomp/fluids.py index 2d2db1a..b490f1d 100644 --- a/encomp/fluids.py +++ b/encomp/fluids.py @@ -58,6 +58,7 @@ SpecificInternalEnergy, SurfaceTension, Temperature, + TemperatureDifference, ThermalConductivity, Velocity, ) @@ -922,6 +923,17 @@ def to_numeric_correct_unit( unit = self.get_coolprop_unit(prop) + # Temperature and TemperatureDifference share pint's [temperature] dimension, and a + # difference converts to K by scale alone, so .to() below would silently accept a + # difference wherever CoolProp wants an absolute temperature (300 ΔK flashing as + # 300 K). No CoolProp state input is a difference, so refuse the whole class here + if issubclass(qty.dt, TemperatureDifference) and unit.dimensionality == Temperature.dimensions: + raise ExpectedDimensionalityError( + f'CoolProp input for property "{prop}" must be an absolute Temperature, ' + f"passed a TemperatureDifference ({qty.u}); use .asdim(Temperature) if the " + "value really is an absolute temperature" + ) + try: m = qty.to(unit).m except DimensionalityError as e: diff --git a/encomp/gases.py b/encomp/gases.py index 80fe922..892f8f3 100644 --- a/encomp/gases.py +++ b/encomp/gases.py @@ -28,7 +28,7 @@ from .conversion import convert_volume_mass from .fluids import Fluid from .misc import isinstance_types -from .units import Quantity +from .units import ExpectedDimensionalityError, Quantity from .utypes import ( MT, Density, @@ -39,6 +39,7 @@ NormalVolumeFlow, Pressure, Temperature, + TemperatureDifference, Volume, VolumeFlow, ) @@ -136,6 +137,14 @@ def ideal_gas_density( Density of the ideal gas at the specified temperature and pressure """ + # a temperature difference converts to K by scale alone, so it would flow into the gas + # law as an absolute temperature; the sibling _resolve_gas_condition rejects it too + if issubclass(T.dt, TemperatureDifference): + raise ExpectedDimensionalityError( + f"T must be an absolute Quantity[Temperature], passed a TemperatureDifference ({T.u}); " + "use .asdim(Temperature) if the value really is an absolute temperature" + ) + # directly from ideal gas law # override the inferred type here since it's sure to be Density rho = (P * M) / (CONSTANTS.R * T.to("K").unknown()) diff --git a/encomp/tests/test_fluids.py b/encomp/tests/test_fluids.py index 8f18aab..6f42184 100644 --- a/encomp/tests/test_fluids.py +++ b/encomp/tests/test_fluids.py @@ -23,6 +23,7 @@ clear_expr_evaluation_cache, ) from ..settings import SETTINGS +from ..units import ExpectedDimensionalityError from ..units import Quantity as Q from ..utypes import DT, Density, SpecificEntropy @@ -1220,3 +1221,28 @@ def test_unknown_property_error_message_points_at_search() -> None: # dunder probing (copy, pickle, inspect) still fails plainly with pytest.raises(AttributeError): fluid.__deepcopy__ + + +def test_state_inputs_refuse_a_temperature_difference() -> None: + # Temperature and TemperatureDifference share pint's [temperature], and a difference + # converts to K by scale alone, so .to() alone would let a difference fix a state: + # 300 ΔK would silently flash as 300 K. No CoolProp state input is a difference + difference = Q(300.0, "K").asdim(ut.TemperatureDifference) + + with pytest.raises(ExpectedDimensionalityError, match="absolute Temperature"): + Water(P=Q(1.0, "bar"), T=cast(Any, difference)).D + + with pytest.raises(ExpectedDimensionalityError, match="absolute Temperature"): + Fluid("Water", P=Q(1.0, "bar"), T=cast(Any, Q(5.0, "delta_degC"))).D + + with pytest.raises(ExpectedDimensionalityError, match="absolute Temperature"): + HumidAir(T=cast(Any, difference), P=Q(1.0, "bar"), R=Q(0.5)).W + + # explicitly reinterpreted, the same value is accepted + absolute = difference.asdim(ut.Temperature) + assert Water(P=Q(1.0, "bar"), T=absolute).D.m == approx(996.5, abs=1.0) + + # a difference is still fine where CoolProp expects one dimensionally: none of the + # state inputs do, so the guard covers every temperature input including Tdb/Twb + with pytest.raises(ExpectedDimensionalityError, match="absolute Temperature"): + HumidAir(Tdb=cast(Any, difference), P=Q(1.0, "bar"), R=Q(0.5)).W diff --git a/encomp/tests/test_gases.py b/encomp/tests/test_gases.py index 3a965c7..9bea4a4 100644 --- a/encomp/tests/test_gases.py +++ b/encomp/tests/test_gases.py @@ -12,6 +12,7 @@ mass_to_normal_volume, normal_volume_to_actual_volume, ) +from ..units import ExpectedDimensionalityError from ..units import Quantity as Q from ..utypes import ( Density, @@ -20,6 +21,8 @@ NormalVolume, NormalVolumeFlow, Numpy1DArray, + Temperature, + TemperatureDifference, Volume, VolumeFlow, ) @@ -88,6 +91,21 @@ def test_ideal_gas_density() -> None: assert mixed.m[0] == approx(vector.m[0]) +def test_ideal_gas_density_refuses_a_temperature_difference() -> None: + # a ΔT converts to K by scale alone, so without this guard a difference would flow + # into the gas law as an absolute temperature and silently produce a density + difference = Q(300.0, "K").asdim(TemperatureDifference) + + with pytest.raises(ExpectedDimensionalityError, match="absolute"): + # pyrefly cannot solve the constrained magnitude TypeVar across several arguments + ideal_gas_density(Q(1.0, "bar"), cast(Any, difference), Q(29.0, "g/mol")) # pyrefly: ignore[bad-specialization] + + # the same value, explicitly reinterpreted as absolute, is accepted + absolute = difference.asdim(Temperature) + density = ideal_gas_density(Q(1.0, "bar"), absolute, Q(29.0, "g/mol")) # pyrefly: ignore[bad-specialization] + assert density.m == approx(1.1626, abs=1e-3) + + def test_gas_conversion() -> None: V = Q(1, "liter") m = Q(1, "kg") diff --git a/encomp/tests/test_units.py b/encomp/tests/test_units.py index d2eb481..4bb8921 100644 --- a/encomp/tests/test_units.py +++ b/encomp/tests/test_units.py @@ -1798,6 +1798,41 @@ def test_temperature_difference_to_multiplicative_units() -> None: _ = Q(300.0, "K").to("delta_degC") +def test_unit_and_dimensionality_never_contradict_for_temperature() -> None: + # a Quantity[Temperature] must never carry a Δ unit and a Quantity[TemperatureDifference] + # must never carry an offset one -- the unit and the dimensionality would then state + # different physical meanings. The two are handled differently on purpose: + + # an offset unit under a difference subclass is a mistake with no safe reading + with pytest.raises(DimensionalityTypeError, match="delta unit"): + Q[TemperatureDifference, float](5.0, "degC") + + # a delta unit under an absolute subclass re-resolves from the unit, exactly as every + # other mismatched subclass hint does (Q[Mass](1, "m") is a Length). It has to: pint + # builds T - T as self.__class__(magnitude, delta_unit) on the absolute class + reinterpreted = Q[Temperature, float](5.0, "delta_degC") + assert reinterpreted.dt is TemperatureDifference + assert reinterpreted.u == Unit("delta_degC") + assert (Q(35.0, "degC") - Q(25.0, "degC")).dt is TemperatureDifference + + # asdim is the deliberate reinterpretation, and it moves the unit to the matching + # scale in BOTH directions so the result cannot be misread + absolute = Q(5.0, "delta_degC").asdim(Temperature) + assert absolute.dt is Temperature + assert absolute.u == Unit("degC") + assert absolute.to("K").m == approx(278.15) + + difference = Q(5.0, "degC").asdim(TemperatureDifference) + assert difference.dt is TemperatureDifference + assert difference.u == Unit("delta_degC") + + # a multiplicative unit is genuinely ambiguous and stays as it is under both readings + assert Q(300.0, "K").dt is Temperature + assert Q(300.0, "K").asdim(TemperatureDifference).u == Unit("K") + assert Q[TemperatureDifference, float](300.0, "K").dt is TemperatureDifference + assert Q(300.0, "K").asdim(TemperatureDifference).asdim(Temperature).u == Unit("K") + + def test_temperature_unit_inputs() -> None: for unit in [ "degC", @@ -2288,6 +2323,35 @@ def test_division_by_zero_follows_the_container() -> None: assert pl.select(expression.m).item() == float("inf") +def test_missing_values_follow_each_containers_own_sentinel() -> None: + # polars distinguishes null (absent) from NaN (a float value); numpy has only NaN for + # both. astype is the boundary between the two worlds, so it translates the sentinel + from_numpy = Q(np.array([1.0, np.nan]), "m").astype("pl.Series") + assert from_numpy.m.to_list() == [1.0, None] + assert not from_numpy.m.is_nan().any() + + # and back again: numpy can only spell it as NaN + to_numpy = Q(pl.Series([1.0, None]), "m").astype("ndarray") + assert to_numpy.m[0] == 1.0 + assert np.isnan(to_numpy.m[1]) + + # round trip through both worlds preserves the meaning + round_tripped = Q(np.array([1.0, np.nan]), "m").astype("pl.Series").astype("ndarray") + assert round_tripped.m[0] == 1.0 + assert np.isnan(round_tripped.m[1]) + + # a NaN the caller puts in a Series is data, not a missing marker: kept as-is (and + # IEEE arithmetic can produce one in either world -- see the division-by-zero test) + kept = Q(pl.Series([1.0, float("nan")]), "m") + assert kept.m.is_nan().to_list() == [False, True] + assert kept.m.null_count() == 0 + + # nulls survive unit conversion and arithmetic as nulls + nulls = Q(pl.Series([1.0, None]), "bar") + assert nulls.to("kPa").m.to_list() == [100.0, None] + assert (nulls * 2.0).m.to_list() == [2.0, None] + + def test_ndim_reports_one_for_every_vector_container() -> None: assert Q(1.0, "m").ndim == 0 assert Q(np.array([1.0, 2.0]), "m").ndim == 1 diff --git a/encomp/units.py b/encomp/units.py index 870603b..75e1eab 100644 --- a/encomp/units.py +++ b/encomp/units.py @@ -461,6 +461,14 @@ class Quantity( separate dimensionality types even though both have Pint dimension ``[temperature]``. Use :meth:`to` for unit conversion and :meth:`asdim` only for explicit semantic reinterpretation between compatible dimensionality classes. + + Missing values follow the container's own convention: ``NaN`` in the numpy world, + ``null`` in the Polars world. encomp never *produces* a ``NaN`` in a Polars + magnitude -- anything it computes without a value (a CoolProp state that cannot be + fixed, for instance) is ``null`` -- and :meth:`astype` translates between the two + spellings at the boundary. A ``NaN`` the caller puts into a ``pl.Series`` is kept + as-is: it is a float value, not a missing marker, and IEEE arithmetic + (``0.0 / 0.0``) can produce one in either world. """ # constants @@ -888,6 +896,10 @@ def _validate_magnitude(val: MT | Sequence[float] | str | bytes | None) -> MT: raise ValueError(f"Only 1-dimensional NumPy arrays can be used as magnitude, got shape {val.shape}") return cast("MT", Quantity._cast_array_float(val)) elif isinstance(val, pl.Series): + # nulls (the polars missing sentinel) and NaNs are both kept verbatim: the + # magnitude is the caller's data, and a NaN here is a float value rather than + # a missing marker. Only values encomp itself computes are normalized to null + # (see the class docstring, and CoolPropFluid.construct_quantity) if val.dtype == pl.Null: return cast("MT", val.cast(pl.Float64)) if val.dtype.is_integer(): @@ -1274,9 +1286,21 @@ def __new__( f"Cannot construct TemperatureDifference with offset unit {valid_unit}; use a delta unit instead" ) + # The mirror case -- a delta unit under an absolute-temperature subclass -- cannot be + # an error, because pint itself takes that path: T - T builds the difference as + # self.__class__(magnitude, delta_unit) on the absolute class. It also cannot be + # accepted as-is (that is how a Quantity[Temperature] ended up carrying Δ°C), and the + # dimensions check below cannot catch it since Temperature and TemperatureDifference + # share pint's [temperature]. So re-resolve the subclass from the unit, exactly as + # the constructor does for every other mismatched hint (Q[Mass](1, "m") is a Length): + # the unit wins, and the result is a TemperatureDifference. + delta_unit_on_absolute_subclass = issubclass( + cls._dimensionality_type, Temperature + ) and cls._is_temperature_difference_unit(valid_unit) + is_valid_subclass = True - if cls._is_incomplete_dimensionality(cls._dimensionality_type): + if cls._is_incomplete_dimensionality(cls._dimensionality_type) or delta_unit_on_absolute_subclass: is_valid_subclass = False else: # compare dimensionalities with tolerance for float precision @@ -1401,6 +1425,25 @@ def _is_offset_temperature_unit(cls, unit: Unit[Any]) -> bool: registry = cast(Any, cls._REGISTRY) # _is_multiplicative is a stable pint internal return not all(registry._is_multiplicative(u) for u in unit._units) + @classmethod + def _as_absolute_temperature_unit(cls, unit: Unit[Any]) -> Unit[Temperature]: + # the mirror of _as_temperature_difference_unit: reinterpreting a difference as an + # absolute temperature moves a delta unit back onto its own absolute scale, so the + # unit can never contradict the dimensionality (5 Δ°C -> 5 °C, not 5 K) + if unit._units == Unit("delta_degC")._units: + return Unit("degC") + + if unit._units == Unit("delta_degF")._units: + return Unit("degF") + + if unit._units == Unit("delta_degRe")._units: + return Unit("degRe") + + if unit._units == Unit("delta_K")._units: + return Unit("K") + + return cast("Unit[Temperature]", unit) + @classmethod def _as_temperature_difference_unit(cls, unit: Unit[Any]) -> Unit[TemperatureDifference]: if unit._units == Unit("delta_K")._units: @@ -2171,8 +2214,13 @@ def asdim(self, other: type[DT_] | Quantity[DT_, MT]) -> Quantity[DT_, MT]: ) unit: Unit[Any] = self.u - if dim == TemperatureDifference: + if issubclass(dim, TemperatureDifference): # ty: ignore[invalid-argument-type] unit = self._as_temperature_difference_unit(unit) + elif issubclass(dim, Temperature): # ty: ignore[invalid-argument-type] + # the reverse reinterpretation: a delta unit moves back onto its absolute scale, + # so a Quantity[Temperature] never carries a Δ unit (the constructor re-resolves + # such a pair to TemperatureDifference -- asdim is the deliberate escape hatch) + unit = self._as_absolute_temperature_unit(unit) subcls = self._get_dimensional_subclass(dim, type(self.m)) # ty: ignore[invalid-argument-type] return cast("Quantity[DT_, MT]", subcls(self.m, unit)) @@ -2204,6 +2252,11 @@ def astype(self, magnitude_type: type[Any] | MagnitudeTypeName) -> Quantity[Any, Polars ``Expr``, or the corresponding string names. Units and dimensionality are unchanged. Converting to ``pl.Expr`` is only defined for scalar quantities, which become a literal expression. + + Crossing between the numpy and Polars worlds translates the missing-value + sentinel, because each world has its own: a ``NaN`` in a numpy magnitude becomes + a ``null`` in the Polars magnitude, and a ``null`` becomes ``NaN`` on the way + back. """ if isinstance(magnitude_type, str): magnitude_type = self._get_magnitude_type_from_name(magnitude_type) @@ -2242,6 +2295,13 @@ def astype(self, magnitude_type: type[Any] | MagnitudeTypeName) -> Quantity[Any, ) _m = [m] if not isinstance(m, Iterable) else m vals = pl.Series(values=_m) + + # translate the missing-value sentinel: NaN is how the numpy world spells + # missing, null is how the polars world does. The reverse direction needs no + # code -- np.array() of a Series already renders null as NaN + if vals.dtype.is_float(): + vals = vals.fill_nan(None) + return cast("Quantity[DT, Any]", self.get_subclass(dt, pl.Series)(vals, u)) else: raise TypeError(f"Cannot convert magnitude from type {type(m)} to {magnitude_type}") From decc293092d592c8bbc1a6cf6b25925477d5126b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?William=20Laur=C3=A9n?= Date: Sat, 25 Jul 2026 11:32:49 +0300 Subject: [PATCH 3/3] fix: type temperature differences through numpy dispatch, and translate the null sentinel for pl.Expr MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit numpy dispatch never reaches __sub__/__add__, and pint's ufunc path rebuilds the result from the unit alone -- so np.diff / np.ediff1d / np.gradient / np.ptp / np.std of a kelvin array came back typed as an absolute Temperature, and such a difference could then fix a CoolProp state as an absolute temperature: exactly the hole the previous commit closed for the operators. np.subtract and np.add now delegate to `-` and `+`, so the rules have one implementation (np.add on two degC quantities consequently raises OffsetUnitCalculusError like the operator instead of answering 45 °C), and the difference-producing array functions re-type their result. degC was already correct because pint emits delta_degC there; K is what leaked. astype("pl.Expr") carried a NaN into the polars world while astype("pl.Series") translated it to null, although the docstring names astype as *the* translating boundary. Both branches now translate. Two consistency fixes around the dimensionality classes: the T ± ΔT arithmetic tested its operands with identity rather than issubclass, so a user-defined subclass took part in every other T/ΔT rule but not in the arithmetic that defines the concept; and _distinct is no longer inherited as a claim -- a `class MyTemperature(Temperature)` used to take over unit resolution for [temperature] process-wide, silently retyping every temperature quantity in the process. An inherited `_distinct = False` is still honoured, since IndistinctDimensionality exists to be subclassed. convert_volume_mass' docstring promised that "a Polars result carries null (never NaN)" while a NaN in inp propagates as IEEE float data through the arithmetic. The guard is about the density argument, where NaN means *missing*; the docstring now says that instead of overreaching, and both behaviors are pinned. The rules behind all of this are lifted into AGENTS.md (library invariants) and the usage guide, which now also covers the numpy-dispatch typing, the one-way collapse of a data-NaN through numpy and CoolProp, and the K + K asymmetry. Also fills the test gaps the review listed: numpy dispatch, null/NaN through the parquet round-trip, the polars->numpy->polars direction, an offset unit declared as a difference column, and temperature cases in test_comparisons. Co-Authored-By: Claude Opus 5 (1M context) --- AGENTS.md | 23 +++++++- docs/usage.md | 56 ++++++++++++++++++++ encomp/conversion.py | 18 ++++--- encomp/fluids.py | 5 +- encomp/tests/test_comparisons.py | 46 +++++++++++++++- encomp/tests/test_conversion.py | 22 ++++++++ encomp/tests/test_polars_units.py | 37 +++++++++++++ encomp/tests/test_units.py | 87 +++++++++++++++++++++++++++++++ encomp/units.py | 81 ++++++++++++++++++++++++---- encomp/utypes.py | 22 ++++++-- 10 files changed, 372 insertions(+), 25 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 5c2026f..99acd05 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -71,7 +71,28 @@ assert pressure.to("kPa").m == 100.0 - `Temperature` and `TemperatureDifference` are distinct dimensionalities with deliberate arithmetic (`T - T -> ΔT`, `T ± ΔT -> T`, `ΔT - T` is an error). The comparison and pickling overrides on `Quantity` intentionally narrow pint's - signatures — that's the point of the library, not an LSP bug to fix. + signatures — that's the point of the library, not an LSP bug to fix. Two consequences + that are easy to break: + - **The unit decides the dimensionality**, and the two must never contradict each + other. `degC`/`degF` are absolute, `delta_degC`/`delta_K` are differences, and only + the multiplicative spellings (`K`, `degR`, `mK`) are genuinely ambiguous — those + default to `Temperature`. `asdim` rewrites the unit onto the matching scale in both + directions; a `Quantity[...]` hint that disagrees with the unit is re-resolved from + the unit (as for any dimensionality: `Q[Mass](1, "m")` is a `Length`). + - **A difference must never reach an absolute-temperature API.** ΔT converts to `K` by + scale alone, so `.to()` cannot catch it: CoolProp state inputs and + `ideal_gas_density` check the dimensionality explicitly. numpy dispatch does not go + through the operators, so `__array_ufunc__`/`__array_function__` route `np.add`/ + `np.subtract` to `+`/`-` and re-type the difference-producing array functions + (`_DIFFERENCE_ARRAY_FUNCTIONS`). Add to that set rather than duplicating the rules. +- **Missing values follow each container's own sentinel**: `NaN` in the numpy world, + `null` in the polars world (where `NaN` is an ordinary float *value* — polars skips + `null` in `mean()` and propagates `NaN`). encomp never *produces* a `NaN` in a polars + magnitude (an unfixable CoolProp state is `null`); a `NaN` the caller puts in a + `pl.Series` is data and is kept verbatim; IEEE arithmetic (`0.0 / 0.0`) may create one + in either world; and `astype` — the boundary between the worlds — translates the + sentinel both ways. Don't normalize magnitudes anywhere else: it would rewrite the + caller's data and cost an O(n) pass per operation. - CoolProp evaluation is **one property per DAG node** by design; don't batch multiple output properties into one plugin call. Scalars detach from the GIL in the direct PyO3 bridge and own thread-local native states; arrays run through the Rust/Polars diff --git a/docs/usage.md b/docs/usage.md index 1c4b8f3..0cb8f19 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -314,6 +314,30 @@ assert np.isnan(Q(pl.Series([1.0, None]), "bar").astype("ndarray").m[1]) assert Q(pl.Series([1.0, float("nan")]), "bar").m.null_count() == 0 ``` +The translation is lossless from NumPy and back, but not the other way around: NumPy has a +single spelling for both meanings, so a *data* `NaN` that goes out to NumPy and comes back +returns as missing. The same one-way collapse happens at the CoolProp boundary, where a +`NaN` input is a state that cannot be fixed and the polars result is therefore `null`: + +```python +import polars as pl + +from encomp.fluids import Water +from encomp.units import Quantity as Q + +data_nan = Q(pl.Series([1.0, float("nan")]), "bar") +assert data_nan.astype("ndarray").astype("pl.Series").m.to_list() == [1.0, None] + +water = Water[pl.Series](P=Q(pl.Series([1e5, 1e5]), "Pa"), T=Q(pl.Series([300.0, float("nan")]), "K")) +density = water.D +assert density.m.null_count() == 1 +assert not density.m.is_nan().any() +``` + +Arithmetic is the exception the rule names explicitly: `0.0 / 0.0` on a polars magnitude +produces a `NaN` in the polars world, exactly as it does in plain polars, because that is +IEEE float arithmetic on data rather than a missing value encomp introduced. + ### Quantities with expression magnitudes Polars Expressions can be used as magnitude: @@ -505,6 +529,38 @@ though it converts to `K` by scale alone: {py:class}`encomp.fluids.Fluid` state {py:class}`encomp.units.ExpectedDimensionalityError` rather than flash a difference as an absolute state. +NumPy functions type their results the same way the operators do: `np.subtract(a, b)` is +exactly `a - b`, `np.add(a, b)` is exactly `a + b`, and the functions that return a +*difference* of their input — `np.diff`, `np.ediff1d`, `np.gradient`, `np.ptp`, `np.std` — +return a `TemperatureDifference` for an absolute-temperature input: + +```python +import numpy as np + +from encomp.misc import isinstance_types +from encomp.units import Quantity as Q +from encomp.utypes import Numpy1DArray, Temperature, TemperatureDifference + +temperatures = Q(np.array([300.0, 600.0, 900.0]), "K") + +# a difference of absolute temperatures ... +assert isinstance_types(np.diff(temperatures), Q[TemperatureDifference, Numpy1DArray]) + +# ... while a mean of them is still an absolute temperature +assert isinstance_types(np.mean(temperatures), Q[Temperature, float]) +``` + +NumPy's own type stubs describe these functions as returning arrays, so the dimensionality +of the result is a runtime guarantee rather than a static one — use +{py:func}`encomp.misc.isinstance_types` (as above) or the operators, which are typed. + +:::{note} +Adding two absolute temperatures is refused for the offset spellings (`Q(25, "degC") + Q(25, "degC")` +raises `pint.errors.OffsetUnitCalculusError`) but not for the multiplicative ones: `Q(300, "K") + Q(300, "K")` +returns `600 K`, because `K` is a plain scale that pint cannot object to. Summing absolute +temperatures is rarely what you want in either spelling — subtract them, or add a difference. +::: + ### Currency units The dimensionality {py:class}`encomp.utypes.Currency` represents an arbitrary currency. diff --git a/encomp/conversion.py b/encomp/conversion.py index e40390e..fee16a0 100644 --- a/encomp/conversion.py +++ b/encomp/conversion.py @@ -48,12 +48,18 @@ def convert_volume_mass( Input mass or volume (or flow) rho : Quantity[Density, Any] Density of the substance. A *missing* density is allowed and yields a missing - result at that position, exactly as a missing ``inp`` does: ``NaN`` for float and - numpy magnitudes, ``null`` for a Polars Series. A Polars result carries null - (never NaN), so with a Polars ``inp`` a NaN density is rejected -- spell the - missing density as a null in a ``pl.Series``. Every *present* value must be - finite and strictly positive. Polars Expr inputs are deferred and cannot be - data-validated here. + result at that position, spelled the way each magnitude container spells missing: + ``NaN`` for float and numpy magnitudes, ``null`` for a Polars Series. Because a + NaN would be the wrong spelling on the Polars side -- and would reach the result + as a float value rather than as missing -- a NaN density is rejected when ``inp`` + is a Polars magnitude; spell it as a null in a ``pl.Series`` instead. Every + *present* value must be finite and strictly positive. Polars Expr inputs are + deferred and cannot be data-validated here. + + This guards the density argument only, which is where NaN means *missing*. A NaN + inside a Polars ``inp`` is ordinary float data (see :class:`encomp.units.Quantity`) + and propagates through the arithmetic as IEEE requires, so the result can carry a + NaN that came from ``inp``. Returns ------- diff --git a/encomp/fluids.py b/encomp/fluids.py index b490f1d..5dc3afc 100644 --- a/encomp/fluids.py +++ b/encomp/fluids.py @@ -907,7 +907,10 @@ def construct_quantity( qty = qty.astype(self._mt) if isinstance(qty.m, pl.Series): - # missing values surface as null, never NaN (the library's single sentinel) + # A computed polars magnitude carries null for missing, never NaN. astype() above + # already translates the sentinel when it converts from the numpy result, so the + # fill_nan is belt-and-braces for any future path that arrives here with a Series + # magnitude already in hand (convert_magnitude=False); the cast is load-bearing qty.m = qty.m.fill_nan(None).cast(self._eager_series_output_dtype()) # ty: ignore[invalid-assignment] return cast("Quantity[Any, MT]", qty) diff --git a/encomp/tests/test_comparisons.py b/encomp/tests/test_comparisons.py index b3642c8..940853a 100644 --- a/encomp/tests/test_comparisons.py +++ b/encomp/tests/test_comparisons.py @@ -1,14 +1,15 @@ import math import operator from collections.abc import Callable -from typing import Any, assert_type +from typing import Any, assert_type, cast import numpy as np import polars as pl import pytest +from ..units import DimensionalityComparisonError from ..units import Quantity as Q -from ..utypes import Numpy1DBoolArray +from ..utypes import Numpy1DBoolArray, TemperatureDifference def _assert_type(val: object, typ: type) -> None: @@ -330,3 +331,44 @@ def test_numpy_and_polars_disagree_on_nan_ordering() -> None: with_null = Q(pl.Series([1.0, None]), "m") assert (with_null > one).to_list() == [False, None] assert (with_null <= one).to_list() == [True, None] + + +def test_temperature_and_difference_comparisons() -> None: + # Temperature and TemperatureDifference share [temperature] but are not comparable. + # == answers False (Python convention: 1 == "a" is False), while the ordering operators + # raise because no answer is correct -- in every magnitude container + absolute = Q(5.0, "degC") + difference = Q(5.0, "delta_degC") + + # the type checkers reject these comparisons statically; cast to reach the runtime + assert (absolute == cast(Any, difference)) is False + assert (absolute != cast(Any, difference)) is True + + for op in (operator.gt, operator.ge, operator.lt, operator.le): + with pytest.raises(DimensionalityComparisonError): + op(absolute, difference) + with pytest.raises(DimensionalityComparisonError): + op(difference, absolute) + + # the same holds for the ambiguous kelvin spelling, where only the dimensionality differs + kelvin_absolute = Q(300.0, "K") + kelvin_difference = Q(300.0, "K").asdim(TemperatureDifference) + + assert (kelvin_absolute == cast(Any, kelvin_difference)) is False + with pytest.raises(DimensionalityComparisonError): + _ = kelvin_absolute > cast(Any, kelvin_difference) + + # within one dimensionality, comparisons work across scales as usual + assert Q(5.0, "delta_degC") == Q(5.0, "K").asdim(TemperatureDifference) + assert Q(25.0, "degC") > Q(200.0, "K") + + # ... and elementwise for the vector containers + array_differences = Q(np.array([5.0, 10.0]), "delta_degC") + array_mask = array_differences > Q(6.0, "delta_degC") + # pyrefly resolves the constrained magnitude TypeVar to the scalar (bool) overload here + assert array_mask.tolist() == [False, True] # pyrefly: ignore[missing-attribute] + + series_absolute = Q(pl.Series([25.0, 30.0]), "degC") + assert (series_absolute > Q(26.0, "degC")).to_list() == [False, True] + with pytest.raises(DimensionalityComparisonError): + _ = series_absolute > cast(Any, array_differences.astype("pl.Series")) diff --git a/encomp/tests/test_conversion.py b/encomp/tests/test_conversion.py index 429cb55..90e3076 100644 --- a/encomp/tests/test_conversion.py +++ b/encomp/tests/test_conversion.py @@ -120,3 +120,25 @@ def test_nan_density_is_rejected_for_polars_inputs() -> None: # the same missing density is fine for float/numpy inputs, where NaN IS the sentinel assert np.isnan(convert_volume_mass(Q(2.0, "kg"), rho=Q(float("nan"), "kg/m³")).m) + + +def test_nan_in_a_polars_input_is_data_not_a_missing_density() -> None: + # the NaN guard covers the DENSITY, where NaN means missing. A NaN inside inp is + # ordinary float data (Quantity's rule for polars magnitudes) and propagates through + # the arithmetic as IEEE requires -- it is not rewritten to null, and not rejected + result = convert_volume_mass( + Q(pl.Series([2.0, float("nan")]), "kg"), + rho=Q(pl.Series([1000.0, 1000.0]), "kg/m³"), + ) + + assert result.m.to_list()[0] == pytest.approx(0.002) + assert result.m.is_nan().to_list() == [False, True] + assert result.m.null_count() == 0 + + # a null inp stays the missing sentinel it already is + missing = convert_volume_mass( + Q(pl.Series([2.0, None]), "kg"), + rho=Q(pl.Series([1000.0, 1000.0]), "kg/m³"), + ) + + assert missing.m.to_list() == [pytest.approx(0.002), None] diff --git a/encomp/tests/test_polars_units.py b/encomp/tests/test_polars_units.py index 75998fa..68187e2 100644 --- a/encomp/tests/test_polars_units.py +++ b/encomp/tests/test_polars_units.py @@ -159,6 +159,22 @@ class KelvinDifference(QuantityFrame): assert_type(Difference.value, Column[TemperatureDifference]) +def test_declaring_a_difference_with_an_offset_unit_rewrites_the_stored_unit() -> None: + # unit(..., asdim=...) goes through Quantity.asdim, which moves an offset unit onto the + # matching delta scale -- so the persisted metadata can never say "absolute °C" for a + # column declared to hold differences + class Differences(QuantityFrame): + value = unit("degC", asdim=TemperatureDifference) + + assert Differences.value.unit == Unit("delta_degC") + assert Differences.value.dimensionality is TemperatureDifference + + typed = Differences.from_untyped(pl.DataFrame({"value": [5.0]})) + assert units_of(typed.lf) == {"value": Unit("delta_degC")} + assert typed.value.dt is TemperatureDifference + assert typed.lf.collect()["value"].ext.storage().to_list() == [5.0] + + def test_quantity_frame_boundaries_are_explicit() -> None: with raises(TypeError, match="from_untyped"): Sensors(pl.DataFrame({"pressure": [1.0], "Volume flow": [1.0]})) @@ -278,6 +294,27 @@ def test_parquet_round_trip(tmp_path: Path) -> None: assert units_of(pl.read_parquet(sunk)) == {"P": Unit("bar"), "V": Unit("m³/h")} +def test_parquet_round_trip_preserves_null_and_nan_distinctly(tmp_path: Path) -> None: + # null (missing) and NaN (a float value) are different things in polars, and the unit + # dtype must not collapse one into the other -- not even through an on-read conversion + path = tmp_path / "temperatures.parquet" + stored = with_units(pl.DataFrame({"value": [25.0, None, float("nan")]}), {"value": "degC"}) + stored.write_parquet(path) + + back = pl.read_parquet(path)["value"].ext.storage() + assert back.to_list()[0] == 25.0 + assert back.null_count() == 1 + assert back.is_nan().to_list() == [False, None, True] + + class Kelvin(QuantityFrame): + value = unit("K") + + converted = Kelvin.scan_parquet(path).lf.collect()["value"].ext.storage() + assert converted.to_list()[0] == pytest.approx(298.15) + assert converted.null_count() == 1 + assert converted.is_nan().to_list() == [False, None, True] + + def test_coolprop_import_registers_before_parquet_read(tmp_path: Path) -> None: path = tmp_path / "bar.parquet" with_units(pl.DataFrame({"P": [5.0], "T": [400.0]}), {"P": "bar", "T": "K"}).write_parquet(path) diff --git a/encomp/tests/test_units.py b/encomp/tests/test_units.py index 4bb8921..d9c5ee0 100644 --- a/encomp/tests/test_units.py +++ b/encomp/tests/test_units.py @@ -1833,6 +1833,84 @@ def test_unit_and_dimensionality_never_contradict_for_temperature() -> None: assert Q(300.0, "K").asdim(TemperatureDifference).asdim(Temperature).u == Unit("K") +def test_numpy_dispatch_types_temperature_differences_like_the_operator() -> None: + # numpy dispatch does not go through __sub__, and pint rebuilds the result from the + # unit: np.diff of a kelvin array used to come back as an absolute Temperature (and + # could then fix a CoolProp state as one), while `-` typed the same computation as a + # difference. degC was already correct because pint emits delta_degC there + kelvin = Q(np.array([300.0, 600.0, 900.0]), "K") + celsius = Q(np.array([25.0, 20.0, 30.0]), "degC") + + for func in (np.diff, np.ediff1d, np.gradient, np.ptp, np.std): + for absolute in (kelvin, celsius): + result = cast(Any, func(absolute)) + assert result.dt is TemperatureDifference, f"{func.__name__} on {absolute.u}" + + # np.subtract(a, b) is exactly a - b, and np.add(a, b) exactly a + b + # numpy's stubs type these as arrays, so the dimensionality is a runtime guarantee + assert cast(Any, np.subtract(kelvin[:1], kelvin[1:2])).dt is TemperatureDifference + subtracted = cast(Any, np.subtract(celsius[:1], celsius[1:2])) + assert subtracted.dt is TemperatureDifference + assert subtracted.u == Unit("delta_degC") + assert subtracted.m[0] == approx((celsius[0] - celsius[1]).m) + + # ... including the ambiguity the operator refuses: pint's ufunc path answered 45 °C + with pytest.raises(OffsetUnitCalculusError): + _ = np.add(celsius[:1], celsius[1:2]) + + difference = Q(np.array([5.0, 10.0]), "delta_degC") + assert cast(Any, np.add(Q(np.array([25.0, 25.0]), "degC"), difference)).dt is Temperature + assert cast(Any, np.diff(difference)).dt is TemperatureDifference + + # a mean of absolute temperatures stays absolute, and a squared result is neither + assert cast(Any, np.mean(celsius)).dt is Temperature + assert cast(Any, np.var(celsius)).dimensionality == Temperature.dimensions**2 + + +def test_temperature_arithmetic_includes_dimensionality_subclasses() -> None: + # a subclass takes part in every other T/ΔT rule (construction, asdim, conversion + # guards), so it takes part in the arithmetic that defines the concept too + class CustomDifference(TemperatureDifference): + dimensions = TemperatureDifference.dimensions + + class CustomTemperature(Temperature): + dimensions = Temperature.dimensions + + difference = Q[CustomDifference, float](5.0, "delta_degC") + absolute = Q[CustomTemperature, float](25.0, "degC") + + assert (Q(25.0, "degC") + difference).m == approx(30.0) + assert (Q(25.0, "degC") - difference).m == approx(20.0) + assert (difference + Q(25.0, "degC")).m == approx(30.0) + assert (absolute + Q(5.0, "delta_degC")).m == approx(30.0) + assert (absolute - Q(35.0, "degC")).dt is TemperatureDifference + + # ΔT - T stays an error for a subclass too (it is not a temperature) + with pytest.raises(DimensionalityTypeError): + _ = cast(Any, difference) - Q(25.0, "degC") + + +def test_subclassing_a_distinct_dimensionality_does_not_hijack_unit_resolution() -> None: + # _distinct is a claim by the class that sets it, not by its subclasses: inheriting it + # would make every degC/K quantity in the process resolve to the subclass instead + with _reset_dimensionality_registry(): + + class CustomTemperature(Temperature): + dimensions = Temperature.dimensions + + assert not CustomTemperature.is_distinct() + assert Q(25.0, "degC").dt is Temperature + assert Q(300.0, "K").dt is Temperature + + # an explicit claim still works, and an inherited False is still honoured + class ExplicitTemperature(Temperature): + _distinct = True + dimensions = Temperature.dimensions + + assert ExplicitTemperature.is_distinct() + assert Q(25.0, "degC").dt is ExplicitTemperature + + def test_temperature_unit_inputs() -> None: for unit in [ "degC", @@ -2351,6 +2429,15 @@ def test_missing_values_follow_each_containers_own_sentinel() -> None: assert nulls.to("kPa").m.to_list() == [100.0, None] assert (nulls * 2.0).m.to_list() == [2.0, None] + # pl.Expr is the polars world too, so astype translates there as well + assert pl.select(v=Q(float("nan"), "m").astype("pl.Expr").m).to_series().to_list() == [None] + assert pl.select(v=Q(2.0, "m").astype("pl.Expr").m).to_series().to_list() == [2.0] + + # the polars -> numpy -> polars direction cannot round trip a data-NaN: numpy has one + # spelling for both meanings, so it comes back as missing. Deliberate and documented + collapsed = Q(pl.Series([1.0, float("nan")]), "m").astype("ndarray").astype("pl.Series") + assert collapsed.m.to_list() == [1.0, None] + def test_ndim_reports_one_for_every_vector_container() -> None: assert Q(1.0, "m").ndim == 0 diff --git a/encomp/units.py b/encomp/units.py index 75e1eab..ad0b397 100644 --- a/encomp/units.py +++ b/encomp/units.py @@ -471,6 +471,10 @@ class Quantity( (``0.0 / 0.0``) can produce one in either world. """ + # numpy array functions whose result is a DIFFERENCE of their input, so an absolute + # temperature input yields a TemperatureDifference (see __array_function__) + _DIFFERENCE_ARRAY_FUNCTIONS: ClassVar[frozenset[str]] = frozenset({"diff", "ediff1d", "gradient", "ptp", "std"}) + # constants NORMAL_M3_VARIANTS = ("nm³", "Nm³", "nm3", "Nm3", "nm**3", "Nm**3", "nm^3", "Nm^3") TEMPERATURE_DIFFERENCE_UCS = ( @@ -598,6 +602,52 @@ def __array__(self, t: Any | None = None, copy: bool = False, dtype: str | None return self._pint_super.__array__(t) + def __array_ufunc__(self, ufunc: Any, method: str, *inputs: Any, **kwargs: Any) -> Any: # noqa: ANN401 + # np.subtract(a, b) must mean exactly a - b, and np.add(a, b) exactly a + b. numpy + # dispatch does not go through __sub__/__add__, and pint's ufunc path rebuilds the + # result from the unit alone -- which loses every rule those operators enforce: + # T - T is a TemperatureDifference (np.subtract typed it absolute, so a difference + # could go on to fix a CoolProp state as an absolute temperature), and T + T on an + # offset scale is ambiguous (np.add quietly answered 45 °C for 25 °C + 20 °C). + # Delegating keeps one implementation of the rules instead of two + if ( + method == "__call__" + and not kwargs + and len(inputs) == 2 + and all(isinstance(operand, Quantity) for operand in inputs) + ): + first, second = cast("tuple[Quantity[Any, Any], Quantity[Any, Any]]", inputs) + + if ufunc is np.subtract: + return first - second + + if ufunc is np.add: + return first + second + + return self._pint_super.__array_ufunc__(ufunc, method, *inputs, **kwargs) + + def __array_function__(self, func: Any, types: Any, args: Any, kwargs: Any) -> Any: # noqa: ANN401 + ret: Any = self._pint_super.__array_function__(func, types, args, kwargs) + + # the same reasoning as __array_ufunc__, for the array functions that return a + # DIFFERENCE of their input: pint types the result from the unit, which is right for + # an offset spelling (degC -> delta_degC) but leaves a multiplicative one absolute + # (np.diff of a K array stayed a Temperature). Re-type so they agree with `-` + if ( + isinstance(ret, Quantity) + and getattr(func, "__name__", "") in self._DIFFERENCE_ARRAY_FUNCTIONS + and issubclass(self.dt, Temperature) + ): + difference = cast("Quantity[Any, Any]", ret) # pyrefly: ignore[redundant-cast] # cast required by pyright + + # np.var and friends leave [temperature]**2 -- only re-type an actual temperature + if difference.dimensionality == Temperature.dimensions: + return difference.asdim(TemperatureDifference) + + # cast: the isinstance narrowing above leaves a partially-unknown pint type here, + # and this hook returns whatever numpy asked pint for + return cast(Any, ret) # ty: ignore[redundant-cast] + @staticmethod def validate_magnitude_type(mt: type) -> None: """Raise ``TypeError`` unless ``mt`` is a supported magnitude container type.""" @@ -1723,7 +1773,9 @@ def correct_unit(unit: str) -> str: "Δ%": "%", "‰": "permille", "r/min": "rpm", - # ΔK does not really make sense, it's not an offset scale + # "ΔK" spells a temperature difference in kelvin: K is not an offset scale, so + # pint has no delta unit for it and encomp defines delta_K itself + # (defs/units.txt) -- it is how a TemperatureDifference is written in kelvin "Δ": "delta_", } @@ -2115,16 +2167,18 @@ def _temperature_difference_add_sub( other: Quantity[TemperatureDifference, Any] | Quantity[Temperature, Any], operator: Literal["add", "sub"], ) -> Quantity[Temperature, MT]: - if self.dt == Temperature: - assert other._dimensionality_type == TemperatureDifference + # issubclass, not identity: a user-defined subclass of either dimensionality takes + # part in the same arithmetic (it does in every other T/ΔT rule) + if issubclass(self.dt, Temperature): + assert issubclass(other._dimensionality_type, TemperatureDifference) v1 = self.to("degC").m v2 = other.to("delta_degC").m val = v1 + v2 if operator == "add" else v1 - v2 temperature_unit = self.u else: - assert self.dt == TemperatureDifference - assert other._dimensionality_type == Temperature + assert issubclass(self.dt, TemperatureDifference) + assert issubclass(other._dimensionality_type, Temperature) v1 = self.to("delta_degC").m v2 = other.to("degC").m @@ -2270,7 +2324,10 @@ def astype(self, magnitude_type: type[Any] | MagnitudeTypeName) -> Quantity[Any, return cast("Quantity[DT, Any]", self) elif magnitude_type is pl.Expr: if isinstance(m, float): - return cast("Quantity[DT, Any]", self.get_subclass(dt, pl.Expr)(pl.lit(m), u)) + # translate the missing-value sentinel, exactly like the pl.Series branch + # below: this crosses into the polars world, where missing is null + literal = pl.lit(None, dtype=pl.Float64) if math.isnan(m) else pl.lit(m) + return cast("Quantity[DT, Any]", self.get_subclass(dt, pl.Expr)(literal, u)) raise TypeError( f"Cannot convert magnitude with type {type(m)} to Polars expression, " @@ -2370,8 +2427,8 @@ def __add__(self, other: Quantity[Any, Any] | float) -> Quantity[Any, Any]: if not isinstance(other, Quantity): raise e - self_is_temp_or_diff_temp = self.dt in (Temperature, TemperatureDifference) - other_is_temp_or_diff_temp = other._dimensionality_type in (Temperature, TemperatureDifference) + self_is_temp_or_diff_temp = issubclass(self.dt, (Temperature, TemperatureDifference)) + other_is_temp_or_diff_temp = issubclass(other._dimensionality_type, (Temperature, TemperatureDifference)) if self_is_temp_or_diff_temp and other_is_temp_or_diff_temp: return self._temperature_difference_add_sub(other, "add") # ty: ignore[invalid-argument-type] @@ -2432,14 +2489,18 @@ def __sub__(self, other: Quantity[Any, Any] | float) -> Quantity[Any, Any]: # only Temperature - TemperatureDifference is meaningful here; the # reverse (ΔT - T) is not a temperature and stays an error - if self.dt == Temperature and other._dimensionality_type == TemperatureDifference: + if issubclass(self.dt, Temperature) and issubclass(other._dimensionality_type, TemperatureDifference): return self._temperature_difference_add_sub(other, "sub") # ty: ignore[invalid-argument-type] raise e ret = cast("Quantity[DT, MT]", self._pint_super.__sub__(other)) - if isinstance(other, Quantity) and self.dt == Temperature and other._dimensionality_type == Temperature: + if ( + isinstance(other, Quantity) + and issubclass(self.dt, Temperature) + and issubclass(other._dimensionality_type, Temperature) + ): _mt = type(ret.m) subcls = self._get_dimensional_subclass(TemperatureDifference, _mt) # ty: ignore[invalid-argument-type] return subcls(ret.m, ret.u) diff --git a/encomp/utypes.py b/encomp/utypes.py index 3a5de95..2225a2e 100644 --- a/encomp/utypes.py +++ b/encomp/utypes.py @@ -799,12 +799,24 @@ def get_dimensionality(cls, dimensions: UnitsContainer) -> type[Dimensionality]: def is_distinct(cls) -> bool: """Whether this class is the one :meth:`get_dimensionality` returns for its dimensions. - Unless ``_distinct`` is set explicitly, only the first registered subclass with a - given ``dimensions`` is distinct. The answer is registry-relative and can change - when a later subclass registers the same dimensions. + Unless ``_distinct`` is set explicitly on the class itself, only the first + registered subclass with a given ``dimensions`` is distinct. The answer is + registry-relative and can change when a later subclass registers the same + dimensions. + + Subclassing a distinct dimensionality does not inherit its claim: a + ``class MyTemperature(Temperature)`` would otherwise take over unit resolution for + ``[temperature]`` process-wide, silently retyping every temperature quantity. An + inherited ``_distinct = False`` IS honoured -- :class:`IndistinctDimensionality` + exists to be subclassed. """ - if cls._distinct is None: + distinct = cls._distinct + + if distinct is True and "_distinct" not in cls.__dict__: + distinct = None + + if distinct is None: # special case if dimensions was overridden to None if getattr(cls, "dimensions", None) is None: return True @@ -815,7 +827,7 @@ def is_distinct(cls) -> bool: # might change when the registry is updated return ucs.count(cls.dimensions) == 1 - return cls._distinct + return distinct _DimensionlessUC = UnitsContainer({})