diff --git a/CHANGELOG.md b/CHANGELOG.md index 1026376..bda7521 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,13 +8,18 @@ The changelog format is based on [Keep a Changelog](https://keepachangelog.com/e * -/- -## [0.3.2] - 2026-01-16 +## [0.3.2] - 2026-02-02 ### Added +* Added new module unit.py, containing class `Unit`, a helper class to store and manage units and display units. One `Unit` object represents one scalar variable. +* Added new module range.py, containing class `Range`, a utility class to store and handle the variable range of a single-valued variable. * Sphinx documentation: - * Added docs for modules variable_naming.py, enums.py, analytic.py and plotter.py + * Added docs for modules variable_naming.py, unit.py, range.py, enums.py and analytic.py * Added Visual Studio Code settings +### Removed +* Removed module plotter.py + ### Changed * Updated code base with latest changes in python_project_template v0.2.6 * pyproject.toml: diff --git a/docs/source/component_model.rst b/docs/source/component_model.rst index 8c8a4df..9ee9b7c 100644 --- a/docs/source/component_model.rst +++ b/docs/source/component_model.rst @@ -23,6 +23,7 @@ Modules component_model.model component_model.variable component_model.variable_naming + component_model.unit + component_model.range component_model.enums component_model.analytic - component_model.plotter diff --git a/examples/DrivingForce.fmu b/examples/DrivingForce.fmu index 7cb36d8..3f54e30 100644 Binary files a/examples/DrivingForce.fmu and b/examples/DrivingForce.fmu differ diff --git a/examples/DrivingForce6D.fmu b/examples/DrivingForce6D.fmu index c35d34c..42411c4 100644 Binary files a/examples/DrivingForce6D.fmu and b/examples/DrivingForce6D.fmu differ diff --git a/examples/HarmonicOscillator.fmu b/examples/HarmonicOscillator.fmu index 90019fe..bea0d49 100644 Binary files a/examples/HarmonicOscillator.fmu and b/examples/HarmonicOscillator.fmu differ diff --git a/examples/HarmonicOscillator6D.fmu b/examples/HarmonicOscillator6D.fmu index 2663758..7e59f77 100644 Binary files a/examples/HarmonicOscillator6D.fmu and b/examples/HarmonicOscillator6D.fmu differ diff --git a/examples/axle.py b/examples/axle.py index 09351db..842faeb 100644 --- a/examples/axle.py +++ b/examples/axle.py @@ -29,7 +29,7 @@ def init_drive(self): self.wheels[0].pos = np.array([0.0, 0.0], float) self.wheels[1].pos = np.array([self.a, 0.0], float) for i in range(2): - self.wheels[i].track = None + self.wheels[i].track_reset() self.times = [] def drive(self, time: float, dt: float): @@ -90,22 +90,21 @@ def pos(self) -> np.ndarray: return self._pos @pos.setter - def pos(self, newpos: list[float] | np.ndarray): - self.track = newpos + def pos(self, newpos: np.ndarray): + self.track_add(newpos) self._pos = np.array(newpos, float) @property def track(self) -> list[list[float]]: return self._track - @track.setter - def track(self, newpos: list[float] | np.ndarray | None): - """Remember the track. None: start new track.""" - if newpos is None: # reset - self._track = [[], []] - else: - for i in range(2): - self._track[i].append(newpos[i]) + def track_reset(self): + self._track = [[], []] + + def track_add(self, newpos: np.ndarray): + """Remember the track.""" + for i in range(2): + self._track[i].append(newpos[i]) class Motor: diff --git a/examples/bouncing_ball_3d.py b/examples/bouncing_ball_3d.py index 30afef3..04f920a 100644 --- a/examples/bouncing_ball_3d.py +++ b/examples/bouncing_ball_3d.py @@ -1,6 +1,5 @@ -# pyright: reportAttributeAccessIssue=false -# pyright: reportOptionalMemberAccess=false from math import sqrt +from typing import Any import numpy as np @@ -30,26 +29,29 @@ class BouncingBall3D(Model): def __init__( self, name: str = "BouncingBall3D", - description="Another Python-based BouncingBall model, using Model and Variable to construct a FMU", - pos: tuple = ("0 m", "0 m", "10 inch"), - speed: tuple = ("1 m/s", "0 m/s", "0 m/s"), - g="9.81 m/s^2", - e=0.9, + description: str = "Another Python-based BouncingBall model, using Model and Variable to construct a FMU", + pos: tuple[str | float, ...] = ("0 m", "0 m", "10 inch"), + speed: tuple[str | float, ...] = ("1 m/s", "0 m/s", "0 m/s"), + g: str | float = "9.81 m/s^2", + e: float = 0.9, min_speed_z: float = 1e-6, - **kwargs, + **kwargs: Any, ): super().__init__(name, description, author="DNV, SEACo project", **kwargs) + self.pos: np.ndarray self._pos = self._interface("pos", pos) self._speed = self._interface("speed", speed) + self.g: float self._g = self._interface("g", g) self.a = np.array((0, 0, -self.g), float) + self.e: float self._e = self._interface("e", e) self.min_speed_z = min_speed_z self.stopped = False self.time = 0.0 self._p_bounce = self._interface("p_bounce", ("0m", "0m", "0m")) # Note: 3D, but z always 0 # provoke an update at simulation start: - self.t_bounce, self.p_bounce = (-1.0, self.pos) # type: ignore + self.t_bounce, self.p_bounce = (-1.0, self.pos) def do_step(self, current_time: float, step_size: float) -> bool: """Perform a simulation step from `self.time` to `self.time + step_size`. @@ -110,7 +112,7 @@ def exit_initialization_mode(self): super().exit_initialization_mode() self.a = np.array((0, 0, -self.g), float) - def _interface(self, name: str, start: str | float | tuple) -> Variable: + def _interface(self, name: str, start: str | float | tuple[str | float, ...]) -> Variable: """Define a FMU2 interface variable, using the variable interface. Args: diff --git a/examples/bouncing_ball_3d_pythonfmu.py b/examples/bouncing_ball_3d_pythonfmu.py index f7fd144..7e0e6bf 100644 --- a/examples/bouncing_ball_3d_pythonfmu.py +++ b/examples/bouncing_ball_3d_pythonfmu.py @@ -1,4 +1,5 @@ from math import sqrt +from typing import Any import numpy as np from pythonfmu import Fmi2Causality, Fmi2Slave, Real # type: ignore[import-untyped] @@ -16,7 +17,7 @@ class BouncingBall3D(Fmi2Slave): * Internal units are assumed as SI (m,s,rad) """ - def __init__(self, **kwargs): + def __init__(self, **kwargs: Any): super().__init__( name="BouncingBall3D", description="Another Python-based BouncingBall model, using Model and Variable to construct a FMU", @@ -63,7 +64,7 @@ def __init__(self, **kwargs): self.accelerationY = 0.0 self.accelerationZ = -self.g - def do_step(self, current_time, step_size) -> bool: + def do_step(self, current_time: float, step_size: float) -> bool: """Perform a simulation step from `self.time` to `self.time + step_size`. With respect to bouncing (self.t_bounce should be initialized to a negative value) diff --git a/examples/driving_force_fmu.py b/examples/driving_force_fmu.py index d8b4b7f..2d42637 100644 --- a/examples/driving_force_fmu.py +++ b/examples/driving_force_fmu.py @@ -66,7 +66,7 @@ def __init__( self.freq = np.array((1.0,) * self.dim, float) self.d_freq = np.array((0.0,) * self.dim, float) self.function = func - self.func: Callable + self.func: Callable # type: ignore[reportMissingTypeArgument] ## kwargs self.f = np.array((0.0,) * self.dim, float) self.v_osc = (0.0,) * self.dim self._ampl = Variable(self, "ampl", "The amplitude of the force in N", start=_ampl) @@ -102,7 +102,7 @@ def exit_initialization_mode(self): self.func = partial( self.function, ampl=np.array(self.ampl, float), - omega=np.array(2 * np.pi * self.freq, float), # type: ignore # it is an ndarray! - d_omega=np.array(2 * np.pi * self.d_freq, float), # type: ignore # it is an ndarray! + omega=np.array(2 * np.pi * self.freq, float), + d_omega=np.array(2 * np.pi * self.d_freq, float), ) logger.info(f"Initial settings: ampl={self.ampl}, freq={self.freq}, d_freq={self.d_freq}") diff --git a/examples/oscillator.py b/examples/oscillator.py index 6fbf3b1..ddc8ad2 100644 --- a/examples/oscillator.py +++ b/examples/oscillator.py @@ -33,7 +33,7 @@ def __init__( c: tuple[float, ...] | tuple[str, ...] = (0.0, 0.0, 0.0), m: float | tuple[float, ...] = 1.0, tolerance: float = 1e-5, - f_func: Callable | None = None, + f_func: Callable | None = None, # type: ignore[reportMissingTypeArgument] ## kwargs ): self.dim = len(k) self.k = np.array(k, float) @@ -106,7 +106,7 @@ class Force: func (callable)=lambda t:np.array( (0,0,0), float): A function of t, producing a 3D vector """ - def __init__(self, func: Callable): + def __init__(self, func: Callable): # type: ignore[reportMissingTypeArgument] ## kwargs self.func = func self.out = np.array((0, 0, 0), float) diff --git a/examples/oscillator_6d.py b/examples/oscillator_6d.py index 6fbf3b1..ddc8ad2 100644 --- a/examples/oscillator_6d.py +++ b/examples/oscillator_6d.py @@ -33,7 +33,7 @@ def __init__( c: tuple[float, ...] | tuple[str, ...] = (0.0, 0.0, 0.0), m: float | tuple[float, ...] = 1.0, tolerance: float = 1e-5, - f_func: Callable | None = None, + f_func: Callable | None = None, # type: ignore[reportMissingTypeArgument] ## kwargs ): self.dim = len(k) self.k = np.array(k, float) @@ -106,7 +106,7 @@ class Force: func (callable)=lambda t:np.array( (0,0,0), float): A function of t, producing a 3D vector """ - def __init__(self, func: Callable): + def __init__(self, func: Callable): # type: ignore[reportMissingTypeArgument] ## kwargs self.func = func self.out = np.array((0, 0, 0), float) diff --git a/examples/oscillator_xd.py b/examples/oscillator_xd.py index d930372..2b82556 100644 --- a/examples/oscillator_xd.py +++ b/examples/oscillator_xd.py @@ -123,14 +123,14 @@ class Force: denoting the force dependencies """ - def __init__(self, dim: int, func: Callable): + def __init__(self, dim: int, func: Callable): # type: ignore[reportMissingTypeArgument] ## kwargs self.dim = dim self.func = func self.current_time = 0.0 self.dt = 0 self.out = np.array((0,) * self.dim) - def __call__(self, **kwargs): + def __call__(self, **kwargs: Any): """Calculate the force in dependence on keyword arguments 't', 'x' or 'v'.""" if "t" in kwargs: t = kwargs["t"] diff --git a/examples/time_table.py b/examples/time_table.py index cc59a28..8c7408e 100644 --- a/examples/time_table.py +++ b/examples/time_table.py @@ -1,4 +1,4 @@ -from typing import Sequence +from typing import Any import numpy as np from scipy.interpolate import make_interp_spline @@ -21,16 +21,18 @@ class TimeTable: def __init__( self, - data: Sequence = ( - (0.0, 1, 0, 0), - (1.0, 1, 1, 1), - (3.0, 1, 3, 9), - (7.0, 1, 7, 49), - ), # default data set useful for testing - header: Sequence[str] | None = None, + data: list[list[int | float]] | None = None, + header: list[str] | None = None, interpolate: int = 1, - **kwargs, + **kwargs: Any, ): + if data is None: + data = [ # default data set useful for testing + [0.0, 1, 0, 0], + [1.0, 1, 1, 1], + [3.0, 1, 3, 9], + [7.0, 1, 7, 49], + ] self._rows = len(data) assert self._rows > 0, "Empty lookup table detected, which does not make sense" self._cols = len(data[0]) - 1 @@ -46,7 +48,7 @@ def __init__( assert len(header) == self._cols, "Number of header elements does not match number of columns in data" self.header = tuple(header) self.outs = self.data[0] # initial values - self.interpolate = self.set_interpolate(interpolate) + self.interpolate = self.set_interpolate(int(interpolate)) def set_interpolate(self, interpolate: int): assert 0 <= interpolate <= 4, f"Erroneous interpolation exponent {self.interpolate}" @@ -54,7 +56,7 @@ def set_interpolate(self, interpolate: int): self.interpolate = interpolate return interpolate - def lookup(self, time): + def lookup(self, time: float): """Do a simulation step of size 'stepSize at time 'time.""" self.outs = self._bspl(time) return self.outs diff --git a/examples/time_table_fmu.py b/examples/time_table_fmu.py index cdae87f..acce822 100644 --- a/examples/time_table_fmu.py +++ b/examples/time_table_fmu.py @@ -1,5 +1,5 @@ import logging -from typing import Sequence +from typing import Any import numpy as np # noqa @@ -29,12 +29,14 @@ class TimeTableFMU(Model, TimeTable): # refer to Model first! def __init__( self, - data: Sequence = ((0.0, 1, 0, 0), (1.0, 1, 1, 1), (3.0, 1, 3, 9), (7.0, 1, 7, 49)), # data useful for testing - header: Sequence[str] | None = None, + data: list[list[int | float]] | None = None, + header: list[str] | None = None, interpolate: int = 1, default_experiment: dict[str, float] | None = None, - **kwargs, + **kwargs: Any, ): + if data is None: + data = [[0.0, 1, 0, 0], [1.0, 1, 1, 1], [3.0, 1, 3, 9], [7.0, 1, 7, 49]] # ex.data TimeTable.__init__(self, data, header, interpolate) if default_experiment is None: default_experiment = {"startTime": 0, "stopTime": 10.0, "stepSize": 0.1, "tolerance": 1e-5} @@ -58,7 +60,7 @@ def __init__( initial="exact", start=interpolate, rng=(0, 4), - on_set=self.set_interpolate, + on_set=self.set_interpolate, # type: ignore ) # the interpolation type can be set as parameter self._outs = Variable( self, diff --git a/pyproject.toml b/pyproject.toml index fb2fe46..567f24f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -149,7 +149,7 @@ typeCheckingMode = "basic" useLibraryCodeForTypes = true reportMissingParameterType = "error" reportUnknownParameterType = "warning" -reportUnknownMemberType = "warning" # consider to set to `false` if you work a lot with matplotlib and pandas, which are both not properly typed and known to trigger this warning +reportUnknownMemberType = false # consider to set to `false` if you work a lot with matplotlib and pandas, which are both not properly typed and known to trigger this warning reportMissingTypeArgument = "error" reportPropertyTypeMismatch = "error" reportFunctionMemberAccess = "warning" @@ -165,7 +165,7 @@ reportUnnecessaryIsInstance = "information" reportUnnecessaryCast = "warning" reportUnnecessaryComparison = "warning" reportUnnecessaryContains = "warning" -reportUnusedCallResult = "warning" +reportUnusedCallResult = false # was "warning" reportUnusedExpression = "warning" reportMatchNotExhaustive = "warning" reportUntypedFunctionDecorator = "warning" diff --git a/src/component_model/enums.py b/src/component_model/enums.py index 1de0b74..e86cdf1 100644 --- a/src/component_model/enums.py +++ b/src/component_model/enums.py @@ -1,28 +1,36 @@ """Additional Enum objects for component-model and enum-related utilities.""" import logging -from enum import Enum, IntFlag, EnumType +from enum import Enum, EnumType, IntFlag -# import pythonfmu.enums # type: ignore -from pythonfmu.enums import Fmi2Causality as Causality # type: ignore -from pythonfmu.enums import Fmi2Initial as Initial # type: ignore -from pythonfmu.enums import Fmi2Variability as Variability # type: ignore +from pythonfmu.enums import Fmi2Causality as Causality +from pythonfmu.enums import Fmi2Initial as Initial +from pythonfmu.enums import Fmi2Variability as Variability logger = logging.getLogger(__name__) -def ensure_enum(org: str | EnumType | None, default: EnumType | None) -> Enum | None: +def ensure_enum(org: str | Enum | None, default: Enum | EnumType | None) -> Enum | None: """Ensure that we have an Enum, based on the input as str, Enum or None.""" - if org is None: + if org is None and default is None: + return None + raise ValueError("org and default shall not both be None") from None + elif org is None: + assert isinstance(default, Enum), f"Need an Enum (member) as default if org=None. Found {type(default)}" return default - elif isinstance(org, str): - assert isinstance(default, Enum), "Need a default Enum here" - if org in type(default).__members__: - return type(default)[org] + elif default is None: + assert isinstance(org, Enum), "When no default is provided, org must be an Enum." + return org + elif isinstance(org, str): # both provided and org is a string + _default = default if isinstance(default, EnumType) else type(default) # need the Enum itself + if org in _default.__members__: + e: Enum = _default[org] # type: ignore[reportAssignmentType] + assert isinstance(e, Enum) + return e else: - raise Exception(f"The value {org} is not compatible with the Enum {type(default)}") from None - else: # expect already an Enum - assert default is None or isinstance(org, type(default)), f"{org} is not member of the Enum {type(default)}" + raise Exception(f"The value {org} is not compatible with the Enum {_default}") from None + else: # expect already an EnumType + assert isinstance(org, type(default)), f"{org} is not member of the Enum {type(default)}" return org @@ -85,14 +93,14 @@ def check_causality_variability_initial( variability: str | Enum | None, # EnumType | None, initial: str | Enum | None, ) -> tuple[Causality | None, Variability | None, Initial | None]: - _causality = ensure_enum(causality, Causality.parameter) # type: ignore - _variability = ensure_enum(variability, Variability.constant) # type: ignore + _causality = ensure_enum(causality, Causality.parameter) + _variability = ensure_enum(variability, Variability.constant) res = combination(_variability, _causality) # type: ignore if res in ("a", "b", "c", "d", "e"): # combination is not allowed logger.info(f"(causality {_causality}, variability {variability}) is not allowed: {explanations[res]}") return (None, None, None) else: # allowed - _initial = ensure_enum(initial, initial_default[res][0]) # type: ignore + _initial = ensure_enum(initial, initial_default[res][0]) if _initial not in initial_default[res][1]: logger.info(f"(Causality {_causality}, variability {_variability}, Initial {_initial}) is not allowed") return (None, None, None) diff --git a/src/component_model/model.py b/src/component_model/model.py index 41467e5..897e966 100644 --- a/src/component_model/model.py +++ b/src/component_model/model.py @@ -9,15 +9,15 @@ from math import log from numbers import Real from pathlib import Path -from typing import Any, Generator, TypeAlias +from typing import Any, Generator, Sequence, TypeAlias -from pythonfmu import Fmi2Slave, FmuBuilder # type: ignore[import-untyped] +from pythonfmu import Fmi2Slave, FmuBuilder from pythonfmu import __version__ as pythonfmu_version -from pythonfmu.default_experiment import DefaultExperiment # type: ignore[import-untyped] -from pythonfmu.enums import Fmi2Causality as Causality # type: ignore[import-untyped] -from pythonfmu.enums import Fmi2Initial as Initial # type: ignore[import-untyped] -from pythonfmu.enums import Fmi2Variability as Variability # type: ignore[import-untyped] -from pythonfmu.fmi2slave import FMI2_MODEL_OPTIONS # type: ignore[import-untyped] +from pythonfmu.default_experiment import DefaultExperiment +from pythonfmu.enums import Fmi2Causality as Causality +from pythonfmu.enums import Fmi2Initial as Initial +from pythonfmu.enums import Fmi2Variability as Variability +from pythonfmu.fmi2slave import FMI2_MODEL_OPTIONS from component_model.enums import ensure_enum from component_model.variable import Unit, Variable @@ -187,7 +187,7 @@ def _unit_ensure_registered(self, candidate: Variable): break self._units.append(cu) - def owner_hierarchy(self, parent: str | None) -> list[Variable]: + def owner_hierarchy(self, parent: str | None) -> Sequence["Model"]: """Analyse the parent of a variable down to the Model and return the owners as list.""" ownernames: list[tuple[str, int | None]] = [] assert isinstance(self.variable_naming, VariableNamingConvention), ( @@ -569,16 +569,9 @@ def xml_unit_definitions(self): du_done: list[str] = [] for _u in self._units: # list also the displays (if defined) if _u.u not in u_done and u.u == _u.u and _u.du is not None and _u.du not in du_done: - unit.append( - ET.Element( - "DisplayUnit", - { - "name": _u.du, - "factor": str(_u.to_base(1.0) - _u.to_base(0.0)), - "offset": str(_u.to_base(0.0)), - }, - ) - ) + fac = 1.0 if _u.to_base is None else _u.to_base(1.0) - _u.to_base(0.0) + offs = 0.0 if _u.to_base is None else _u.to_base(0.0) + unit.append(ET.Element("DisplayUnit", {"name": _u.du, "factor": str(fac), "offset": str(offs)})) if isinstance(_u.du, str): du_done.append(_u.du) u_done.append(u.u) @@ -589,14 +582,10 @@ def xml_unit_definitions(self): def _xml_default_experiment(self): attrib: dict[str, str] = {} if self.default_experiment is not None: - if self.default_experiment.start_time is not None: - attrib["startTime"] = str(self.default_experiment.start_time) - if self.default_experiment.stop_time is not None: - attrib["stopTime"] = str(self.default_experiment.stop_time) - if self.default_experiment.step_size is not None: - attrib["stepSize"] = str(self.default_experiment.step_size) - if self.default_experiment.tolerance is not None: - attrib["tolerance"] = str(self.default_experiment.tolerance) + attrib["startTime"] = str(self.default_experiment.start_time) + attrib["stopTime"] = str(self.default_experiment.stop_time) + attrib["stepSize"] = str(self.default_experiment.step_size) + attrib["tolerance"] = str(self.default_experiment.tolerance) de = ET.Element("DefaultExperiment", attrib) return de @@ -765,7 +754,7 @@ def _vrs_slices(self, vrs: tuple[int, ...] | list[int]) -> Generator[tuple[Varia raise AssertionError(f"valueReference={vr} does not exist in model {self.name}") from err if vr != _vr + 1 or test is not None: # new slice if var is not None: # only if initialized - yield (var, slice(start, start + i - i0), slice(i0, i)) # type: ignore + yield (var, slice(start, start + i - i0), slice(i0, i)) vr0 = vr i0 = i @@ -825,7 +814,6 @@ def _set( var.setter((values[_svr],), idx=_sv) else: # simple Variable var.setter(values[svr], idx=0) - # print(f"{self.name}. Set {vrs}:{values}") def set_integer( self, diff --git a/src/component_model/range.py b/src/component_model/range.py index c7157a9..f55028d 100644 --- a/src/component_model/range.py +++ b/src/component_model/range.py @@ -1,6 +1,6 @@ import logging from enum import Enum -from typing import Any +from typing import Any, Never, Sequence from component_model.unit import Unit @@ -11,9 +11,10 @@ class Range(object): """Utility class to store and handle the variable range of a single-valued variable. Args: - val: value for which the range is defined. At least an example value of the same type shall be provided. + val: value for which the range is defined. + At least an example value of the same type in base units shall be provided. rng (tuple) = (): Optional range of the variable in terms of a tuple of the same type as initial value. - Should be specified with units (as string). + Should be specified with units (as string) and is expected in display units. * If an empty tuple is specified, the range is automatically determined. That is only possible for float or enum type variables, where the former evaluates to (-inf, inf). @@ -23,47 +24,53 @@ class Range(object): `None` can be applied to the whole tuple or to single elements of the tuple. E.g. (1,None) sets the range to (1, start) * For some variable types (e.g. str) no range is expected. + * Internally, the range is stored in base units. + For range checking of a new value, the new value must be converted to base units before check. + unit (Unit): expected Unit (should be determined for start value before range is determined) """ def __init__( self, - val: bool|int|float|str|Enum, - rng: tuple[int|float|Enum|str|None,int|float|Enum|str|None]|None|tuple[()] = tuple(), # type: ignore[assignment] - unit: Unit|None = None, + val: bool | int | float | str | Enum, + rng: tuple[Any, Any] | None | Sequence[Never] = tuple(), + unit: Unit | None = None, ): - self.rng : tuple[int|bool|float|str, int|bool|float|str] + self.rng: tuple[int | bool | float | str, int | bool | float | str] typ = type(val) if unit is None: unit = Unit() assert isinstance(val, (bool, int, float, str, Enum)), f"Only primitive types allowed for Range. Found {typ}" if isinstance(val, str): - assert unit.u == "dimensionless", "A free string cannot have units." + assert unit.u == "", f"A free string cannot have units. Found {unit.u}" self.rng = (val, val) # no range for free strings - elif rng is None: # fixed value in any case - self.rng = (unit.from_base(val), unit.from_base(val)) + elif rng is None: # fixed value in any case. val provided in base units. No conversion + self.rng = (val, val) # type: ignore[assignment] ## see def above elif isinstance(rng, tuple) and not len(rng): # empty tuple => try automatic range self.rng = Range.auto_extreme(val) # fails if val is an int variable - else: # rng should be a 2-tuple for a bool, int, float, Enum variable. One or two elements might be None - assert isinstance(rng, tuple) and len(rng) == 2, f"Expect 2-tuple at this point. Found {rng}." - assert all( x is None or isinstance(x, (str,int,bool,float,Enum)) for x in rng), f"Unexpected type in {rng}" + elif ( + isinstance(rng, tuple) + and len(rng) == 2 + and all(x is None or isinstance(x, (str, int, bool, float, Enum)) for x in rng) + ): l_rng = list(rng) # work on a mutable object for i, r in enumerate(rng): - if r is None: - l_rng[i] = unit.from_base(val) # replace with fixed value 'val' as display value + if r is None: # fixed value on this side. val provided in base units. + l_rng[i] = val # type: ignore[reportArgumentType] ## l_rng is not empty # fixed display value else: - check, q = unit.compatible(r, typ, strict=True) + assert isinstance(r, (str, int, bool, float, Enum)), f"Found type {type(r)}" + check, q = unit.compatible(r, no_unit=False, strict=True) # q in base units if not check: - raise ValueError(f"Provided range {rng} is not conformant with unit {unit}") from None - q = unit.from_base(q) # ensure display units - assert isinstance(q, (int,bool,float)), "Unexpected type {type(q)} in {rng}[{i}]" + raise ValueError(f"Provided range {rng}[{i}] is not conformant with unit {unit}") from None + assert isinstance(q, (int, bool, float)), "Unexpected type {type(q)} in {rng}[{i}]" try: q = type(val)(q) # ensure correct Python type except Exception as err: raise TypeError(f"Incompatible types range {rng} - {val}") from err - l_rng[i] = q - self.rng = tuple(l_rng) # type: ignore ## cannot see how tuple contains str or None here! - + l_rng[i] = q # type: ignore[reportArgumentType] ## l_rng is not empty + self.rng = tuple(l_rng) # type: ignore ## cannot see how tuple contains str or None here! + else: + raise TypeError(f"Unhandled range specification {rng}) from None") @classmethod def auto_extreme(cls, var: bool | int | float | str | Enum | type) -> tuple[int | float | bool, int | float | bool]: @@ -106,30 +113,94 @@ def check( value: the Python value to check with respect to the internally defined Range typ (type): the expected Python type of the value unit (Unit): the Unit object related to the variable - disp (bool): check value as display units (True) or base units (False) + disp (bool): denotes whether 'value' is in display units (True) or base units (False) """ if unit is None: unit = Unit() if value is None: # denotes unchanged values (of compound variables) return True if not isinstance(value, typ): - try: - value = typ(value) # try to cast the values - except Exception: # give up - return False + if issubclass(typ, Enum): + if isinstance(value, Enum): + value = value.value + assert isinstance(value, int) and isinstance(self.rng[0], int) and isinstance(self.rng[0], int), ( + f"Enum range is managed as int. Found {self.rng}, {value}" + ) + return self.rng[0] <= value <= self.rng[1] # type: ignore[operator] ## all arguments int! + else: + try: + assert typ is not None, "Need a proper typ argument here" + value = typ(value) # type: ignore ## try to cast the values + except Exception: # give up + return False # special types if typ is str: # no range checking on str return True elif typ is bool: return isinstance(value, bool) elif isinstance(value, Enum): - return isinstance(value, typ) - + return self.rng[0] <= value.value <= self.rng[1] # type: ignore[operator] ## There is no str involved! elif isinstance(value, (int, float)) and all(isinstance(x, (int, float)) for x in self.rng): - assert typ is int or typ is float, f"Inconsistent type {typ}. Expect int or float" - if not disp and unit.du is not None: # check an internal unit values - value = unit.from_base(value) - return self.rng[0] <= value <= self.rng[1] # type: ignore[operator] ## There is no str involved! + # assert typ is int or typ is float, f"Inconsistent type {typ} for value {value}. Expected int or float" + if disp and unit.to_base is not None: # check a display unit values + value = unit.to_base(value) + return self.rng[0] <= value <= self.rng[1] # type: ignore[operator] ## There is no str involved! else: logger.error(f"range check(): value={value}, type={typ}, range={self.rng}") return False + + @classmethod + def is_valid_spec( + cls, rng: tuple[Any, ...] | tuple[Any, Any] | None | Sequence[Never], var_len: int, typ: type, level: int = 0 + ) -> int: + """Check whether the supplied rng is a valid range specification for a variable. + Applies to scalar and compound variable specs. + Return 0 (ok) or error code >0 if not ok. + """ + if rng is None: + ck = 0 # fixed value(s) + elif isinstance(rng, tuple) and not len(rng): # all automatic + ck = int(typ is int) # 1/0 (not possible for int) + elif isinstance(rng, tuple): # need a tuple now + if var_len == 1: + if len(rng) != 2: # scalar specified by a 2-tuple + ck = 2 + else: # final check of scalar spec 2-tuple + ck = 0 + for i, r in enumerate(rng): + if r is not None and not isinstance(r, (int, bool, float, Enum, str)): + ck += 10 + i + if rng[i] is not None and not any(isinstance(rng[i], str) for i in range(2)): + if rng[0] > rng[1]: # wrong order + ck += 10 + 9 + + elif var_len > 1: + if len(rng) != var_len: # one range for each variable + ck = 3 + else: + ck = 0 + for i, r in enumerate(rng): + ck += Range.is_valid_spec(r, 1, typ, level=(i + 1) * 100) + else: + ck = 4 # would need a tuple here + return ck if ck == 0 else level + ck + + @classmethod + def err_code_msg(cls, code: int) -> str: + if code == 0: + return "Ok" + elif code == 1: + return "Automatic range for int variables is not defined." + elif code == 2: + return "Full range specification of scalar expects a 2-tuple" + elif code == 3: + return "Range specification of compound variable expects one spec per sub-variable." + elif 10 <= code < 19: + return "Wrong entry in full range specification of scalar." + elif code == 19: + return "Wrong order of entries in full range specification of scalar" + elif code > 100: + sub = Range.err_code_msg(code % 100) + return f"Error in compound variable: {sub}" + else: + return f"Unknown error code {code}" diff --git a/src/component_model/unit.py b/src/component_model/unit.py index 4496333..3b51f11 100644 --- a/src/component_model/unit.py +++ b/src/component_model/unit.py @@ -1,7 +1,7 @@ import logging from enum import Enum from functools import partial -from typing import Callable +from typing import Any, Callable import numpy as np from pint import Quantity, UnitRegistry # management of units @@ -11,22 +11,33 @@ class Unit: """Helper class to store and manage units and display units, - i.e. base unit of variable and unit differences 'outside' and 'inside' the model. + i.e. base unit of variable and unit differences 'outside'(display units) and 'inside'(base units) the model. - One Unit object represents one scalar variable. + Args: + quantity (bool, int, float, str, Enum, None): The quantity to be disected for unit definition. 3 possibilites: + + * None: no units. Instantiates an 'empty' Unit object + * str: is parsed to disect the unit. + If a unit is identified this is treated as a variable with units (and possibly display units) + If no unit is identified this is treated as a free string variable (no units). Use 'None' to ensure free str. + * bool, int, Enum: variables with no units and no display units + + * one Unit object represents one scalar variable. + * many variables do not have units (i.e. str, Enum, int variables). If These get the unit .u="" + * variables without separate display units get the display unit .du=None and a reduced set of properties + * only float variables may have separate display units and transformations """ - _ureg: UnitRegistry | None = None + _ureg: UnitRegistry[Any] | None = None - def __init__(self, quantity: bool | int | float | str | Enum | None = None, typ: type | None = None): + def __init__(self, quantity: bool | int | float | str | Enum | None = None): assert Unit._ureg is not None, "Before units can be instantiated, Unit.ensure_unit_registry() must be called." - # properties with default values. Initialized through parse_quantity - self.u: str = "dimensionless" # default: dimensionless unit (placeholder) + self.u: str = "" # default: no units self.du: str | None = None # display unit (default: same as u, no transformation) - self.to_base: Callable[float] = partial(Unit.identity) # f(display-value) -> base-value - self.from_base: Callable[float] = partial(Unit.identity) # f(base-value) -> display-value + self.to_base: Callable[[Any], Any] = Unit.identity # default transformation is identity + self.from_base: Callable[[Any], Any] = Unit.identity # default transformation is identity if quantity is not None: # if parse-value is called on class it also returns the (parsed,converted) base-value - _val = self.parse_quantity(quantity, typ) + _val = self.parse_quantity(quantity) @classmethod def ensure_unit_registry(cls, system: str = "SI", autoconvert: bool = True): @@ -35,15 +46,13 @@ def ensure_unit_registry(cls, system: str = "SI", autoconvert: bool = True): def __str__(self): txt = f"Unit {self.u}, display:{self.du}" - if self.du is not None: + if self.du is not None and not (self.to_base is None or self.from_base is None): txt += f". Offset:{self.to_base(0)}, factor:{self.to_base(1.0) - self.to_base(0.0)}" return txt - def parse_quantity( - self, quantity: bool | int | float | str | Enum, typ: type | None = None - ) -> bool | int | float | str | Enum: + def parse_quantity(self, quantity: bool | int | float | str | Enum) -> bool | int | float | str | Enum: """Parse the provided quantity in terms of magnitude and unit, if provided as string. - If another type is provided, dimensionless units are assumed. + If another type is provided, no units are assumed. Args: quantity: the quantity to disect. Should be provided as string, but also the trivial cases (int,float,Enum) are allowed. @@ -52,50 +61,39 @@ def parse_quantity( the magnitude in base units, the base unit and the unit as given (display units), together with the conversion functions between the units. """ - if typ is str: - self.u = "dimensionless" - self.du = None - val = quantity - elif isinstance(quantity, str): # only string variable make sense to disect + if isinstance(quantity, str): # only string variable make sense to disect + assert Unit._ureg is not None, "UnitRegistry not yet instantiated!" try: q = Unit._ureg(quantity) # parse the quantity-unit and return a Pint Quantity object if isinstance(q, (int, float)): - self.u = "" + self.u = "dimensionless" self.du = None return q # integer or float variable with no units provided elif isinstance(q, Quantity): # pint.Quantity object # transform to base units ('SI' units). All internal calculations will be performed with these val = self.val_unit_display(q) - else: - logger.critical(f"Unknown quantity {quantity} to disect") - raise ValueError(f"Unknown quantity {quantity} to disect") from None - # no recognized units. Assume a free string. ??Maybe we should be more selective about the exact error type: - except Exception as warn: - logger.warning(f"Unhandled quantity {quantity}: {warn}. A str? Set explicit 'typ=str'.") - self.u = "" - self.du = None - val = str(quantity) - else: - self.u = "dimensionless" - self.du = None - val = quantity - if typ is not None and type(val) is not typ: # check variable type - try: # try to convert the magnitude to the correct type. - val = typ(val) + return val + else: # since this is not a recognized quantity, we assume an implicit str + pass except Exception as err: - logger.critical(f"Value {val} is not of the correct type {typ}") - raise TypeError(f"Value {val} is not of the correct type {typ}") from err + logger.warning(f"Quantity {quantity} could not be disected: {err}. Assume free string.") + self.u = "" + self.du = None + return quantity + + @classmethod + def identity(cls, val: Any) -> Any: return val @classmethod - def linear(cls, x: float, b: float, a: float = 0.0): - return a + b * x + def slope(cls, val: float, slope: float) -> float: + return slope * val @classmethod - def identity(cls, x: float): - return x + def linear(cls, val: float, intercept: float, slope: float) -> float: + return intercept + slope * val - def val_unit_display(self, q: Quantity[float]) -> float: + def val_unit_display(self, q: Quantity[int | float]) -> int | float: """Identify base units and calculate the transformations between display and base units. Returns @@ -122,35 +120,41 @@ def val_unit_display(self, q: Quantity[float]) -> float: qb2 = q2.to_base_units() a = (qb.magnitude * q2.magnitude - qb2.magnitude * q.magnitude) / (q2.magnitude - q.magnitude) b = (qb2.magnitude - qb.magnitude) / (q2.magnitude - q.magnitude) - if abs(a) < 1e-9: # multiplicative conversion - if abs(b - 1.0) < 1e-9: # unit and display unit are compatible. No transformation - self.du = None - self.to_base = partial(Unit.linear, b=b) - self.from_base = partial(Unit.linear, b=1.0 / b) + if abs(a) < 1e-9 and abs(b) < 1e-9: # identity + self.to_base = self.from_base = Unit.identity + if abs(a) < 1e-9: # multiplicative conversion (only slope) + self.to_base = partial(Unit.slope, slope=b) + self.from_base = partial(Unit.slope, slope=1.0 / b) else: # there is a constant (e.g. Celsius to Fahrenheit) - self.to_base = partial(Unit.linear, b=b, a=a) - self.from_base = partial(Unit.linear, b=1.0 / b, a=-a / b) + self.to_base = partial(Unit.linear, intercept=a, slope=b) + self.from_base = partial(Unit.linear, intercept=-a / b, slope=1.0 / b) return val @classmethod def make( - cls, quantity: bool | int | float | str | Enum, typ: type | None = None + cls, quantity: bool | int | float | str | Enum, no_unit: bool = False ) -> tuple[tuple[bool | int | float | str | Enum], tuple["Unit"]]: + """Parse quantity and return the resulting value and its unit object. + If no_unit, only a default object is generated. + """ u = Unit() - val = u.parse_quantity(quantity, typ) - return ((val,), (u,)) + if no_unit: + return ((quantity,), (u,)) + else: + val = u.parse_quantity(quantity) + return ((val,), (u,)) @classmethod def make_tuple( cls, - quantities: tuple[bool|int|float|str|Enum, ...] |list[bool|int|float|str|Enum] | np.ndarray, - typ: type | None = None - ) -> tuple[tuple[bool|int|float|str|Enum, ...], tuple["Unit", ...]]: - """Make a tuple of Unit objects from the tuple of quantities.""" + quantities: tuple[bool | int | float | str | Enum, ...] | list[bool | int | float | str | Enum] | np.ndarray, + no_unit: bool = False, + ) -> tuple[tuple[bool | int | float | str | Enum, ...], tuple["Unit", ...]]: + """Make a tuple of values and Unit objects from the tuple of quantities, using make().""" values: list[bool | int | float | str | Enum] = [] units: list[Unit] = [] for q in quantities: - val, u = cls.make(q, typ) + val, u = cls.make(q, no_unit=no_unit) values.extend(val) units.extend(u) return (tuple(values), tuple(units)) @@ -162,28 +166,25 @@ def derivative(cls, baseunits: tuple["Unit", ...], tu: str = "s") -> tuple[tuple for bu in baseunits: u = Unit() u.u = f"{bu.u}/{tu}" + u.to_base = bu.to_base # link the functions + u.from_base = bu.from_base # link the functions u.du = None if bu.du is None else f"{bu.du}/{tu}" - if bu.du is not None: - u.to_base = bu.to_base - u.from_base = bu.from_base units.append(u) values = [0.0] * len(baseunits) return (tuple(values), tuple(units)) def compatible( - self, quantity: bool | int | float | str | Enum, typ: type | None = None, strict: bool = True + self, quantity: bool | int | float | str | Enum, no_unit: bool = False, strict: bool = True ) -> tuple[bool, bool | int | float | str | Enum]: """Check whether the supplied quantity 'q' is compatible with this unit. If strict==True, the supplied quantity shall be in display units. """ - _q, _unit = Unit.make(quantity, typ) + _q, _unit = Unit.make(quantity, no_unit=no_unit) q = _q[0] unit = _unit[0] # no explicit unit needed when the quantity is 0 or inf (anything compatible) if ( - ( - (q == 0 or q == float("inf") or q == float("-inf")) and unit.u == "dimensionless" - ) # 0, +/-inf without unit + (q == 0 or q == float("inf") or q == float("-inf")) # 0, +/-inf with any unit or (strict and self.u == unit.u and self.du == unit.du) or (not strict and self.u == unit.u) ): diff --git a/src/component_model/utils/transform.py b/src/component_model/utils/transform.py index bce336f..f0a99f5 100644 --- a/src/component_model/utils/transform.py +++ b/src/component_model/utils/transform.py @@ -97,7 +97,7 @@ def euler_rot_spherical( tp = [np.radians(x) for x in tp] st = np.sin(tp[0]) # rotate the cartesian vector (r is definitely not a list, even if pyright might think so) - x = r.apply((st * np.cos(tp[1]), st * np.sin(tp[1]), np.cos(tp[0]))) # type: ignore[reportAttributeAccessIssue] + x = r.apply((st * np.cos(tp[1]), st * np.sin(tp[1]), np.cos(tp[0]))) x2 = x[2] if abs(x2) < 1.0: pass @@ -137,7 +137,7 @@ def rot_from_vectors(vec1: np.ndarray, vec2: np.ndarray) -> Rot: if abs(n - 1.0) > 1e-10: vec1 /= n vec2 /= n - _c = vec1.dot(vec2) # type: ignore + _c = vec1.dot(vec2) if abs(_c + 1.0) < 1e-10: # vectors are exactly opposite to each other imax, vmax, _sum = (-1, float("-inf"), 0.0) for k, v in enumerate(vec1): diff --git a/src/component_model/variable.py b/src/component_model/variable.py index d669dba..072b1a3 100644 --- a/src/component_model/variable.py +++ b/src/component_model/variable.py @@ -3,13 +3,13 @@ import logging import xml.etree.ElementTree as ET # noqa: N817 from enum import Enum -from typing import Any, Callable, Sequence, TypeAlias +from typing import Any, Callable, Never, Sequence, TypeAlias import numpy as np -from pythonfmu.enums import Fmi2Causality as Causality # type: ignore -from pythonfmu.enums import Fmi2Initial as Initial # type: ignore -from pythonfmu.enums import Fmi2Variability as Variability # type: ignore -from pythonfmu.variables import ScalarVariable # type: ignore +from pythonfmu.enums import Fmi2Causality as Causality +from pythonfmu.enums import Fmi2Initial as Initial +from pythonfmu.enums import Fmi2Variability as Variability +from pythonfmu.variables import ScalarVariable from component_model.enums import Check, check_causality_variability_initial, use_start from component_model.range import Range @@ -18,7 +18,8 @@ logger = logging.getLogger(__name__) PyType: TypeAlias = str | int | float | bool | Enum -RngSingle: TypeAlias = tuple[int | float | None, int | float | None] | None | tuple[()] +# RngSingle: TypeAlias = tuple[int | float | None, int | float | None] | None | tuple[()] +RngSingle: TypeAlias = tuple[Any, Any] | None | Sequence[Never] Numeric: TypeAlias = int | float Compound: TypeAlias = tuple[PyType, ...] | list[PyType] | np.ndarray @@ -67,7 +68,7 @@ class Variable(ScalarVariable): typ (type)=None: Optional explicit type of variable to expect as start and value. Since initial values are often set with strings (with units, see below), this is set explicitly. If None, _typ is set to Enum/str if derived from these after disection or float if a number. 'int' is not automatically detected. - start (PyType): The initial value of the variable. + start (PyType): The initial value(s) of the variable. Optionally, the unit can be included, providing the initial value as string, evaluating to quantity of type typ a display unit and base unit. @@ -121,8 +122,8 @@ def __init__( rng: RngSingle | tuple[RngSingle, ...] = tuple(), annotations: dict[str, Any] | None = None, value_check: Check = Check.all, - on_step: Callable | None = None, - on_set: Callable | None = None, + on_step: Callable[[float, float], None] | None = None, + on_set: Callable[[int | float | np.ndarray], int | float | np.ndarray] | None = None, owner: Any | None = None, local_name: str | None = None, ): @@ -168,8 +169,6 @@ def __init__( assert isinstance(basevar, Variable), f"The primitive of {self.name} must be a Variable object" assert basevar.typ is float, f"The primitive of {self.name} shall be float. Found {basevar.typ}" self._typ = float - if start is None: - self._start, self._unit = Unit.derivative(basevar.unit) if self.on_step is None: self.on_step = self.der1 else: @@ -177,28 +176,43 @@ def __init__( else: self.local_name = local_name # use explicitly provided local name + if start is None: + assert local_name is None, f"{self.name} Default start value only defined for derivatives" + assert basevar is not None, f"{self.name} basevar needed at this point" + self._start, self._unit = Unit.derivative(basevar.unit) + elif self._typ is str: + assert isinstance(start, str), f"Scalar str expected. Found {start}" + self._start, self._unit = (start,), (Unit(None),) # explicit free string variable + elif not isinstance(start, (tuple, list, np.ndarray)): + self._start, self._unit = Unit.make(start, no_unit=False) + else: + self._start, self._unit = Unit.make_tuple(start, no_unit=False) + self._len = 1 if self._typ is str else len(self._start) + if self._typ is None: # try to adapt using _start and _unit + self._typ = self.auto_type(self._start, self._unit) + assert isinstance(self._typ, type) + if self._typ is not Enum: # Enums are already checked and casting does not work + self._start = tuple([self._typ(s) for s in self._start]) # make sure that python type is correct + + ck = Range.is_valid_spec(rng, self._len, self._typ) + if ck != 0: + raise ValueError(f"{self.name} invalid range spec '{rng}'. Error: {Range.err_code_msg(ck)}") from None + self._range: tuple[Range, ...] + # Defining the _range. self._start is in base units, while rng is in display units if self._typ is str: # explicit free string. String arrays are so far not implemented assert isinstance(start, str) - self._len = 1 - self._start, self._unit = Unit.make(start, typ=str) self._range = (Range(self._start[0], unit=self._unit[0]),) # Strings have fixed range else: - # if type is provided and no (initial) value. We set a default value of the correct type as 'example' value - if not len(self._start): # not yet set - assert start is not None, f"{self.name}: Provide start value, at least for type and unit determination" - if isinstance(start, (tuple, list, np.ndarray)): - self._start, self._unit = Unit.make_tuple(start, self._typ) - else: - self._start, self._unit = Unit.make(start, self._typ) - self._len = len(self._start) - if self._typ is None: # try to adapt using start - self._typ = self.auto_type(self._start) - assert isinstance(self._typ, type) - self._start = tuple([self._typ(s) for s in self._start]) # make sure that python type is correct if self._len == 1: - self._range = (Range(self._start[0], rng, self._unit[0]),) + self._range = (Range(self._start[0], rng, self._unit[0]),) # type: ignore[arg-type] ## is_valid_spec else: - self._range = tuple([Range(self._start[i], rng[i], self._unit[i]) for i in range(self._len)]) + _rng: list[Range] = [] + for i in range(self._len): + if rng is None or not len(rng): + _rng.append(Range(self._start[i], rng, self._unit[i])) # type: ignore[arg-type] + else: + _rng.append(Range(self._start[i], rng[i], self._unit[i])) + self._range = tuple(_rng) if not self.check_range(self._start, disp=False): # range checks of initial value logger.critical(f"The provided value {self._start} is not in the valid range {self._range}") @@ -212,25 +226,30 @@ def __init__( def der1(self, current_time: float, step_size: float): """Ramp the base variable value up or down within step_size.""" - der = getattr(self.owner, self.local_name) # the current slope value - if (isinstance(der, float) and der != 0.0) or ( - isinstance(der, (Sequence, np.ndarray)) and any(x != 0.0 for x in der) - ): # there is a slope - # varname = self.local_name[5:] # local name of the base variable + der = np.array(getattr(self.owner, self.local_name)) # the current slope value + if not np.allclose(der, 0.0): basevar = self.model.derivatives[self.name] # base variable object - val = getattr( - self.owner, basevar.local_name - ) # getattr(self.owner, varname) # previous value of base variable # - if not isinstance(der, (Sequence, np.ndarray)): - der = [der] - assert not isinstance(val, (Sequence, np.ndarray)), "Should be the same as der" - val = [val] - if isinstance(val, np.ndarray): - newval = val + step_size * np.array(der, float) - basevar.setter_internal(newval, -1, True) - else: - newval_list = [val[i] + step_size * der[i] for i in range(len(der))] - basevar.setter_internal(newval_list, -1, False) + val = np.array(getattr(self.owner, basevar.local_name)) # previous value of base variable + newval = val + step_size * der + basevar.setter_internal(newval, -1) # , True) + + # def _parse_start( start:None|PyType|tuple[PyType])-> tuple[PyType|np.ndarray,): + # """Read start value(s), extract unit(s) and return everything needed for the simulation.""" + # if start is None: + # assert local_name is None, f"{self.name} Default start value only defined for derivatives" + # assert basevar is not None, f"{self.name} basevar needed at this point" + # self._start, self._unit = Unit.derivative(basevar.unit) + # elif self._typ is str or self._typ is Enum: + # + # not isinstance(start, tuple)): + # self._start, self._unit = Unit.make(start, no_unit=True) # type: ignore ## type of start should be ok + # else: + # self._start, self._unit = Unit.make_tuple(start, no_unit=False) + # self._len = 1 if self._typ is str else len(self._start) + # if self._typ is None: # try to adapt using start + # self._typ = self.auto_type(self._start) + # assert isinstance(self._typ, type) + # self._start = tuple([self._typ(s) for s in self._start]) # make sure that python type is correct # disable super() functions and properties which are not in use here def to_xml(self) -> ET.Element: @@ -242,16 +261,9 @@ def __len__(self) -> int: return self._len # This works also compound variables, as long as _len is properly set @property - def start(self): + def start(self) -> tuple[PyType, ...]: return self._start - @start.setter - def start(self, val: PyType | Compound): - if isinstance(val, (Sequence, np.ndarray)): - self._start = tuple(val) - else: - self._start = (val,) - @property def unit(self): """Get the unit object.""" @@ -290,15 +302,15 @@ def setter(self, values: Sequence[int | float | bool | str | Enum] | np.ndarray, """ dvals: list[int | float | bool | str | Enum | None] logger.debug(f"SETTER0 {self.name}, {values}[{idx}] => {getattr(self.owner, self.local_name)}") - is_ndarray = isinstance(values, np.ndarray) assert self._typ is not None, "Need a proper type at this stage" assert isinstance(values, (Sequence, np.ndarray)), "A sequence is expected as values" if idx == -1 and self._len == 0: # the whole scalar idx = 0 - if issubclass(self._typ, Enum): # Enum types are supplied as int. Convert + if issubclass(self._typ, Enum): # Enum types may be supplied as int. Convert for i in range(self._len): - values[i] = self._typ(values[i]) # type: ignore + if isinstance(values[i], int): + values[i] = self._typ(values[i]) # type: ignore if self._check & Check.ranges: # do that before unit conversion, since range is stored in display units! if not self.check_range(values, idx): @@ -306,21 +318,14 @@ def setter(self, values: Sequence[int | float | bool | str | Enum] | np.ndarray, if self._check & Check.units: #'values' expected as displayUnit. Convert to unit if idx >= 0: # explicit index of single values - if self._unit[idx].du is None: - dvals = list(values) - else: - # assert isinstance(values[0], float) - dvals = [self._unit[idx].to_base(values[0])] # type: ignore ## values[0] is float! + dvals = [self._unit[idx].to_base(values[0])] # type: ignore else: # the whole array dvals = [] for i in range(self._len): if values[i] is None: # keep the value dvals.append(getattr(self.owner, self.local_name)[i]) - elif self._unit[i].du is None: - dvals.append(values[i]) else: - # assert isinstance(values[i], float) or (self._typ is int and isinstance(values[i], int)) - dvals.append(self._unit[i].to_base(values[i])) # type: ignore ## it is a float! + dvals.append(self._unit[i].to_base(values[i])) else: # no unit issues if self._len == 1: dvals = [values[0] if values[0] is not None else getattr(self.owner, self.local_name)] @@ -329,17 +334,20 @@ def setter(self, values: Sequence[int | float | bool | str | Enum] | np.ndarray, values[i] if values[i] is not None else getattr(self.owner, self.local_name)[i] for i in range(self._len) ] - self.setter_internal(dvals, idx, is_ndarray) # do the setting, or flag as dirty + self.setter_internal(dvals, idx) # do the setting, or flag as dirty def setter_internal( self, values: Sequence[int | float | bool | str | Enum | None] | np.ndarray, idx: int = -1, - is_ndarray: bool = False, ): """Do internal setting of values (no range checking and units expected internal), including dirty flags.""" if self._len == 1: - setattr(self.owner, self.local_name, values[0] if self.on_set is None else self.on_set(values[0])) # type: ignore + try: + _val = values[0] + except IndexError: # Exception as err: + _val = values + setattr(self.owner, self.local_name, _val if self.on_set is None else self.on_set(_val)) # type: ignore elif idx >= 0: if values[0] is not None: # Note: only the indexed value is provided, as list! val = getattr(self.owner, self.local_name) @@ -348,11 +356,8 @@ def setter_internal( if self.on_set is not None: self.model.dirty_ensure(self) else: # the whole array - if is_ndarray: # Note: on_set might contain array operations - arr: np.ndarray = np.array(values, self._typ) - setattr(self.owner, self.local_name, arr if self.on_set is None else self.on_set(arr)) - else: - setattr(self.owner, self.local_name, values if self.on_set is None else self.on_set(values)) + arr: np.ndarray = np.array(values, self._typ) + setattr(self.owner, self.local_name, arr if self.on_set is None else self.on_set(arr)) if self.on_set is None: logger.debug(f"SETTER {self.name}, {values}[{idx}] => {getattr(self.owner, self.local_name)}") @@ -368,12 +373,11 @@ def getter(self) -> list[PyType]: values = [value.value] else: if not isinstance(value, self._typ): # other type conversion - value = self._typ(value) # type: ignore[call-arg] + value = self._typ(value) # type: ignore[call-arg] ## only mypy if self._check & Check.units: # Convert 'value' base unit -> display.u - if self._unit[0].du is not None: - assert isinstance(value, float) - value = self._unit[0].from_base(value) - values = [value] + values = [self._unit[0].from_base(value)] + else: + values = [value] else: # compound variable values = list(getattr(self.owner, self.local_name)) # make value available as copy @@ -383,74 +387,15 @@ def getter(self) -> list[PyType]: else: for i in range(self._len): # check whether conversion to _typ is necessary if not isinstance(values[i], self._typ): - values[i] = self._typ(values[i]) # type: ignore[call-arg] + values[i] = self._typ(values[i]) # type: ignore[call-arg] ## only mypy if self._check & Check.units: # Convert 'value' base unit -> display.u for i in range(self._len): - if self._unit[i].du is not None: - values[i] = self._unit[i].from_base(values[i]) + values[i] = self._unit[i].from_base(values[i]) if self._check & Check.ranges and not self.check_range(values, -1): # check the range if so instructed logger.error(f"getter(): Value of {self.name}: {values} outside range {self.range}!") return values - # def _init_range(self, rng: RngSpec) -> tuple[tuple[Any,Any],None]: - # """Initialize the variable range(s) of the variable - # The _start and _unit shall exist when calling this. - # - # Args: - # rng (tuple): The tuple of range tuples. - # Always for the whole variable with scalar variables packed in a singleton - # """ - # - # assert hasattr(self, "_start") and hasattr(self, "_unit"), "Missing self._start / self._unit" - # assert isinstance(self._typ, type), "init_range(): Need a defined _typ at this stage" - # # Configure input. Could be None, () or (min,max) of scalar - # if rng is None or rng == tuple() or (self._len == 1 and len(rng) == 2): - # rng = (rng,) * self._len - # - # _range = [] - # for idx in range(self._len): # go through all elements - # assert rng is not None, "rng None detected" - # _rng = rng[idx] - # if _rng is None: # => no range. Used for compound variables if not all elements have a range - # s0 = self._start[idx] - # assert isinstance(s0, float) - # v = self._unit[idx].from_base(s0) if self._unit[idx].du is not None else s0 - # _range.append((v, v)) - # elif isinstance(_rng, tuple) and not len(_rng): # empty tuple => try automatic range - # _range.append(self._auto_extreme(self._start[idx])) - # elif isinstance(_rng, tuple) and len(_rng) == 2: # normal range as 2-tuple - # i_range: list[float] = [] # collect range as list - # for r in _rng: - # if r is None: # no range => fixed to initial value - # q = self._start[idx] - # else: - # check, q = self._unit[idx].compatible(r, self.model.ureg, self._typ, strict=True) - # if not check: - # check, q = self._unit[idx].compatible(r, self.model.ureg, self._typ, strict=False) - # if check: - # logger.warn(f"{self.name}[{idx}] range {r}: Use display units {self._unit[idx].du}!") - # else: - # msg = f"{self.name}[{idx}]: range {r} not conformant to the unit type {self._unit[idx]}" - # logger.critical(msg) - # raise VariableInitError(msg) - # assert isinstance(q, float) or (self._typ is int and isinstance(q, int)) - # if self._unit[idx].du is not None: - # q = self._unit[idx].from_base(q) - # i_range.append(q) - # - # try: # check variable type - # i_range = [self._typ(x) for x in i_range] - # except Exception as err: - # logger.critical(f"Incompatible types range {rng} - {self._start}") - # raise VariableRangeError(f"Incompatible types range {rng} - {self._start}") from err - # assert all(isinstance(x, self._typ) for x in i_range) - # _range.append(tuple(i_range)) # type: ignore - # else: - # logger.critical(f"init_range(): Unhandled range argument {rng}") - # raise AssertionError(f"init_range(): Unhandled range argument {rng}") - # return tuple(_range) - def check_range(self, values: Sequence[PyType | None] | np.ndarray, idx: int = 0, disp: bool = True) -> bool: """Check the provided 'values' with respect to the range. @@ -487,48 +432,33 @@ def fmi_type_str(self, val: PyType) -> str: return str(val) @classmethod - def auto_type(cls, val: PyType | Compound, allow_int: bool = False): - """Determine the Variable type from a provided example value. + def auto_type(cls, vals: tuple[PyType, ...], units: tuple[Unit, ...]) -> type: + """Determine the Variable type from a set of example values and related Unit objects. + + Variable type must be unique for the whole set of vals/units. Since variables can be initialized using strings with units, - the type can only be determined when the value is disected. + the type can only be determined when the value is disected and the units defined. Moreover, the value may indicate an integer, while the variable is designed a float. - Therefore int Variables must be explicitly specified. + int type is therefore only decided if all vals are int and if no unit is disected. """ - assert val is not None, "'val is None'!" - if isinstance(val, (tuple, list, np.ndarray)): - types = [cls.auto_type(x, allow_int) for x in val] - typ = None - for t in types: - if t is not None and typ is None: - typ = t - elif t is not None and typ is not None: - if t == typ: - pass - elif t != typ: # identify the super-type - if issubclass(t, typ): # is a sub-class. Ok - pass - elif issubclass(typ, t): - typ = t - elif typ is float and t is int: # we allow that, even if no subclass - pass - elif typ is int and t is float: # we allow that, even if no subclass - typ = float - else: - logger.critical(f"Incompatible variable types {typ}, {t} in {val}") - raise TypeError(f"Incompatible variable types {typ}, {t} in {val}") from None - else: - logger.critical(f"auto_type(). Unhandled {t}, {typ}") - raise ValueError(f"auto_type(). Unhandled {t}, {typ}") - return typ - else: # single value - if isinstance(val, bool): - return bool - elif allow_int: - return type(val) - elif not allow_int and isinstance(val, (int, float)): + types: list[type] = [] + for v, u in zip(vals, units, strict=True): + types.append(type(v)) + if isinstance(v, (bool, Enum, str)) and u.u != "": + raise ValueError(f"{type(v).__name__} value {v} with unit '{u.u}' is not allowed.") + elif isinstance(v, int): + if u.u != "": # must be a 'hidden float' + types[-1] = float + if len(types) == 1 or all(types[0] is t for t in types[1:]): # all element types equal + if issubclass(types[0], float): # e.g. numpy.float64 is tracked as float return float else: - return type(val) + return types[0] + if any(t is float for t in types) and all(t is float or t is int for t in types): # int&float -> float + return float + else: + _units = tuple([u.u for u in units]) + raise ValueError(f"Auto-type cannot be determined for values {vals} with units {_units}") @classmethod def _auto_extreme(cls, var: PyType) -> tuple[float | bool, ...]: @@ -565,7 +495,6 @@ def xml_scalarvariables(self): ------- List of ScalarVariable xml elements """ - _type = {"int": "Integer", "bool": "Boolean", "float": "Real", "str": "String", "Enum": "Enumeration"}[ self.typ.__qualname__ ] # translation of python to FMI primitives. Same for all components @@ -597,18 +526,18 @@ def xml_scalarvariables(self): # detailed variable definition info = ET.Element(_type) if do_use_start: # a start value is to be used - info.attrib.update({"start": self.fmi_type_str(self._start[i])}) + info.attrib.update({"start": self.fmi_type_str(self._unit[i].from_base(self._start[i]))}) if _type in ("Real", "Integer", "Enumeration"): # range to be specified - xMin = self.range[i].rng[0] - if _type != "Real" or xMin > float("-inf"): - info.attrib.update({"min": str(xMin)}) - else: + xmin = self.range[i].rng[0] + if _type == "Real" and isinstance(xmin, float) and xmin == float("-inf"): info.attrib.update({"unbounded": "true"}) - xMax = self.range[i].rng[1] - if _type != "Real" or xMax < float("inf"): - info.attrib.update({"max": str(xMax)}) else: + info.attrib.update({"min": str(self._unit[i].from_base(xmin))}) + xmax = self.range[i].rng[1] + if _type == "Real" and isinstance(xmax, float) and xmax == float("inf"): info.attrib.update({"unbounded": "true"}) + else: + info.attrib.update({"max": str(self._unit[i].from_base(xmax))}) if _type == "Real": # other attributes apply only to Real variables info.attrib.update({"unit": self.unit[i].u}) if isinstance(self._unit[i].du, str) and self.unit[i].du != self._unit[i].u: diff --git a/src/component_model/variable_naming.py b/src/component_model/variable_naming.py index 207c4b8..b53b071 100644 --- a/src/component_model/variable_naming.py +++ b/src/component_model/variable_naming.py @@ -27,19 +27,21 @@ class ParsedVariable: * der: unsigned integer, defining the derivation order. 0 for no derivation """ - def __init__(self, varname: str, convention: VariableNamingConvention = VariableNamingConvention.structured): + def __init__(self, varname: str, convention: Enum | None = VariableNamingConvention.structured): self.parent: str | None # None indicates no parent self.var: str self.indices: list[int] = [] # empty list indicates no indices self.der: int = 0 # 0 indicates 'no derivative' - if convention == VariableNamingConvention.flat: # expect python-conformant name (with indexing) + if ( + convention is None or convention == VariableNamingConvention.flat + ): # expect (indexed) python-conformant names var, indices = ParsedVariable.disect_indices(varname) self.parent = None self.var = var self.indices = indices self.der = 0 - else: # structured variable naming (only these two are defined) + elif convention == VariableNamingConvention.structured: # structured variable naming self.der = 0 # default and count start var = varname while True: @@ -62,7 +64,8 @@ def __init__(self, varname: str, convention: VariableNamingConvention = Variable self.parent = None self.var, self.indices = ParsedVariable.disect_indices(var) - # assert self.var.isidentifier(), f"The variable name {self.var} is not a valid identifier" + else: + raise ValueError("VariableNamingConvention Enum expected. Got {convention}") def as_tuple(self): """Return all fields as tuple.""" diff --git a/tests/test_analysis.py b/tests/test_analysis.py index 8bd08ea..aba25e2 100644 --- a/tests/test_analysis.py +++ b/tests/test_analysis.py @@ -1,4 +1,3 @@ -# pyright: ignore[reportAttributeAccessIssue] # PythonFMU generates variable value objects using setattr() import logging import numpy as np diff --git a/tests/test_axle_fmu.py b/tests/test_axle_fmu.py index 6ac665a..a6d3fae 100644 --- a/tests/test_axle_fmu.py +++ b/tests/test_axle_fmu.py @@ -6,9 +6,9 @@ import numpy as np import pytest from examples.axle import Axle -from fmpy.simulation import simulate_fmu # type: ignore[import-untyped] -from fmpy.util import fmu_info # type: ignore[import-untyped] -from fmpy.validation import validate_fmu # type: ignore[import-untyped] +from fmpy.simulation import simulate_fmu +from fmpy.util import fmu_info +from fmpy.validation import validate_fmu from component_model.model import Model diff --git a/tests/test_basic.py b/tests/test_basic.py index aed4903..5ce7dc9 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -2,8 +2,8 @@ from typing import Any import pytest -from fmpy import simulate_fmu # type: ignore -from pythonfmu import ( # type: ignore +from fmpy import simulate_fmu +from pythonfmu import ( Boolean, DefaultExperiment, Fmi2Causality, @@ -131,7 +131,7 @@ def test_make_fmu(build_fmu: Path): def test_use_fmu(build_fmu: Path): - _ = simulate_fmu( # type: ignore #fmpy does not comply to pyright expectations + _ = simulate_fmu( build_fmu, stop_time=1, step_size=0.1, diff --git a/tests/test_bouncing_ball_3d_fmu.py b/tests/test_bouncing_ball_3d_fmu.py index 50ddc5c..092e0b3 100644 --- a/tests/test_bouncing_ball_3d_fmu.py +++ b/tests/test_bouncing_ball_3d_fmu.py @@ -9,10 +9,10 @@ import matplotlib.pyplot as plt import numpy as np import pytest -from fmpy import plot_result, simulate_fmu # type: ignore[import-untyped] -from fmpy.util import fmu_info # type: ignore[import-untyped] -from fmpy.validation import validate_fmu # type: ignore[import-untyped] -from pythonfmu.default_experiment import DefaultExperiment # type: ignore[import-untyped] +from fmpy import plot_result, simulate_fmu +from fmpy.util import fmu_info +from fmpy.validation import validate_fmu +from pythonfmu.default_experiment import DefaultExperiment from component_model.model import Model from component_model.utils.fmu import model_from_fmu @@ -101,9 +101,9 @@ def get_result(): if len(bb._pos) > 1 and bb._pos.unit[2].du is not None: bb._pos.setter((0, 0, 10)) t_b, p_b = bb.next_bounce() - assert t_bounce == t_b + assert t_bounce == t_b, f"Bounce time {t_bounce} != {t_b}" # print("Bounce", t_bounce, x_bounce, p_b) - assert np.allclose((x_bounce, 0, 0), p_b), f"x_bounce:{x_bounce} != {p_b[0]}" # type: ignore ##?? + assert np.allclose((x_bounce, 0, 0), p_b), f"x_bounce:{x_bounce} != {p_b[0]}" get_result() # after one step bb.do_step(time, dt) @@ -224,7 +224,7 @@ def check_result(res: np.ndarray, expected: tuple[float, ...], eps: float = 1e-1 assert bouncing_ball_fmu.exists(), f"File {bouncing_ball_fmu} does not exist" dt = 0.01 - result = simulate_fmu( # type: ignore[reportArgumentType] + result = simulate_fmu( bouncing_ball_fmu, start_time=0.0, stop_time=3.0, @@ -363,12 +363,12 @@ def test_from_fmu(bouncing_ball_fmu: Path): if __name__ == "__main__": - retcode = pytest.main(["-rA", "-v", "--rootdir", "../", "--show", "False", __file__]) + retcode = 0 # pytest.main(["-rA", "-v", "--rootdir", "../", "--show", "False", __file__]) assert retcode == 0, f"Non-zero return code {retcode}" import os os.chdir(Path(__file__).parent / "test_working_directory") - # test_bouncing_ball_class(show=False) + test_bouncing_ball_class(show=False) # test_make_bouncing_ball(_bouncing_ball_fmu()) # test_use_fmu(_bouncing_ball_fmu(), True) # test_from_fmu( _bouncing_ball_fmu()) diff --git a/tests/test_controls.py b/tests/test_controls.py index c972ab8..8ff6341 100644 --- a/tests/test_controls.py +++ b/tests/test_controls.py @@ -36,7 +36,7 @@ def test_limits(): # try to set goal outside limits _b.limit_err = logging.CRITICAL - with pytest.raises(ValueError) as err: # type: ignore[assignment] #it is a 'ValueError' + with pytest.raises(ValueError) as err: # type: ignore[assignment] ## mypy believes that it is an AssertionError?? _b.setgoal(1, 2, 9.9, 0.0) assert err.value.args[0] == "Goal 'polar'@ 9.9 is above the limit 0.0. Stopping execution." _b.limit_err = logging.WARNING diff --git a/tests/test_enums.py b/tests/test_enums.py index a0842ff..258dddb 100644 --- a/tests/test_enums.py +++ b/tests/test_enums.py @@ -1,10 +1,10 @@ import logging -from enum import Enum +from enum import Enum, EnumType import pytest -from pythonfmu.enums import Fmi2Causality as Causality # type: ignore -from pythonfmu.enums import Fmi2Initial as Initial # type: ignore -from pythonfmu.enums import Fmi2Variability as Variability # type: ignore +from pythonfmu.enums import Fmi2Causality as Causality +from pythonfmu.enums import Fmi2Initial as Initial +from pythonfmu.enums import Fmi2Variability as Variability from component_model.enums import check_causality_variability_initial, combination, combinations, ensure_enum from component_model.variable_naming import VariableNamingConvention @@ -14,10 +14,25 @@ def test_enum(): + def enum_func(e: Enum) -> None: + assert isinstance(e, Enum), f"Argument {e} should be an enum member" + logger.info(f"Name:{e.name}, value:{e.value}") + + def enumtype_func(e: EnumType): + assert isinstance(e, EnumType), f"Argument {e} should be an EnumType, i.e. the Enum Class itself" + logger.info(f"Members:{e._member_names_}") + if "flat" in e.__members__: + m: Enum = e["flat"] # type: ignore[reportAssignmentType] + logger.info(f"Member flat: {m}") + f = VariableNamingConvention.flat assert isinstance(f, Enum) assert type(f) is VariableNamingConvention assert type(f)["structured"] == VariableNamingConvention.structured + logger.info(f"Type of Enum class itself:{type(VariableNamingConvention)}") + logger.info(f"Type of member:{type(VariableNamingConvention.flat)}") + enum_func(VariableNamingConvention.flat) + enumtype_func(VariableNamingConvention) def test_combinations(): @@ -33,7 +48,7 @@ def test_ensure_enum(): assert str(err.value).startswith("The value input is not compatible with ") assert ensure_enum("discrete", Variability.continuous) == Variability.discrete assert ensure_enum("input", Causality.local) == Causality.input - assert ensure_enum(None, Causality.input) == Causality.input + assert ensure_enum(None, Causality.input) == Causality.input, f"Found {ensure_enum(None, Causality.input)}" def test_check(): diff --git a/tests/test_model.py b/tests/test_model.py index 1557485..5f6e1f8 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -6,7 +6,7 @@ import pytest -from component_model.model import Model # type: ignore +from component_model.model import Model from component_model.utils.fmu import model_from_fmu from component_model.variable import Check, Variable from component_model.variable_naming import ParsedVariable, VariableNamingConvention @@ -148,9 +148,9 @@ def test_from_fmu(bouncing_ball_fmu: Path): if __name__ == "__main__": - retcode = pytest.main(["-rA", "-v", __file__]) + retcode = 0 # pytest.main(["-rA", "-v", __file__]) assert retcode == 0, f"Non-zero return code {retcode}" # test_license() - # test_xml() + test_xml() # test_from_fmu(_bouncing_ball_fmu()) # test_variable_naming() diff --git a/tests/test_oscillator_6dof_fmu.py b/tests/test_oscillator_6dof_fmu.py index 073e749..4b501b6 100644 --- a/tests/test_oscillator_6dof_fmu.py +++ b/tests/test_oscillator_6dof_fmu.py @@ -4,9 +4,9 @@ from typing import Any import matplotlib.pyplot as plt import pytest -from fmpy.simulation import simulate_fmu # type: ignore[import-untyped] -from fmpy.util import fmu_info, plot_result # type: ignore[import-untyped] -from fmpy.validation import validate_fmu # type: ignore[import-untyped] +from fmpy.simulation import simulate_fmu +from fmpy.util import fmu_info, plot_result +from fmpy.validation import validate_fmu from component_model.model import Model diff --git a/tests/test_oscillator_fmu.py b/tests/test_oscillator_fmu.py index ab514cd..cbfd32f 100644 --- a/tests/test_oscillator_fmu.py +++ b/tests/test_oscillator_fmu.py @@ -3,9 +3,9 @@ import matplotlib.pyplot as plt import pytest -from fmpy.simulation import simulate_fmu # type: ignore[import-untyped] -from fmpy.util import fmu_info, plot_result # type: ignore[import-untyped] -from fmpy.validation import validate_fmu # type: ignore[import-untyped] +from fmpy.simulation import simulate_fmu +from fmpy.util import fmu_info, plot_result +from fmpy.validation import validate_fmu from component_model.model import Model diff --git a/tests/test_pint.py b/tests/test_pint.py index ef20206..a4d6d88 100644 --- a/tests/test_pint.py +++ b/tests/test_pint.py @@ -1,6 +1,7 @@ """Test the pint package and identify the functions we need for this package""" import logging +from typing import Any import pytest from pint import UnitRegistry @@ -9,7 +10,7 @@ def test_needed_functions(): - _reg: UnitRegistry = UnitRegistry( + _reg: UnitRegistry[Any] = UnitRegistry( system="SI", autoconvert_offset_to_baseunit=True ) # , auto_reduce_dimensions=True) print("AVAILABLE UNITS", dir(_reg.sys.SI)) diff --git a/tests/test_range.py b/tests/test_range.py index 2c68909..7619918 100644 --- a/tests/test_range.py +++ b/tests/test_range.py @@ -1,3 +1,7 @@ +import logging +from enum import Enum +from typing import Any, Never, Sequence + import numpy as np import pytest @@ -5,6 +9,8 @@ from component_model.range import Range from component_model.unit import Unit +logger = logging.getLogger(__name__) + @pytest.fixture def unt(scope: str = "module", autouse: bool = True): @@ -31,7 +37,7 @@ def test_init(unt: Unit): float3 = Range(1.0, ("0m", "10m"), unit=Unit("1 m")) assert float3.rng == (0.0, 10.0), f"Found {float3.rng}" float4 = Range(0.55, ("0 %", None), unit=Unit("55%")) - assert np.allclose(float4.rng, (0.0, 55.0)), f"Found {float4.rng} != {(0.0, 55.0)}" + assert np.allclose(float4.rng, (0.0, 0.55)), f"Found {float4.rng} != {(0.0, 0.55)} (base units)" def test_auto_extreme(): @@ -45,9 +51,48 @@ def test_auto_extreme(): assert str(err.value) == "Auto-extremes for type cannot be determined" +def test_range_spec(): + def do_check( + example: Any, + rng: tuple[Any, ...] | tuple[Any, Any] | None | Sequence[Never], + var_len: int, + typ: type, + level: int = 0, + expect: int = 0, + msg: str = "", + ): + ck = Range.is_valid_spec(rng, var_len, typ, level) + if ck == 0: + if var_len == 1: + try: + _rng = Range(example, rng) + except ValueError as err: + logger.error(f"{rng} set to valid, but {err}") + else: + assert isinstance(rng, tuple) and len(rng) == len(example) + for e, r in zip(example, rng, strict=True): + try: + _rng = Range(e, r) + except ValueError as err: + logger.error(f"{r} set to valid, but {err}") + assert ck == expect, f"{msg}: Range:{rng}: {ck} != {expect}" + + Unit.ensure_unit_registry() + do_check(9.9, tuple(), 1, float, expect=0, msg="Valid single variable spec with automatic floats.") + do_check(9, tuple(), 1, int, expect=1, msg="InValid single variable spec with automatic int.") + do_check(9, None, 1, int, expect=0, msg="Valid fixed single variable spec (any type).") + do_check(Check.all, None, 1, Enum, expect=0, msg="Valid fixed single variable spec (any type).") + do_check(Check.all, tuple(), 1, Enum, expect=0, msg="Valid automatic enum single variable range.") + do_check(1, (1, 2), 1, int, expect=0, msg="Valid range of single int variable.") + do_check(5.0, (9.9, 2.0), 1, float, expect=19, msg="InValid range of single float variable: wrong order.") + do_check(1.0, ("1m", 2.0), 1, float, expect=0, msg="Valid range of single float variable (partial units).") + do_check((1.0, 2.0, 3.0), (None, tuple(), (1.0, 2.0)), 3, float, expect=0, msg="Valid mixed vector spec.") + + if __name__ == "__main__": retcode = pytest.main(["-rP -s -v", __file__]) assert retcode == 0, f"Return code {retcode}" # _unt() # initialize UnitRegistry (otherwise Unit cannot be used) # test_init(_unt()) # test_auto_extreme() + # test_range_spec() diff --git a/tests/test_solver.py b/tests/test_solver.py index 2a597ae..3cf2747 100644 --- a/tests/test_solver.py +++ b/tests/test_solver.py @@ -102,8 +102,8 @@ def hit_ground(t: np.ndarray, y: np.ndarray): (0, 100), np.array((0, 200), float), t_eval=np.array([t for t in range(100)]), - events=hit_ground # type: ignore - ) + events=hit_ground, # type: ignore + ) assert np.allclose(sol.t_events, [2 * 200 / 9.81]), "Time when hitting the ground" # type: ignore ## it works assert np.allclose(sol.y_events, [[0.0, -200.0]]), "Position and speed when hitting the ground" # type: ignore if show: diff --git a/tests/test_time_table.py b/tests/test_time_table.py index a12e40a..6fcb3b5 100644 --- a/tests/test_time_table.py +++ b/tests/test_time_table.py @@ -31,8 +31,8 @@ def test_time_table(show: bool = False): from examples.time_table import TimeTable tbl = TimeTable( - data=((0.0, 1, 0, 0), (1.0, 1, 1, 1), (3.0, 1, 3, 9), (7.0, 1, 7, 49)), - header=("x", "y", "z"), + data=[[0.0, 1, 0, 0], [1.0, 1, 1, 1], [3.0, 1, 3, 9], [7.0, 1, 7, 49]], + header=["x", "y", "z"], interpolate=0, ) assert not tbl.interpolate, f"Interpolation=0 expected. Found {tbl.interpolate}" diff --git a/tests/test_time_table_fmu.py b/tests/test_time_table_fmu.py index 5aab099..81822b9 100644 --- a/tests/test_time_table_fmu.py +++ b/tests/test_time_table_fmu.py @@ -6,9 +6,9 @@ import numpy as np import pytest -from fmpy.simulation import simulate_fmu # type: ignore -from fmpy.util import fmu_info, plot_result # type: ignore -from fmpy.validation import validate_fmu # type: ignore +from fmpy.simulation import simulate_fmu +from fmpy.util import fmu_info, plot_result +from fmpy.validation import validate_fmu from pythonfmu.enums import Fmi2Causality as Causality from pythonfmu.enums import Fmi2Variability as Variability @@ -112,7 +112,7 @@ def test_use_fmu(time_table_fmu: Path, show: bool = False): print(fmu_info(str(time_table_fmu))) _t = np.linspace(0, 10, 101) for ipol in range(4): - result = simulate_fmu( # type: ignore[reportArgumentType] + result = simulate_fmu( time_table_fmu, stop_time=10.0, step_size=0.1, @@ -158,7 +158,7 @@ def test_make_with_new_data(): # @pytest.mark.skip(reason="Does so far not work within pytest, only stand-alone") def test_use_with_new_data(show: bool): fmu_path = Path(__file__).parent / "test_working_directory" / "TimeTableFMU.fmu" - result = simulate_fmu( # type: ignore[reportArgumentType] + result = simulate_fmu( fmu_path, stop_time=2 * np.pi, step_size=0.1, diff --git a/tests/test_unit.py b/tests/test_unit.py index 826ce40..b6ed2ab 100644 --- a/tests/test_unit.py +++ b/tests/test_unit.py @@ -1,5 +1,7 @@ from math import degrees, radians +from typing import Any +import numpy as np import pytest from pint import UnitRegistry @@ -17,10 +19,10 @@ def _ureg(): return _registry -def test_parsing(ureg: UnitRegistry): +def test_parsing(ureg: UnitRegistry[Any]): u1 = Unit() # default values: - assert u1.u == "dimensionless" + assert u1.u == "" assert u1.du is None val = u1.parse_quantity("9.9m") assert val == 9.9 @@ -41,17 +43,17 @@ def test_parsing(ureg: UnitRegistry): assert uf.u == "kelvin" assert uf.du == "degree_Fahrenheit" assert uf.parse_quantity("0.0 degF") == 255.37222222222223 - assert uf.to_base(0.0) == 255.37222222222223 + assert uf.to_base is not None and uf.to_base(0.0) == 255.37222222222223 -def test_make(ureg: UnitRegistry): +def test_make(ureg: UnitRegistry[Any]): val, unit = Unit.make("2m") assert val[0] == 2 assert unit[0].u == "meter", f"Found {unit[0].u}" assert unit[0].du is None - val, unit = Unit.make("Hello World", typ=str) + val, unit = Unit.make("Hello World", no_unit=True) assert val[0] == "Hello World" - assert unit[0].u == "dimensionless" + assert unit[0].u == "" assert unit[0].du is None val, unit = Unit.make("99.0%") assert val[0] == 0.99 @@ -59,7 +61,7 @@ def test_make(ureg: UnitRegistry): assert unit[0].du == "percent" -def test_make_tuple(ureg: UnitRegistry): +def test_make_tuple(ureg: UnitRegistry[Any]): vals, units = Unit.make_tuple(("2m", "3deg", "0.0 degF")) k2degc = 273.15 assert units[0].u == "meter" @@ -67,30 +69,35 @@ def test_make_tuple(ureg: UnitRegistry): assert vals[0] == 2 assert units[1].u == "radian", f"Found {units[1].u}" assert units[1].du == "degree" + assert units[1].to_base is not None assert units[1].to_base(1.0) == radians(1.0) + assert units[1].from_base is not None assert units[1].from_base(1.0) == degrees(1.0) assert vals[1] == radians(3) assert units[2].u == "kelvin", f"Found {units[2].u}" assert units[2].du == "degree_Fahrenheit", f"Found {units[2].du}" + assert units[2].from_base is not None assert abs(units[2].from_base(k2degc) - (k2degc * 9 / 5 - 459.67)) < 1e-10 + assert units[2].to_base is not None assert abs(units[2].to_base(0.0) - (0.0 + 459.67) * 5 / 9) < 1e-10, ( f"Found {units[2].to_base(0.0)}, {(0.0 + 459.67) * 5 / 9}" ) -def test_derivative(ureg: UnitRegistry): +def test_derivative(ureg: UnitRegistry[Any]): bv, bu = Unit.make_tuple(("2m", "3deg")) vals, units = Unit.derivative(bu) assert vals == (0.0, 0.0) assert units[0].u == "meter/s" assert units[0].du is None assert units[1].u == "radian/s", f"Found {units[1].u}" - assert units[1].du == "degree/s" - assert units[1].to_base == bu[1].to_base + assert units[1].du == "degree/s", f"{units[1].du} != 'degree/s'" + assert units[1].to_base == bu[1].to_base, f"{units[1].to_base} != {bu[1].to_base}" assert units[1].from_base == bu[1].from_base + assert units[1].to_base(360) == 2 * np.pi -def test_compatible(ureg: UnitRegistry): +def test_compatible(ureg: UnitRegistry[Any]): v, u = Unit.make_tuple(("2m", "3deg")) ck, q = u[0].compatible("4m", strict=True) assert ck diff --git a/tests/test_variable.py b/tests/test_variable.py index f464c53..7cbdab5 100644 --- a/tests/test_variable.py +++ b/tests/test_variable.py @@ -1,4 +1,3 @@ -# pyright: ignore[reportAttributeAccessIssue] # PythonFMU generates variable value objects using setattr() import logging import math import xml.etree.ElementTree as ET # noqa: N817 @@ -7,12 +6,14 @@ import numpy as np import pytest -from pythonfmu.enums import Fmi2Causality as Causality # type: ignore -from pythonfmu.enums import Fmi2Initial as Initial # type: ignore -from pythonfmu.enums import Fmi2Variability as Variability # type: ignore +from pint import UnitRegistry +from pythonfmu.enums import Fmi2Causality as Causality +from pythonfmu.enums import Fmi2Initial as Initial +from pythonfmu.enums import Fmi2Variability as Variability from scipy.spatial.transform import Rotation as Rot from component_model.model import Model +from component_model.unit import Unit from component_model.utils.analysis import extremum, extremum_series from component_model.utils.transform import ( cartesian_to_spherical, @@ -29,6 +30,17 @@ logging.basicConfig(level=logging.INFO) +@pytest.fixture +def ureg(scope: str = "module", autouse: bool = True): + return _ureg() + + +def _ureg(): + _registry = Unit.ensure_unit_registry("SI") + assert isinstance(_registry, UnitRegistry) + return _registry + + class DummyModel(Model): def __init__(self, name: str, **kwargs: Any): super().__init__(name=name, description="Just a dummy model to be able to do testing", **kwargs) @@ -94,7 +106,7 @@ def test_range(): mod = DummyModel("MyModel2", instance_name="MyModel2") with pytest.raises(ValueError) as err: _int1 = Variable(mod, "int1", typ=int, start=1) - assert err.value.args[0] == "Auto-extremes for type cannot be determined" + assert err.value.args[0] == "int1 invalid range spec '()'. Error: Automatic range for int variables is not defined." int2 = Variable(mod, "int2", start=1, rng=(0, 5)) assert int2.range[0].rng == (0, 5), "That works" mod.int2 = 6 @@ -111,21 +123,29 @@ def test_range(): assert r.rng == exp, f"{r.rng} != {exp}" -def test_auto_type(): - assert Variable.auto_type(1) is float, "int not allowed (default)" - assert Variable.auto_type(1, allow_int=True) is int, "int allowed" - assert Variable.auto_type(0.99, allow_int=True) is float - assert Variable.auto_type(0.99, allow_int=False) is float - assert Variable.auto_type((1, 2, 0.99), allow_int=False) is float - assert Variable.auto_type((1, 2, 0.99), allow_int=True) is float, "Ok by our rules" - assert Variable.auto_type((1, 2, 3), allow_int=True) is int - assert Variable.auto_type((True, False, 3), allow_int=True) is int - assert Variable.auto_type((True, False), allow_int=True) is bool - assert Variable.auto_type((True, False), allow_int=False) is bool - assert Variable.auto_type((True, 1, 9.9), allow_int=False) is bool - # with pytest.raises(VariableInitError) as err: # that goes too far - # assert Variable.auto_type( (True,1, 9.9), allow_int=False) == float - # assert str(err.value).startswith("Incompatible variable types") +def test_auto_type(ureg: UnitRegistry[Any]): + no_unit = (Unit(),) + no_units = (Unit(), Unit(), Unit()) + lengths = (Unit("1 m"), Unit("1 inch"), Unit("100 m")) + assert Variable.auto_type((1,), no_unit) is int + assert Variable.auto_type((1, 2, 3), lengths) is float, "int with units become float" + assert Variable.auto_type((0.99,), no_unit) is float + assert Variable.auto_type((1, 2, 0.99), no_units) is float, "float overrides int" + assert Variable.auto_type((1, 2, 3), no_units) is int + with pytest.raises(ValueError) as err: + assert Variable.auto_type((True, False, 3), no_units) is int + assert str(err.value).startswith("Auto-type cannot be determined for values") + assert Variable.auto_type((True, False, True), no_units) is bool + with pytest.raises(ValueError) as err: + assert Variable.auto_type((True, False, True), lengths) is bool + assert str(err.value) == "bool value True with unit 'meter' is not allowed." + with pytest.raises(ValueError) as err: + assert Variable.auto_type((True, 1, 9.9), no_units) is bool + assert str(err.value).startswith("Auto-type cannot be determined for values") + assert Variable.auto_type((Causality.input, Causality.output, Causality.parameter), no_units) is Causality + with pytest.raises(ValueError) as err: + assert Variable.auto_type((Causality.input, Variability.continuous, Causality.parameter), no_units) is Enum + assert str(err.value).startswith("Auto-type cannot be determined for values") def test_spherical_cartesian(): @@ -364,7 +384,7 @@ def test_init(): # internally packed into tuple: assert int1.start == (99,) assert int1.range[0].rng == (0, 100), f"Found {int1.range[0].rng}" - assert int1.unit[0].u == "dimensionless" + assert int1.unit[0].u == "" assert int1.unit[0].du is None assert int1.check_range([50]) assert not int1.check_range([110]) @@ -378,19 +398,20 @@ def test_init(): mod.set_integer([mod.variable_by_name("int1").value_reference], [99]) # simulate setting from outside assert mod.get_integer([mod.variable_by_name("int1").value_reference]) == [99] - assert float1.typ is float + assert float1.typ is float, f"Found {float1.typ}" assert float1.causality == Causality.input assert float1.variability == Variability.continuous assert float1.initial is None, f"initial: {float1.initial}" assert float1.check == Check.all # internally packed into tuple: assert float1.start == (0.99,) - assert float1.range[0].rng == (0.0, 99.0), f"Range: {float1.range[0].rng} in display units." assert float1.unit[0].u == "dimensionless" assert float1.unit[0].du == "percent", f"Display: {float1.unit[0].du}" - assert float1.unit[0].to_base(99) == 0.99, "Transform to dimensionless" - assert float1.unit[0].from_base(0.99) == 99, "... and back." - assert float1.check_range([0.5]) + assert float1.range[0].rng == (0.0, 0.99), f"Stored range: {float1.range[0].rng}!=(0.0,0.99) in base units." + assert float1.unit[0].to_base is not None and float1.unit[0].from_base is not None, "Transformations needed" + assert float1.unit[0].to_base(99) == 0.99, f"Transform % to dimensionless: {float1.unit[0].to_base(99)}" + assert float1.unit[0].from_base(0.99) == 99.0, f"... and back:: {float1.unit[0].from_base(0.99)}" + assert float1.check_range([50]) assert not float1.check_range([1.0], disp=False), "Check as internal units" assert not float1.check_range([100.0]), "Check as display units" assert mod.float1 == 0.99, "Value directly accessible as model variable" @@ -412,7 +433,7 @@ def test_init(): # internally packed into tuple: assert enum1.start == (Causality.parameter,) assert enum1.range[0].rng == (0, 4), f"Range: {enum1.range[0].rng}" - assert enum1.unit[0].u == "dimensionless" + assert enum1.unit[0].u == "" assert enum1.unit[0].du is None, f"Display: {enum1.unit[0].du}" assert enum1.check_range([1]) assert not enum1.check_range([7]) @@ -423,7 +444,6 @@ def test_init(): assert enum1.getter() == [4], f"Value {enum1.getter()}. Translated to int!" mod.set_integer([mod.variable_by_name("enum1").value_reference], [2]) # simulate setting from outside assert mod.get_integer([mod.variable_by_name("enum1").value_reference]) == [2] - assert bool1.typ is bool assert bool1.causality == Causality.parameter assert bool1.variability == Variability.fixed @@ -432,7 +452,7 @@ def test_init(): # internally packed into tuple: assert bool1.start == (True,) assert bool1.range[0].rng == (False, True) - assert bool1.unit[0].u == "dimensionless" + assert bool1.unit[0].u == "" assert bool1.unit[0].du is None assert bool1.check_range([True]) assert bool1.check_range([100.5]), "Any number will work" @@ -454,7 +474,7 @@ def test_init(): # internally packed into tuple: assert str1.start == ("Hello World!",) assert str1.range[0].rng == ("Hello World!", "Hello World!"), f"Range: {str1.range[0].rng}. Basically irrelevant" - assert str1.unit[0].u == "dimensionless", f"Unit {str1.unit}" + assert str1.unit[0].u == "", f"Unit {str1.unit}" assert str1.unit[0].du is None, f"Display: {str1.unit[0].du}" assert str1.check_range([0.5]), "Everything is ok" assert mod.str1 == "Hello World!", f"Value {mod.str1} directly accessible as model variable" @@ -466,7 +486,7 @@ def test_init(): mod.set_string([mod.variable_by_name("str1").value_reference], ["Hello"]) # simulate setting from outside assert mod.get_string([mod.variable_by_name("str1").value_reference]) == ["Hello"] - assert np1.typ is float + assert np1.typ is float # np1: start=("1.0m","2deg","3rad"), rng=((0,"3m"),("1deg","5deg"),(float("-inf"), "5rad")) assert np1 == mod.variable_by_name("np1") assert np1.description == "A NP variable" assert mod.variable_by_name("np1[1]") == mod.variable_by_name("np1"), "Returns always the parent" @@ -476,7 +496,9 @@ def test_init(): assert np1.check == Check.all # internally packed into tuple: assert np1.start == (1, math.radians(2), 3) - for r, expect in zip(np1.range, ((0.0, 3.0), (1.0, 5.0), (float("-inf"), 5.0)), strict=True): + for r, expect in zip( + np1.range, ((0.0, 3.0), (np.radians(1.0), np.radians(5.0)), (float("-inf"), 5.0)), strict=True + ): assert np.allclose(r.rng, expect), f"{r.rng} != {expect}" assert not np1.check_range([5.1], idx=1), "Checks performed on display units!" assert not np1.check_range([0.9], idx=1), "Checks performed on display units!" @@ -533,8 +555,10 @@ def test_init(): annotations=None, typ=int, ) - assert err3.value.args[0] == "Auto-extremes for type cannot be determined" - assert float1.range[0].rng[1] == 99.0 + assert err3.value.args[0].startswith( + "bool1 invalid range spec '()'. Error: Automatic range for int variables is not defined." + ) + assert float1.range[0].rng[1] == 0.99 assert enum1.range[0].rng == (0, 4) assert enum1.check_range([Causality.parameter]) assert str1.range[0].rng == ("Hello World!", "Hello World!"), "Just a placeholder. Range of str is not checked" @@ -670,7 +694,7 @@ def test_set(): assert mod.int1 == 60 assert mod.vars[1].getter() == [61], f"Found {mod.vars[99].getter()}" with pytest.raises(AssertionError) as err: - mod.set_integer([6, 7], [2.0, "30 deg"]) # type: ignore # we want to produce an error! + mod.set_integer([6, 7], [2.0, "30 deg"]) assert str(err.value) == "Invalid type in 'set_'. Found variable np1 with type " mod.set_real([6, 7], [2.0, 3.0]) # "3 deg"]) @@ -691,11 +715,17 @@ def test_xml(): lst = np2.xml_scalarvariables() assert len(lst) == 3 expected = '' - assert ET.tostring(lst[0], encoding="unicode") == expected, ET.tostring(lst[0], encoding="unicode") - expected = '' - assert ET.tostring(lst[1], encoding="unicode") == expected, ET.tostring(lst[1], encoding="unicode") - expected = '' - assert ET.tostring(lst[2], encoding="unicode") == expected, ET.tostring(lst[2], encoding="unicode") + assert ET.tostring(lst[0], encoding="unicode") == expected, ( + f"Found:\n{ET.tostring(lst[0], encoding='unicode')} != \n{expected}" + ) + expected = '' + assert ET.tostring(lst[1], encoding="unicode") == expected, ( + f"Found:\n{ET.tostring(lst[1], encoding='unicode')} != \n{expected}" + ) + expected = '' + assert ET.tostring(lst[2], encoding="unicode") == expected, ( + f"Found:\n{ET.tostring(lst[2], encoding='unicode')} != \n{expected}" + ) int1 = Variable( mod, @@ -709,7 +739,7 @@ def test_xml(): value_check=Check.all, ) lst = int1.xml_scalarvariables() - expected = '' + expected = '' found = ET.tostring(lst[0], encoding="unicode") assert found == expected, f"\nFound :{found}\nExpected:{expected}" @@ -760,13 +790,14 @@ def test_extremum(): if __name__ == "__main__": - retcode = pytest.main(["-rP -s -v", __file__]) + retcode = 0 # pytest.main(["-rP -s -v", __file__]) assert retcode == 0, f"Return code {retcode}" - # test_init() + # ureg = _ureg() + test_init() # test_range() # test_var_check() # test_spherical_cartesian() - # test_auto_type() + # test_auto_type(ureg) # test_dirty() # test_var_ref() # test_vars_iter() diff --git a/uv.lock b/uv.lock index bc0537d..506adb0 100644 --- a/uv.lock +++ b/uv.lock @@ -2,12 +2,8 @@ version = 1 revision = 3 requires-python = ">=3.11" resolution-markers = [ - "python_full_version >= '3.12' and sys_platform != 'linux' and sys_platform != 'win32'", - "python_full_version < '3.12' and sys_platform != 'linux' and sys_platform != 'win32'", - "python_full_version >= '3.12' and sys_platform == 'linux'", - "python_full_version < '3.12' and sys_platform == 'linux'", - "python_full_version >= '3.12' and sys_platform == 'win32'", - "python_full_version < '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12'", + "python_full_version < '3.12'", ] required-markers = [ "sys_platform == 'win32'", @@ -201,8 +197,7 @@ dev = [ { name = "pytest" }, { name = "pytest-cov" }, { name = "ruff" }, - { name = "sourcery", version = "1.40.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, - { name = "sourcery", version = "1.41.1", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux' and sys_platform != 'win32'" }, + { name = "sourcery" }, { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "sphinx-argparse-cli" }, @@ -329,89 +324,89 @@ wheels = [ [[package]] name = "coverage" -version = "7.13.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/23/f9/e92df5e07f3fc8d4c7f9a0f146ef75446bf870351cd37b788cf5897f8079/coverage-7.13.1.tar.gz", hash = "sha256:b7593fe7eb5feaa3fbb461ac79aac9f9fc0387a5ca8080b0c6fe2ca27b091afd", size = 825862, upload-time = "2025-12-28T15:42:56.969Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/9b/77baf488516e9ced25fc215a6f75d803493fc3f6a1a1227ac35697910c2a/coverage-7.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a55d509a1dc5a5b708b5dad3b5334e07a16ad4c2185e27b40e4dba796ab7f88", size = 218755, upload-time = "2025-12-28T15:40:30.812Z" }, - { url = "https://files.pythonhosted.org/packages/d7/cd/7ab01154e6eb79ee2fab76bf4d89e94c6648116557307ee4ebbb85e5c1bf/coverage-7.13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d010d080c4888371033baab27e47c9df7d6fb28d0b7b7adf85a4a49be9298b3", size = 219257, upload-time = "2025-12-28T15:40:32.333Z" }, - { url = "https://files.pythonhosted.org/packages/01/d5/b11ef7863ffbbdb509da0023fad1e9eda1c0eaea61a6d2ea5b17d4ac706e/coverage-7.13.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d938b4a840fb1523b9dfbbb454f652967f18e197569c32266d4d13f37244c3d9", size = 249657, upload-time = "2025-12-28T15:40:34.1Z" }, - { url = "https://files.pythonhosted.org/packages/f7/7c/347280982982383621d29b8c544cf497ae07ac41e44b1ca4903024131f55/coverage-7.13.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bf100a3288f9bb7f919b87eb84f87101e197535b9bd0e2c2b5b3179633324fee", size = 251581, upload-time = "2025-12-28T15:40:36.131Z" }, - { url = "https://files.pythonhosted.org/packages/82/f6/ebcfed11036ade4c0d75fa4453a6282bdd225bc073862766eec184a4c643/coverage-7.13.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef6688db9bf91ba111ae734ba6ef1a063304a881749726e0d3575f5c10a9facf", size = 253691, upload-time = "2025-12-28T15:40:37.626Z" }, - { url = "https://files.pythonhosted.org/packages/02/92/af8f5582787f5d1a8b130b2dcba785fa5e9a7a8e121a0bb2220a6fdbdb8a/coverage-7.13.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b609fc9cdbd1f02e51f67f51e5aee60a841ef58a68d00d5ee2c0faf357481a3", size = 249799, upload-time = "2025-12-28T15:40:39.47Z" }, - { url = "https://files.pythonhosted.org/packages/24/aa/0e39a2a3b16eebf7f193863323edbff38b6daba711abaaf807d4290cf61a/coverage-7.13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c43257717611ff5e9a1d79dce8e47566235ebda63328718d9b65dd640bc832ef", size = 251389, upload-time = "2025-12-28T15:40:40.954Z" }, - { url = "https://files.pythonhosted.org/packages/73/46/7f0c13111154dc5b978900c0ccee2e2ca239b910890e674a77f1363d483e/coverage-7.13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e09fbecc007f7b6afdfb3b07ce5bd9f8494b6856dd4f577d26c66c391b829851", size = 249450, upload-time = "2025-12-28T15:40:42.489Z" }, - { url = "https://files.pythonhosted.org/packages/ac/ca/e80da6769e8b669ec3695598c58eef7ad98b0e26e66333996aee6316db23/coverage-7.13.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a03a4f3a19a189919c7055098790285cc5c5b0b3976f8d227aea39dbf9f8bfdb", size = 249170, upload-time = "2025-12-28T15:40:44.279Z" }, - { url = "https://files.pythonhosted.org/packages/af/18/9e29baabdec1a8644157f572541079b4658199cfd372a578f84228e860de/coverage-7.13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3820778ea1387c2b6a818caec01c63adc5b3750211af6447e8dcfb9b6f08dbba", size = 250081, upload-time = "2025-12-28T15:40:45.748Z" }, - { url = "https://files.pythonhosted.org/packages/00/f8/c3021625a71c3b2f516464d322e41636aea381018319050a8114105872ee/coverage-7.13.1-cp311-cp311-win32.whl", hash = "sha256:ff10896fa55167371960c5908150b434b71c876dfab97b69478f22c8b445ea19", size = 221281, upload-time = "2025-12-28T15:40:47.232Z" }, - { url = "https://files.pythonhosted.org/packages/27/56/c216625f453df6e0559ed666d246fcbaaa93f3aa99eaa5080cea1229aa3d/coverage-7.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:a998cc0aeeea4c6d5622a3754da5a493055d2d95186bad877b0a34ea6e6dbe0a", size = 222215, upload-time = "2025-12-28T15:40:49.19Z" }, - { url = "https://files.pythonhosted.org/packages/5c/9a/be342e76f6e531cae6406dc46af0d350586f24d9b67fdfa6daee02df71af/coverage-7.13.1-cp311-cp311-win_arm64.whl", hash = "sha256:fea07c1a39a22614acb762e3fbbb4011f65eedafcb2948feeef641ac78b4ee5c", size = 220886, upload-time = "2025-12-28T15:40:51.067Z" }, - { url = "https://files.pythonhosted.org/packages/ce/8a/87af46cccdfa78f53db747b09f5f9a21d5fc38d796834adac09b30a8ce74/coverage-7.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6f34591000f06e62085b1865c9bc5f7858df748834662a51edadfd2c3bfe0dd3", size = 218927, upload-time = "2025-12-28T15:40:52.814Z" }, - { url = "https://files.pythonhosted.org/packages/82/a8/6e22fdc67242a4a5a153f9438d05944553121c8f4ba70cb072af4c41362e/coverage-7.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b67e47c5595b9224599016e333f5ec25392597a89d5744658f837d204e16c63e", size = 219288, upload-time = "2025-12-28T15:40:54.262Z" }, - { url = "https://files.pythonhosted.org/packages/d0/0a/853a76e03b0f7c4375e2ca025df45c918beb367f3e20a0a8e91967f6e96c/coverage-7.13.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e7b8bd70c48ffb28461ebe092c2345536fb18bbbf19d287c8913699735f505c", size = 250786, upload-time = "2025-12-28T15:40:56.059Z" }, - { url = "https://files.pythonhosted.org/packages/ea/b4/694159c15c52b9f7ec7adf49d50e5f8ee71d3e9ef38adb4445d13dd56c20/coverage-7.13.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c223d078112e90dc0e5c4e35b98b9584164bea9fbbd221c0b21c5241f6d51b62", size = 253543, upload-time = "2025-12-28T15:40:57.585Z" }, - { url = "https://files.pythonhosted.org/packages/96/b2/7f1f0437a5c855f87e17cf5d0dc35920b6440ff2b58b1ba9788c059c26c8/coverage-7.13.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:794f7c05af0763b1bbd1b9e6eff0e52ad068be3b12cd96c87de037b01390c968", size = 254635, upload-time = "2025-12-28T15:40:59.443Z" }, - { url = "https://files.pythonhosted.org/packages/e9/d1/73c3fdb8d7d3bddd9473c9c6a2e0682f09fc3dfbcb9c3f36412a7368bcab/coverage-7.13.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0642eae483cc8c2902e4af7298bf886d605e80f26382124cddc3967c2a3df09e", size = 251202, upload-time = "2025-12-28T15:41:01.328Z" }, - { url = "https://files.pythonhosted.org/packages/66/3c/f0edf75dcc152f145d5598329e864bbbe04ab78660fe3e8e395f9fff010f/coverage-7.13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5e772ed5fef25b3de9f2008fe67b92d46831bd2bc5bdc5dd6bfd06b83b316f", size = 252566, upload-time = "2025-12-28T15:41:03.319Z" }, - { url = "https://files.pythonhosted.org/packages/17/b3/e64206d3c5f7dcbceafd14941345a754d3dbc78a823a6ed526e23b9cdaab/coverage-7.13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:45980ea19277dc0a579e432aef6a504fe098ef3a9032ead15e446eb0f1191aee", size = 250711, upload-time = "2025-12-28T15:41:06.411Z" }, - { url = "https://files.pythonhosted.org/packages/dc/ad/28a3eb970a8ef5b479ee7f0c484a19c34e277479a5b70269dc652b730733/coverage-7.13.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f18eca6028ffa62adbd185a8f1e1dd242f2e68164dba5c2b74a5204850b4cf", size = 250278, upload-time = "2025-12-28T15:41:08.285Z" }, - { url = "https://files.pythonhosted.org/packages/54/e3/c8f0f1a93133e3e1291ca76cbb63565bd4b5c5df63b141f539d747fff348/coverage-7.13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8dca5590fec7a89ed6826fce625595279e586ead52e9e958d3237821fbc750c", size = 252154, upload-time = "2025-12-28T15:41:09.969Z" }, - { url = "https://files.pythonhosted.org/packages/d0/bf/9939c5d6859c380e405b19e736321f1c7d402728792f4c752ad1adcce005/coverage-7.13.1-cp312-cp312-win32.whl", hash = "sha256:ff86d4e85188bba72cfb876df3e11fa243439882c55957184af44a35bd5880b7", size = 221487, upload-time = "2025-12-28T15:41:11.468Z" }, - { url = "https://files.pythonhosted.org/packages/fa/dc/7282856a407c621c2aad74021680a01b23010bb8ebf427cf5eacda2e876f/coverage-7.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:16cc1da46c04fb0fb128b4dc430b78fa2aba8a6c0c9f8eb391fd5103409a6ac6", size = 222299, upload-time = "2025-12-28T15:41:13.386Z" }, - { url = "https://files.pythonhosted.org/packages/10/79/176a11203412c350b3e9578620013af35bcdb79b651eb976f4a4b32044fa/coverage-7.13.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d9bc218650022a768f3775dd7fdac1886437325d8d295d923ebcfef4892ad5c", size = 220941, upload-time = "2025-12-28T15:41:14.975Z" }, - { url = "https://files.pythonhosted.org/packages/a3/a4/e98e689347a1ff1a7f67932ab535cef82eb5e78f32a9e4132e114bbb3a0a/coverage-7.13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cb237bfd0ef4d5eb6a19e29f9e528ac67ac3be932ea6b44fb6cc09b9f3ecff78", size = 218951, upload-time = "2025-12-28T15:41:16.653Z" }, - { url = "https://files.pythonhosted.org/packages/32/33/7cbfe2bdc6e2f03d6b240d23dc45fdaf3fd270aaf2d640be77b7f16989ab/coverage-7.13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1dcb645d7e34dcbcc96cd7c132b1fc55c39263ca62eb961c064eb3928997363b", size = 219325, upload-time = "2025-12-28T15:41:18.609Z" }, - { url = "https://files.pythonhosted.org/packages/59/f6/efdabdb4929487baeb7cb2a9f7dac457d9356f6ad1b255be283d58b16316/coverage-7.13.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3d42df8201e00384736f0df9be2ced39324c3907607d17d50d50116c989d84cd", size = 250309, upload-time = "2025-12-28T15:41:20.629Z" }, - { url = "https://files.pythonhosted.org/packages/12/da/91a52516e9d5aea87d32d1523f9cdcf7a35a3b298e6be05d6509ba3cfab2/coverage-7.13.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa3edde1aa8807de1d05934982416cb3ec46d1d4d91e280bcce7cca01c507992", size = 252907, upload-time = "2025-12-28T15:41:22.257Z" }, - { url = "https://files.pythonhosted.org/packages/75/38/f1ea837e3dc1231e086db1638947e00d264e7e8c41aa8ecacf6e1e0c05f4/coverage-7.13.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9edd0e01a343766add6817bc448408858ba6b489039eaaa2018474e4001651a4", size = 254148, upload-time = "2025-12-28T15:41:23.87Z" }, - { url = "https://files.pythonhosted.org/packages/7f/43/f4f16b881aaa34954ba446318dea6b9ed5405dd725dd8daac2358eda869a/coverage-7.13.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:985b7836931d033570b94c94713c6dba5f9d3ff26045f72c3e5dbc5fe3361e5a", size = 250515, upload-time = "2025-12-28T15:41:25.437Z" }, - { url = "https://files.pythonhosted.org/packages/84/34/8cba7f00078bd468ea914134e0144263194ce849ec3baad187ffb6203d1c/coverage-7.13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ffed1e4980889765c84a5d1a566159e363b71d6b6fbaf0bebc9d3c30bc016766", size = 252292, upload-time = "2025-12-28T15:41:28.459Z" }, - { url = "https://files.pythonhosted.org/packages/8c/a4/cffac66c7652d84ee4ac52d3ccb94c015687d3b513f9db04bfcac2ac800d/coverage-7.13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8842af7f175078456b8b17f1b73a0d16a65dcbdc653ecefeb00a56b3c8c298c4", size = 250242, upload-time = "2025-12-28T15:41:30.02Z" }, - { url = "https://files.pythonhosted.org/packages/f4/78/9a64d462263dde416f3c0067efade7b52b52796f489b1037a95b0dc389c9/coverage-7.13.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ccd7a6fca48ca9c131d9b0a2972a581e28b13416fc313fb98b6d24a03ce9a398", size = 250068, upload-time = "2025-12-28T15:41:32.007Z" }, - { url = "https://files.pythonhosted.org/packages/69/c8/a8994f5fece06db7c4a97c8fc1973684e178599b42e66280dded0524ef00/coverage-7.13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0403f647055de2609be776965108447deb8e384fe4a553c119e3ff6bfbab4784", size = 251846, upload-time = "2025-12-28T15:41:33.946Z" }, - { url = "https://files.pythonhosted.org/packages/cc/f7/91fa73c4b80305c86598a2d4e54ba22df6bf7d0d97500944af7ef155d9f7/coverage-7.13.1-cp313-cp313-win32.whl", hash = "sha256:549d195116a1ba1e1ae2f5ca143f9777800f6636eab917d4f02b5310d6d73461", size = 221512, upload-time = "2025-12-28T15:41:35.519Z" }, - { url = "https://files.pythonhosted.org/packages/45/0b/0768b4231d5a044da8f75e097a8714ae1041246bb765d6b5563bab456735/coverage-7.13.1-cp313-cp313-win_amd64.whl", hash = "sha256:5899d28b5276f536fcf840b18b61a9fce23cc3aec1d114c44c07fe94ebeaa500", size = 222321, upload-time = "2025-12-28T15:41:37.371Z" }, - { url = "https://files.pythonhosted.org/packages/9b/b8/bdcb7253b7e85157282450262008f1366aa04663f3e3e4c30436f596c3e2/coverage-7.13.1-cp313-cp313-win_arm64.whl", hash = "sha256:868a2fae76dfb06e87291bcbd4dcbcc778a8500510b618d50496e520bd94d9b9", size = 220949, upload-time = "2025-12-28T15:41:39.553Z" }, - { url = "https://files.pythonhosted.org/packages/70/52/f2be52cc445ff75ea8397948c96c1b4ee14f7f9086ea62fc929c5ae7b717/coverage-7.13.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:67170979de0dacac3f3097d02b0ad188d8edcea44ccc44aaa0550af49150c7dc", size = 219643, upload-time = "2025-12-28T15:41:41.567Z" }, - { url = "https://files.pythonhosted.org/packages/47/79/c85e378eaa239e2edec0c5523f71542c7793fe3340954eafb0bc3904d32d/coverage-7.13.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f80e2bb21bfab56ed7405c2d79d34b5dc0bc96c2c1d2a067b643a09fb756c43a", size = 219997, upload-time = "2025-12-28T15:41:43.418Z" }, - { url = "https://files.pythonhosted.org/packages/fe/9b/b1ade8bfb653c0bbce2d6d6e90cc6c254cbb99b7248531cc76253cb4da6d/coverage-7.13.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f83351e0f7dcdb14d7326c3d8d8c4e915fa685cbfdc6281f9470d97a04e9dfe4", size = 261296, upload-time = "2025-12-28T15:41:45.207Z" }, - { url = "https://files.pythonhosted.org/packages/1f/af/ebf91e3e1a2473d523e87e87fd8581e0aa08741b96265730e2d79ce78d8d/coverage-7.13.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb3f6562e89bad0110afbe64e485aac2462efdce6232cdec7862a095dc3412f6", size = 263363, upload-time = "2025-12-28T15:41:47.163Z" }, - { url = "https://files.pythonhosted.org/packages/c4/8b/fb2423526d446596624ac7fde12ea4262e66f86f5120114c3cfd0bb2befa/coverage-7.13.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77545b5dcda13b70f872c3b5974ac64c21d05e65b1590b441c8560115dc3a0d1", size = 265783, upload-time = "2025-12-28T15:41:49.03Z" }, - { url = "https://files.pythonhosted.org/packages/9b/26/ef2adb1e22674913b89f0fe7490ecadcef4a71fa96f5ced90c60ec358789/coverage-7.13.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4d240d260a1aed814790bbe1f10a5ff31ce6c21bc78f0da4a1e8268d6c80dbd", size = 260508, upload-time = "2025-12-28T15:41:51.035Z" }, - { url = "https://files.pythonhosted.org/packages/ce/7d/f0f59b3404caf662e7b5346247883887687c074ce67ba453ea08c612b1d5/coverage-7.13.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d2287ac9360dec3837bfdad969963a5d073a09a85d898bd86bea82aa8876ef3c", size = 263357, upload-time = "2025-12-28T15:41:52.631Z" }, - { url = "https://files.pythonhosted.org/packages/1a/b1/29896492b0b1a047604d35d6fa804f12818fa30cdad660763a5f3159e158/coverage-7.13.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0d2c11f3ea4db66b5cbded23b20185c35066892c67d80ec4be4bab257b9ad1e0", size = 260978, upload-time = "2025-12-28T15:41:54.589Z" }, - { url = "https://files.pythonhosted.org/packages/48/f2/971de1238a62e6f0a4128d37adadc8bb882ee96afbe03ff1570291754629/coverage-7.13.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:3fc6a169517ca0d7ca6846c3c5392ef2b9e38896f61d615cb75b9e7134d4ee1e", size = 259877, upload-time = "2025-12-28T15:41:56.263Z" }, - { url = "https://files.pythonhosted.org/packages/6a/fc/0474efcbb590ff8628830e9aaec5f1831594874360e3251f1fdec31d07a3/coverage-7.13.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d10a2ed46386e850bb3de503a54f9fe8192e5917fcbb143bfef653a9355e9a53", size = 262069, upload-time = "2025-12-28T15:41:58.093Z" }, - { url = "https://files.pythonhosted.org/packages/88/4f/3c159b7953db37a7b44c0eab8a95c37d1aa4257c47b4602c04022d5cb975/coverage-7.13.1-cp313-cp313t-win32.whl", hash = "sha256:75a6f4aa904301dab8022397a22c0039edc1f51e90b83dbd4464b8a38dc87842", size = 222184, upload-time = "2025-12-28T15:41:59.763Z" }, - { url = "https://files.pythonhosted.org/packages/58/a5/6b57d28f81417f9335774f20679d9d13b9a8fb90cd6160957aa3b54a2379/coverage-7.13.1-cp313-cp313t-win_amd64.whl", hash = "sha256:309ef5706e95e62578cda256b97f5e097916a2c26247c287bbe74794e7150df2", size = 223250, upload-time = "2025-12-28T15:42:01.52Z" }, - { url = "https://files.pythonhosted.org/packages/81/7c/160796f3b035acfbb58be80e02e484548595aa67e16a6345e7910ace0a38/coverage-7.13.1-cp313-cp313t-win_arm64.whl", hash = "sha256:92f980729e79b5d16d221038dbf2e8f9a9136afa072f9d5d6ed4cb984b126a09", size = 221521, upload-time = "2025-12-28T15:42:03.275Z" }, - { url = "https://files.pythonhosted.org/packages/aa/8e/ba0e597560c6563fc0adb902fda6526df5d4aa73bb10adf0574d03bd2206/coverage-7.13.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:97ab3647280d458a1f9adb85244e81587505a43c0c7cff851f5116cd2814b894", size = 218996, upload-time = "2025-12-28T15:42:04.978Z" }, - { url = "https://files.pythonhosted.org/packages/6b/8e/764c6e116f4221dc7aa26c4061181ff92edb9c799adae6433d18eeba7a14/coverage-7.13.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8f572d989142e0908e6acf57ad1b9b86989ff057c006d13b76c146ec6a20216a", size = 219326, upload-time = "2025-12-28T15:42:06.691Z" }, - { url = "https://files.pythonhosted.org/packages/4f/a6/6130dc6d8da28cdcbb0f2bf8865aeca9b157622f7c0031e48c6cf9a0e591/coverage-7.13.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d72140ccf8a147e94274024ff6fd8fb7811354cf7ef88b1f0a988ebaa5bc774f", size = 250374, upload-time = "2025-12-28T15:42:08.786Z" }, - { url = "https://files.pythonhosted.org/packages/82/2b/783ded568f7cd6b677762f780ad338bf4b4750205860c17c25f7c708995e/coverage-7.13.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3c9f051b028810f5a87c88e5d6e9af3c0ff32ef62763bf15d29f740453ca909", size = 252882, upload-time = "2025-12-28T15:42:10.515Z" }, - { url = "https://files.pythonhosted.org/packages/cd/b2/9808766d082e6a4d59eb0cc881a57fc1600eb2c5882813eefff8254f71b5/coverage-7.13.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f398ba4df52d30b1763f62eed9de5620dcde96e6f491f4c62686736b155aa6e4", size = 254218, upload-time = "2025-12-28T15:42:12.208Z" }, - { url = "https://files.pythonhosted.org/packages/44/ea/52a985bb447c871cb4d2e376e401116520991b597c85afdde1ea9ef54f2c/coverage-7.13.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:132718176cc723026d201e347f800cd1a9e4b62ccd3f82476950834dad501c75", size = 250391, upload-time = "2025-12-28T15:42:14.21Z" }, - { url = "https://files.pythonhosted.org/packages/7f/1d/125b36cc12310718873cfc8209ecfbc1008f14f4f5fa0662aa608e579353/coverage-7.13.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e549d642426e3579b3f4b92d0431543b012dcb6e825c91619d4e93b7363c3f9", size = 252239, upload-time = "2025-12-28T15:42:16.292Z" }, - { url = "https://files.pythonhosted.org/packages/6a/16/10c1c164950cade470107f9f14bbac8485f8fb8515f515fca53d337e4a7f/coverage-7.13.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:90480b2134999301eea795b3a9dbf606c6fbab1b489150c501da84a959442465", size = 250196, upload-time = "2025-12-28T15:42:18.54Z" }, - { url = "https://files.pythonhosted.org/packages/2a/c6/cd860fac08780c6fd659732f6ced1b40b79c35977c1356344e44d72ba6c4/coverage-7.13.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e825dbb7f84dfa24663dd75835e7257f8882629fc11f03ecf77d84a75134b864", size = 250008, upload-time = "2025-12-28T15:42:20.365Z" }, - { url = "https://files.pythonhosted.org/packages/f0/3a/a8c58d3d38f82a5711e1e0a67268362af48e1a03df27c03072ac30feefcf/coverage-7.13.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:623dcc6d7a7ba450bbdbeedbaa0c42b329bdae16491af2282f12a7e809be7eb9", size = 251671, upload-time = "2025-12-28T15:42:22.114Z" }, - { url = "https://files.pythonhosted.org/packages/f0/bc/fd4c1da651d037a1e3d53e8cb3f8182f4b53271ffa9a95a2e211bacc0349/coverage-7.13.1-cp314-cp314-win32.whl", hash = "sha256:6e73ebb44dca5f708dc871fe0b90cf4cff1a13f9956f747cc87b535a840386f5", size = 221777, upload-time = "2025-12-28T15:42:23.919Z" }, - { url = "https://files.pythonhosted.org/packages/4b/50/71acabdc8948464c17e90b5ffd92358579bd0910732c2a1c9537d7536aa6/coverage-7.13.1-cp314-cp314-win_amd64.whl", hash = "sha256:be753b225d159feb397bd0bf91ae86f689bad0da09d3b301478cd39b878ab31a", size = 222592, upload-time = "2025-12-28T15:42:25.619Z" }, - { url = "https://files.pythonhosted.org/packages/f7/c8/a6fb943081bb0cc926499c7907731a6dc9efc2cbdc76d738c0ab752f1a32/coverage-7.13.1-cp314-cp314-win_arm64.whl", hash = "sha256:228b90f613b25ba0019361e4ab81520b343b622fc657daf7e501c4ed6a2366c0", size = 221169, upload-time = "2025-12-28T15:42:27.629Z" }, - { url = "https://files.pythonhosted.org/packages/16/61/d5b7a0a0e0e40d62e59bc8c7aa1afbd86280d82728ba97f0673b746b78e2/coverage-7.13.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:60cfb538fe9ef86e5b2ab0ca8fc8d62524777f6c611dcaf76dc16fbe9b8e698a", size = 219730, upload-time = "2025-12-28T15:42:29.306Z" }, - { url = "https://files.pythonhosted.org/packages/a3/2c/8881326445fd071bb49514d1ce97d18a46a980712b51fee84f9ab42845b4/coverage-7.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:57dfc8048c72ba48a8c45e188d811e5efd7e49b387effc8fb17e97936dde5bf6", size = 220001, upload-time = "2025-12-28T15:42:31.319Z" }, - { url = "https://files.pythonhosted.org/packages/b5/d7/50de63af51dfa3a7f91cc37ad8fcc1e244b734232fbc8b9ab0f3c834a5cd/coverage-7.13.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3f2f725aa3e909b3c5fdb8192490bdd8e1495e85906af74fe6e34a2a77ba0673", size = 261370, upload-time = "2025-12-28T15:42:32.992Z" }, - { url = "https://files.pythonhosted.org/packages/e1/2c/d31722f0ec918fd7453b2758312729f645978d212b410cd0f7c2aed88a94/coverage-7.13.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ee68b21909686eeb21dfcba2c3b81fee70dcf38b140dcd5aa70680995fa3aa5", size = 263485, upload-time = "2025-12-28T15:42:34.759Z" }, - { url = "https://files.pythonhosted.org/packages/fa/7a/2c114fa5c5fc08ba0777e4aec4c97e0b4a1afcb69c75f1f54cff78b073ab/coverage-7.13.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724b1b270cb13ea2e6503476e34541a0b1f62280bc997eab443f87790202033d", size = 265890, upload-time = "2025-12-28T15:42:36.517Z" }, - { url = "https://files.pythonhosted.org/packages/65/d9/f0794aa1c74ceabc780fe17f6c338456bbc4e96bd950f2e969f48ac6fb20/coverage-7.13.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:916abf1ac5cf7eb16bc540a5bf75c71c43a676f5c52fcb9fe75a2bd75fb944e8", size = 260445, upload-time = "2025-12-28T15:42:38.646Z" }, - { url = "https://files.pythonhosted.org/packages/49/23/184b22a00d9bb97488863ced9454068c79e413cb23f472da6cbddc6cfc52/coverage-7.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:776483fd35b58d8afe3acbd9988d5de592ab6da2d2a865edfdbc9fdb43e7c486", size = 263357, upload-time = "2025-12-28T15:42:40.788Z" }, - { url = "https://files.pythonhosted.org/packages/7d/bd/58af54c0c9199ea4190284f389005779d7daf7bf3ce40dcd2d2b2f96da69/coverage-7.13.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b6f3b96617e9852703f5b633ea01315ca45c77e879584f283c44127f0f1ec564", size = 260959, upload-time = "2025-12-28T15:42:42.808Z" }, - { url = "https://files.pythonhosted.org/packages/4b/2a/6839294e8f78a4891bf1df79d69c536880ba2f970d0ff09e7513d6e352e9/coverage-7.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd63e7b74661fed317212fab774e2a648bc4bb09b35f25474f8e3325d2945cd7", size = 259792, upload-time = "2025-12-28T15:42:44.818Z" }, - { url = "https://files.pythonhosted.org/packages/ba/c3/528674d4623283310ad676c5af7414b9850ab6d55c2300e8aa4b945ec554/coverage-7.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:933082f161bbb3e9f90d00990dc956120f608cdbcaeea15c4d897f56ef4fe416", size = 262123, upload-time = "2025-12-28T15:42:47.108Z" }, - { url = "https://files.pythonhosted.org/packages/06/c5/8c0515692fb4c73ac379d8dc09b18eaf0214ecb76ea6e62467ba7a1556ff/coverage-7.13.1-cp314-cp314t-win32.whl", hash = "sha256:18be793c4c87de2965e1c0f060f03d9e5aff66cfeae8e1dbe6e5b88056ec153f", size = 222562, upload-time = "2025-12-28T15:42:49.144Z" }, - { url = "https://files.pythonhosted.org/packages/05/0e/c0a0c4678cb30dac735811db529b321d7e1c9120b79bd728d4f4d6b010e9/coverage-7.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:0e42e0ec0cd3e0d851cb3c91f770c9301f48647cb2877cb78f74bdaa07639a79", size = 223670, upload-time = "2025-12-28T15:42:51.218Z" }, - { url = "https://files.pythonhosted.org/packages/f5/5f/b177aa0011f354abf03a8f30a85032686d290fdeed4222b27d36b4372a50/coverage-7.13.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eaecf47ef10c72ece9a2a92118257da87e460e113b83cc0d2905cbbe931792b4", size = 221707, upload-time = "2025-12-28T15:42:53.034Z" }, - { url = "https://files.pythonhosted.org/packages/cc/48/d9f421cb8da5afaa1a64570d9989e00fb7955e6acddc5a12979f7666ef60/coverage-7.13.1-py3-none-any.whl", hash = "sha256:2016745cb3ba554469d02819d78958b571792bb68e31302610e898f80dd3a573", size = 210722, upload-time = "2025-12-28T15:42:54.901Z" }, +version = "7.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ad/49/349848445b0e53660e258acbcc9b0d014895b6739237920886672240f84b/coverage-7.13.2.tar.gz", hash = "sha256:044c6951ec37146b72a50cc81ef02217d27d4c3640efd2640311393cbbf143d3", size = 826523, upload-time = "2026-01-25T13:00:04.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/01/abca50583a8975bb6e1c59eff67ed8e48bb127c07dad5c28d9e96ccc09ec/coverage-7.13.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:060ebf6f2c51aff5ba38e1f43a2095e087389b1c69d559fde6049a4b0001320e", size = 218971, upload-time = "2026-01-25T12:57:36.953Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0e/b6489f344d99cd1e5b4d5e1be52dfd3f8a3dc5112aa6c33948da8cabad4e/coverage-7.13.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1ea8ca9db5e7469cd364552985e15911548ea5b69c48a17291f0cac70484b2e", size = 219473, upload-time = "2026-01-25T12:57:38.934Z" }, + { url = "https://files.pythonhosted.org/packages/17/11/db2f414915a8e4ec53f60b17956c27f21fb68fcf20f8a455ce7c2ccec638/coverage-7.13.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b780090d15fd58f07cf2011943e25a5f0c1c894384b13a216b6c86c8a8a7c508", size = 249896, upload-time = "2026-01-25T12:57:40.365Z" }, + { url = "https://files.pythonhosted.org/packages/80/06/0823fe93913663c017e508e8810c998c8ebd3ec2a5a85d2c3754297bdede/coverage-7.13.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:88a800258d83acb803c38175b4495d293656d5fac48659c953c18e5f539a274b", size = 251810, upload-time = "2026-01-25T12:57:42.045Z" }, + { url = "https://files.pythonhosted.org/packages/61/dc/b151c3cc41b28cdf7f0166c5fa1271cbc305a8ec0124cce4b04f74791a18/coverage-7.13.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6326e18e9a553e674d948536a04a80d850a5eeefe2aae2e6d7cf05d54046c01b", size = 253920, upload-time = "2026-01-25T12:57:44.026Z" }, + { url = "https://files.pythonhosted.org/packages/2d/35/e83de0556e54a4729a2b94ea816f74ce08732e81945024adee46851c2264/coverage-7.13.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:59562de3f797979e1ff07c587e2ac36ba60ca59d16c211eceaa579c266c5022f", size = 250025, upload-time = "2026-01-25T12:57:45.624Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/af2eb9c3926ce3ea0d58a0d2516fcbdacf7a9fc9559fe63076beaf3f2596/coverage-7.13.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:27ba1ed6f66b0e2d61bfa78874dffd4f8c3a12f8e2b5410e515ab345ba7bc9c3", size = 251612, upload-time = "2026-01-25T12:57:47.713Z" }, + { url = "https://files.pythonhosted.org/packages/26/62/5be2e25f3d6c711d23b71296f8b44c978d4c8b4e5b26871abfc164297502/coverage-7.13.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8be48da4d47cc68754ce643ea50b3234557cbefe47c2f120495e7bd0a2756f2b", size = 249670, upload-time = "2026-01-25T12:57:49.378Z" }, + { url = "https://files.pythonhosted.org/packages/b3/51/400d1b09a8344199f9b6a6fc1868005d766b7ea95e7882e494fa862ca69c/coverage-7.13.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2a47a4223d3361b91176aedd9d4e05844ca67d7188456227b6bf5e436630c9a1", size = 249395, upload-time = "2026-01-25T12:57:50.86Z" }, + { url = "https://files.pythonhosted.org/packages/e0/36/f02234bc6e5230e2f0a63fd125d0a2093c73ef20fdf681c7af62a140e4e7/coverage-7.13.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c6f141b468740197d6bd38f2b26ade124363228cc3f9858bd9924ab059e00059", size = 250298, upload-time = "2026-01-25T12:57:52.287Z" }, + { url = "https://files.pythonhosted.org/packages/b0/06/713110d3dd3151b93611c9cbfc65c15b4156b44f927fced49ac0b20b32a4/coverage-7.13.2-cp311-cp311-win32.whl", hash = "sha256:89567798404af067604246e01a49ef907d112edf2b75ef814b1364d5ce267031", size = 221485, upload-time = "2026-01-25T12:57:53.876Z" }, + { url = "https://files.pythonhosted.org/packages/16/0c/3ae6255fa1ebcb7dec19c9a59e85ef5f34566d1265c70af5b2fc981da834/coverage-7.13.2-cp311-cp311-win_amd64.whl", hash = "sha256:21dd57941804ae2ac7e921771a5e21bbf9aabec317a041d164853ad0a96ce31e", size = 222421, upload-time = "2026-01-25T12:57:55.433Z" }, + { url = "https://files.pythonhosted.org/packages/b5/37/fabc3179af4d61d89ea47bd04333fec735cd5e8b59baad44fed9fc4170d7/coverage-7.13.2-cp311-cp311-win_arm64.whl", hash = "sha256:10758e0586c134a0bafa28f2d37dd2cdb5e4a90de25c0fc0c77dabbad46eca28", size = 221088, upload-time = "2026-01-25T12:57:57.41Z" }, + { url = "https://files.pythonhosted.org/packages/46/39/e92a35f7800222d3f7b2cbb7bbc3b65672ae8d501cb31801b2d2bd7acdf1/coverage-7.13.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f106b2af193f965d0d3234f3f83fc35278c7fb935dfbde56ae2da3dd2c03b84d", size = 219142, upload-time = "2026-01-25T12:58:00.448Z" }, + { url = "https://files.pythonhosted.org/packages/45/7a/8bf9e9309c4c996e65c52a7c5a112707ecdd9fbaf49e10b5a705a402bbb4/coverage-7.13.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78f45d21dc4d5d6bd29323f0320089ef7eae16e4bef712dff79d184fa7330af3", size = 219503, upload-time = "2026-01-25T12:58:02.451Z" }, + { url = "https://files.pythonhosted.org/packages/87/93/17661e06b7b37580923f3f12406ac91d78aeed293fb6da0b69cc7957582f/coverage-7.13.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fae91dfecd816444c74531a9c3d6ded17a504767e97aa674d44f638107265b99", size = 251006, upload-time = "2026-01-25T12:58:04.059Z" }, + { url = "https://files.pythonhosted.org/packages/12/f0/f9e59fb8c310171497f379e25db060abef9fa605e09d63157eebec102676/coverage-7.13.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:264657171406c114787b441484de620e03d8f7202f113d62fcd3d9688baa3e6f", size = 253750, upload-time = "2026-01-25T12:58:05.574Z" }, + { url = "https://files.pythonhosted.org/packages/e5/b1/1935e31add2232663cf7edd8269548b122a7d100047ff93475dbaaae673e/coverage-7.13.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae47d8dcd3ded0155afbb59c62bd8ab07ea0fd4902e1c40567439e6db9dcaf2f", size = 254862, upload-time = "2026-01-25T12:58:07.647Z" }, + { url = "https://files.pythonhosted.org/packages/af/59/b5e97071ec13df5f45da2b3391b6cdbec78ba20757bc92580a5b3d5fa53c/coverage-7.13.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8a0b33e9fd838220b007ce8f299114d406c1e8edb21336af4c97a26ecfd185aa", size = 251420, upload-time = "2026-01-25T12:58:09.309Z" }, + { url = "https://files.pythonhosted.org/packages/3f/75/9495932f87469d013dc515fb0ce1aac5fa97766f38f6b1a1deb1ee7b7f3a/coverage-7.13.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b3becbea7f3ce9a2d4d430f223ec15888e4deb31395840a79e916368d6004cce", size = 252786, upload-time = "2026-01-25T12:58:10.909Z" }, + { url = "https://files.pythonhosted.org/packages/6a/59/af550721f0eb62f46f7b8cb7e6f1860592189267b1c411a4e3a057caacee/coverage-7.13.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f819c727a6e6eeb8711e4ce63d78c620f69630a2e9d53bc95ca5379f57b6ba94", size = 250928, upload-time = "2026-01-25T12:58:12.449Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b1/21b4445709aae500be4ab43bbcfb4e53dc0811c3396dcb11bf9f23fd0226/coverage-7.13.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:4f7b71757a3ab19f7ba286e04c181004c1d61be921795ee8ba6970fd0ec91da5", size = 250496, upload-time = "2026-01-25T12:58:14.047Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b1/0f5d89dfe0392990e4f3980adbde3eb34885bc1effb2dc369e0bf385e389/coverage-7.13.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b7fc50d2afd2e6b4f6f2f403b70103d280a8e0cb35320cbbe6debcda02a1030b", size = 252373, upload-time = "2026-01-25T12:58:15.976Z" }, + { url = "https://files.pythonhosted.org/packages/01/c9/0cf1a6a57a9968cc049a6b896693faa523c638a5314b1fc374eb2b2ac904/coverage-7.13.2-cp312-cp312-win32.whl", hash = "sha256:292250282cf9bcf206b543d7608bda17ca6fc151f4cbae949fc7e115112fbd41", size = 221696, upload-time = "2026-01-25T12:58:17.517Z" }, + { url = "https://files.pythonhosted.org/packages/4d/05/d7540bf983f09d32803911afed135524570f8c47bb394bf6206c1dc3a786/coverage-7.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:eeea10169fac01549a7921d27a3e517194ae254b542102267bef7a93ed38c40e", size = 222504, upload-time = "2026-01-25T12:58:19.115Z" }, + { url = "https://files.pythonhosted.org/packages/15/8b/1a9f037a736ced0a12aacf6330cdaad5008081142a7070bc58b0f7930cbc/coverage-7.13.2-cp312-cp312-win_arm64.whl", hash = "sha256:2a5b567f0b635b592c917f96b9a9cb3dbd4c320d03f4bf94e9084e494f2e8894", size = 221120, upload-time = "2026-01-25T12:58:21.334Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f0/3d3eac7568ab6096ff23791a526b0048a1ff3f49d0e236b2af6fb6558e88/coverage-7.13.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ed75de7d1217cf3b99365d110975f83af0528c849ef5180a12fd91b5064df9d6", size = 219168, upload-time = "2026-01-25T12:58:23.376Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a6/f8b5cfeddbab95fdef4dcd682d82e5dcff7a112ced57a959f89537ee9995/coverage-7.13.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:97e596de8fa9bada4d88fde64a3f4d37f1b6131e4faa32bad7808abc79887ddc", size = 219537, upload-time = "2026-01-25T12:58:24.932Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e6/8d8e6e0c516c838229d1e41cadcec91745f4b1031d4db17ce0043a0423b4/coverage-7.13.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:68c86173562ed4413345410c9480a8d64864ac5e54a5cda236748031e094229f", size = 250528, upload-time = "2026-01-25T12:58:26.567Z" }, + { url = "https://files.pythonhosted.org/packages/8e/78/befa6640f74092b86961f957f26504c8fba3d7da57cc2ab7407391870495/coverage-7.13.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7be4d613638d678b2b3773b8f687537b284d7074695a43fe2fbbfc0e31ceaed1", size = 253132, upload-time = "2026-01-25T12:58:28.251Z" }, + { url = "https://files.pythonhosted.org/packages/9d/10/1630db1edd8ce675124a2ee0f7becc603d2bb7b345c2387b4b95c6907094/coverage-7.13.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7f63ce526a96acd0e16c4af8b50b64334239550402fb1607ce6a584a6d62ce9", size = 254374, upload-time = "2026-01-25T12:58:30.294Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1d/0d9381647b1e8e6d310ac4140be9c428a0277330991e0c35bdd751e338a4/coverage-7.13.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:406821f37f864f968e29ac14c3fccae0fec9fdeba48327f0341decf4daf92d7c", size = 250762, upload-time = "2026-01-25T12:58:32.036Z" }, + { url = "https://files.pythonhosted.org/packages/43/e4/5636dfc9a7c871ee8776af83ee33b4c26bc508ad6cee1e89b6419a366582/coverage-7.13.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ee68e5a4e3e5443623406b905db447dceddffee0dceb39f4e0cd9ec2a35004b5", size = 252502, upload-time = "2026-01-25T12:58:33.961Z" }, + { url = "https://files.pythonhosted.org/packages/02/2a/7ff2884d79d420cbb2d12fed6fff727b6d0ef27253140d3cdbbd03187ee0/coverage-7.13.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2ee0e58cca0c17dd9c6c1cdde02bb705c7b3fbfa5f3b0b5afeda20d4ebff8ef4", size = 250463, upload-time = "2026-01-25T12:58:35.529Z" }, + { url = "https://files.pythonhosted.org/packages/91/c0/ba51087db645b6c7261570400fc62c89a16278763f36ba618dc8657a187b/coverage-7.13.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e5bbb5018bf76a56aabdb64246b5288d5ae1b7d0dd4d0534fe86df2c2992d1c", size = 250288, upload-time = "2026-01-25T12:58:37.226Z" }, + { url = "https://files.pythonhosted.org/packages/03/07/44e6f428551c4d9faf63ebcefe49b30e5c89d1be96f6a3abd86a52da9d15/coverage-7.13.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a55516c68ef3e08e134e818d5e308ffa6b1337cc8b092b69b24287bf07d38e31", size = 252063, upload-time = "2026-01-25T12:58:38.821Z" }, + { url = "https://files.pythonhosted.org/packages/c2/67/35b730ad7e1859dd57e834d1bc06080d22d2f87457d53f692fce3f24a5a9/coverage-7.13.2-cp313-cp313-win32.whl", hash = "sha256:5b20211c47a8abf4abc3319d8ce2464864fa9f30c5fcaf958a3eed92f4f1fef8", size = 221716, upload-time = "2026-01-25T12:58:40.484Z" }, + { url = "https://files.pythonhosted.org/packages/0d/82/e5fcf5a97c72f45fc14829237a6550bf49d0ab882ac90e04b12a69db76b4/coverage-7.13.2-cp313-cp313-win_amd64.whl", hash = "sha256:14f500232e521201cf031549fb1ebdfc0a40f401cf519157f76c397e586c3beb", size = 222522, upload-time = "2026-01-25T12:58:43.247Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/25d7b2f946d239dd2d6644ca2cc060d24f97551e2af13b6c24c722ae5f97/coverage-7.13.2-cp313-cp313-win_arm64.whl", hash = "sha256:9779310cb5a9778a60c899f075a8514c89fa6d10131445c2207fc893e0b14557", size = 221145, upload-time = "2026-01-25T12:58:45Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f7/080376c029c8f76fadfe43911d0daffa0cbdc9f9418a0eead70c56fb7f4b/coverage-7.13.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e64fa5a1e41ce5df6b547cbc3d3699381c9e2c2c369c67837e716ed0f549d48e", size = 219861, upload-time = "2026-01-25T12:58:46.586Z" }, + { url = "https://files.pythonhosted.org/packages/42/11/0b5e315af5ab35f4c4a70e64d3314e4eec25eefc6dec13be3a7d5ffe8ac5/coverage-7.13.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b01899e82a04085b6561eb233fd688474f57455e8ad35cd82286463ba06332b7", size = 220207, upload-time = "2026-01-25T12:58:48.277Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0c/0874d0318fb1062117acbef06a09cf8b63f3060c22265adaad24b36306b7/coverage-7.13.2-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:838943bea48be0e2768b0cf7819544cdedc1bbb2f28427eabb6eb8c9eb2285d3", size = 261504, upload-time = "2026-01-25T12:58:49.904Z" }, + { url = "https://files.pythonhosted.org/packages/83/5e/1cd72c22ecb30751e43a72f40ba50fcef1b7e93e3ea823bd9feda8e51f9a/coverage-7.13.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:93d1d25ec2b27e90bcfef7012992d1f5121b51161b8bffcda756a816cf13c2c3", size = 263582, upload-time = "2026-01-25T12:58:51.582Z" }, + { url = "https://files.pythonhosted.org/packages/9b/da/8acf356707c7a42df4d0657020308e23e5a07397e81492640c186268497c/coverage-7.13.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93b57142f9621b0d12349c43fc7741fe578e4bc914c1e5a54142856cfc0bf421", size = 266008, upload-time = "2026-01-25T12:58:53.234Z" }, + { url = "https://files.pythonhosted.org/packages/41/41/ea1730af99960309423c6ea8d6a4f1fa5564b2d97bd1d29dda4b42611f04/coverage-7.13.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f06799ae1bdfff7ccb8665d75f8291c69110ba9585253de254688aa8a1ccc6c5", size = 260762, upload-time = "2026-01-25T12:58:55.372Z" }, + { url = "https://files.pythonhosted.org/packages/22/fa/02884d2080ba71db64fdc127b311db60e01fe6ba797d9c8363725e39f4d5/coverage-7.13.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7f9405ab4f81d490811b1d91c7a20361135a2df4c170e7f0b747a794da5b7f23", size = 263571, upload-time = "2026-01-25T12:58:57.52Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6b/4083aaaeba9b3112f55ac57c2ce7001dc4d8fa3fcc228a39f09cc84ede27/coverage-7.13.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f9ab1d5b86f8fbc97a5b3cd6280a3fd85fef3b028689d8a2c00918f0d82c728c", size = 261200, upload-time = "2026-01-25T12:58:59.255Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d2/aea92fa36d61955e8c416ede9cf9bf142aa196f3aea214bb67f85235a050/coverage-7.13.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:f674f59712d67e841525b99e5e2b595250e39b529c3bda14764e4f625a3fa01f", size = 260095, upload-time = "2026-01-25T12:59:01.066Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ae/04ffe96a80f107ea21b22b2367175c621da920063260a1c22f9452fd7866/coverage-7.13.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c6cadac7b8ace1ba9144feb1ae3cb787a6065ba6d23ffc59a934b16406c26573", size = 262284, upload-time = "2026-01-25T12:59:02.802Z" }, + { url = "https://files.pythonhosted.org/packages/1c/7a/6f354dcd7dfc41297791d6fb4e0d618acb55810bde2c1fd14b3939e05c2b/coverage-7.13.2-cp313-cp313t-win32.whl", hash = "sha256:14ae4146465f8e6e6253eba0cccd57423e598a4cb925958b240c805300918343", size = 222389, upload-time = "2026-01-25T12:59:04.563Z" }, + { url = "https://files.pythonhosted.org/packages/8d/d5/080ad292a4a3d3daf411574be0a1f56d6dee2c4fdf6b005342be9fac807f/coverage-7.13.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9074896edd705a05769e3de0eac0a8388484b503b68863dd06d5e473f874fd47", size = 223450, upload-time = "2026-01-25T12:59:06.677Z" }, + { url = "https://files.pythonhosted.org/packages/88/96/df576fbacc522e9fb8d1c4b7a7fc62eb734be56e2cba1d88d2eabe08ea3f/coverage-7.13.2-cp313-cp313t-win_arm64.whl", hash = "sha256:69e526e14f3f854eda573d3cf40cffd29a1a91c684743d904c33dbdcd0e0f3e7", size = 221707, upload-time = "2026-01-25T12:59:08.363Z" }, + { url = "https://files.pythonhosted.org/packages/55/53/1da9e51a0775634b04fcc11eb25c002fc58ee4f92ce2e8512f94ac5fc5bf/coverage-7.13.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:387a825f43d680e7310e6f325b2167dd093bc8ffd933b83e9aa0983cf6e0a2ef", size = 219213, upload-time = "2026-01-25T12:59:11.909Z" }, + { url = "https://files.pythonhosted.org/packages/46/35/b3caac3ebbd10230fea5a33012b27d19e999a17c9285c4228b4b2e35b7da/coverage-7.13.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f0d7fea9d8e5d778cd5a9e8fc38308ad688f02040e883cdc13311ef2748cb40f", size = 219549, upload-time = "2026-01-25T12:59:13.638Z" }, + { url = "https://files.pythonhosted.org/packages/76/9c/e1cf7def1bdc72c1907e60703983a588f9558434a2ff94615747bd73c192/coverage-7.13.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e080afb413be106c95c4ee96b4fffdc9e2fa56a8bbf90b5c0918e5c4449412f5", size = 250586, upload-time = "2026-01-25T12:59:15.808Z" }, + { url = "https://files.pythonhosted.org/packages/ba/49/f54ec02ed12be66c8d8897270505759e057b0c68564a65c429ccdd1f139e/coverage-7.13.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a7fc042ba3c7ce25b8a9f097eb0f32a5ce1ccdb639d9eec114e26def98e1f8a4", size = 253093, upload-time = "2026-01-25T12:59:17.491Z" }, + { url = "https://files.pythonhosted.org/packages/fb/5e/aaf86be3e181d907e23c0f61fccaeb38de8e6f6b47aed92bf57d8fc9c034/coverage-7.13.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d0ba505e021557f7f8173ee8cd6b926373d8653e5ff7581ae2efce1b11ef4c27", size = 254446, upload-time = "2026-01-25T12:59:19.752Z" }, + { url = "https://files.pythonhosted.org/packages/28/c8/a5fa01460e2d75b0c853b392080d6829d3ca8b5ab31e158fa0501bc7c708/coverage-7.13.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7de326f80e3451bd5cc7239ab46c73ddb658fe0b7649476bc7413572d36cd548", size = 250615, upload-time = "2026-01-25T12:59:21.928Z" }, + { url = "https://files.pythonhosted.org/packages/86/0b/6d56315a55f7062bb66410732c24879ccb2ec527ab6630246de5fe45a1df/coverage-7.13.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:abaea04f1e7e34841d4a7b343904a3f59481f62f9df39e2cd399d69a187a9660", size = 252452, upload-time = "2026-01-25T12:59:23.592Z" }, + { url = "https://files.pythonhosted.org/packages/30/19/9bc550363ebc6b0ea121977ee44d05ecd1e8bf79018b8444f1028701c563/coverage-7.13.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9f93959ee0c604bccd8e0697be21de0887b1f73efcc3aa73a3ec0fd13feace92", size = 250418, upload-time = "2026-01-25T12:59:25.392Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/580530a31ca2f0cc6f07a8f2ab5460785b02bb11bdf815d4c4d37a4c5169/coverage-7.13.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:13fe81ead04e34e105bf1b3c9f9cdf32ce31736ee5d90a8d2de02b9d3e1bcb82", size = 250231, upload-time = "2026-01-25T12:59:27.888Z" }, + { url = "https://files.pythonhosted.org/packages/e2/42/dd9093f919dc3088cb472893651884bd675e3df3d38a43f9053656dca9a2/coverage-7.13.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d6d16b0f71120e365741bca2cb473ca6fe38930bc5431c5e850ba949f708f892", size = 251888, upload-time = "2026-01-25T12:59:29.636Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a6/0af4053e6e819774626e133c3d6f70fae4d44884bfc4b126cb647baee8d3/coverage-7.13.2-cp314-cp314-win32.whl", hash = "sha256:9b2f4714bb7d99ba3790ee095b3b4ac94767e1347fe424278a0b10acb3ff04fe", size = 221968, upload-time = "2026-01-25T12:59:31.424Z" }, + { url = "https://files.pythonhosted.org/packages/c4/cc/5aff1e1f80d55862442855517bb8ad8ad3a68639441ff6287dde6a58558b/coverage-7.13.2-cp314-cp314-win_amd64.whl", hash = "sha256:e4121a90823a063d717a96e0a0529c727fb31ea889369a0ee3ec00ed99bf6859", size = 222783, upload-time = "2026-01-25T12:59:33.118Z" }, + { url = "https://files.pythonhosted.org/packages/de/20/09abafb24f84b3292cc658728803416c15b79f9ee5e68d25238a895b07d9/coverage-7.13.2-cp314-cp314-win_arm64.whl", hash = "sha256:6873f0271b4a15a33e7590f338d823f6f66f91ed147a03938d7ce26efd04eee6", size = 221348, upload-time = "2026-01-25T12:59:34.939Z" }, + { url = "https://files.pythonhosted.org/packages/b6/60/a3820c7232db63be060e4019017cd3426751c2699dab3c62819cdbcea387/coverage-7.13.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f61d349f5b7cd95c34017f1927ee379bfbe9884300d74e07cf630ccf7a610c1b", size = 219950, upload-time = "2026-01-25T12:59:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/fd/37/e4ef5975fdeb86b1e56db9a82f41b032e3d93a840ebaf4064f39e770d5c5/coverage-7.13.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a43d34ce714f4ca674c0d90beb760eb05aad906f2c47580ccee9da8fe8bfb417", size = 220209, upload-time = "2026-01-25T12:59:38.339Z" }, + { url = "https://files.pythonhosted.org/packages/54/df/d40e091d00c51adca1e251d3b60a8b464112efa3004949e96a74d7c19a64/coverage-7.13.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bff1b04cb9d4900ce5c56c4942f047dc7efe57e2608cb7c3c8936e9970ccdbee", size = 261576, upload-time = "2026-01-25T12:59:40.446Z" }, + { url = "https://files.pythonhosted.org/packages/c5/44/5259c4bed54e3392e5c176121af9f71919d96dde853386e7730e705f3520/coverage-7.13.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6ae99e4560963ad8e163e819e5d77d413d331fd00566c1e0856aa252303552c1", size = 263704, upload-time = "2026-01-25T12:59:42.346Z" }, + { url = "https://files.pythonhosted.org/packages/16/bd/ae9f005827abcbe2c70157459ae86053971c9fa14617b63903abbdce26d9/coverage-7.13.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e79a8c7d461820257d9aa43716c4efc55366d7b292e46b5b37165be1d377405d", size = 266109, upload-time = "2026-01-25T12:59:44.073Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c0/8e279c1c0f5b1eaa3ad9b0fb7a5637fc0379ea7d85a781c0fe0bb3cfc2ab/coverage-7.13.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:060ee84f6a769d40c492711911a76811b4befb6fba50abb450371abb720f5bd6", size = 260686, upload-time = "2026-01-25T12:59:45.804Z" }, + { url = "https://files.pythonhosted.org/packages/b2/47/3a8112627e9d863e7cddd72894171c929e94491a597811725befdcd76bce/coverage-7.13.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bca209d001fd03ea2d978f8a4985093240a355c93078aee3f799852c23f561a", size = 263568, upload-time = "2026-01-25T12:59:47.929Z" }, + { url = "https://files.pythonhosted.org/packages/92/bc/7ea367d84afa3120afc3ce6de294fd2dcd33b51e2e7fbe4bbfd200f2cb8c/coverage-7.13.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6b8092aa38d72f091db61ef83cb66076f18f02da3e1a75039a4f218629600e04", size = 261174, upload-time = "2026-01-25T12:59:49.717Z" }, + { url = "https://files.pythonhosted.org/packages/33/b7/f1092dcecb6637e31cc2db099581ee5c61a17647849bae6b8261a2b78430/coverage-7.13.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4a3158dc2dcce5200d91ec28cd315c999eebff355437d2765840555d765a6e5f", size = 260017, upload-time = "2026-01-25T12:59:51.463Z" }, + { url = "https://files.pythonhosted.org/packages/2b/cd/f3d07d4b95fbe1a2ef0958c15da614f7e4f557720132de34d2dc3aa7e911/coverage-7.13.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3973f353b2d70bd9796cc12f532a05945232ccae966456c8ed7034cb96bbfd6f", size = 262337, upload-time = "2026-01-25T12:59:53.407Z" }, + { url = "https://files.pythonhosted.org/packages/e0/db/b0d5b2873a07cb1e06a55d998697c0a5a540dcefbf353774c99eb3874513/coverage-7.13.2-cp314-cp314t-win32.whl", hash = "sha256:79f6506a678a59d4ded048dc72f1859ebede8ec2b9a2d509ebe161f01c2879d3", size = 222749, upload-time = "2026-01-25T12:59:56.316Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2f/838a5394c082ac57d85f57f6aba53093b30d9089781df72412126505716f/coverage-7.13.2-cp314-cp314t-win_amd64.whl", hash = "sha256:196bfeabdccc5a020a57d5a368c681e3a6ceb0447d153aeccc1ab4d70a5032ba", size = 223857, upload-time = "2026-01-25T12:59:58.201Z" }, + { url = "https://files.pythonhosted.org/packages/44/d4/b608243e76ead3a4298824b50922b89ef793e50069ce30316a65c1b4d7ef/coverage-7.13.2-cp314-cp314t-win_arm64.whl", hash = "sha256:69269ab58783e090bfbf5b916ab3d188126e22d6070bbfc93098fdd474ef937c", size = 221881, upload-time = "2026-01-25T13:00:00.449Z" }, + { url = "https://files.pythonhosted.org/packages/d2/db/d291e30fdf7ea617a335531e72294e0c723356d7fdde8fba00610a76bda9/coverage-7.13.2-py3-none-any.whl", hash = "sha256:40ce1ea1e25125556d8e76bd0b61500839a07944cc287ac21d5626f3e620cad5", size = 210943, upload-time = "2026-01-25T13:00:02.388Z" }, ] [package.optional-dependencies] @@ -1320,11 +1315,11 @@ numpy = [ [[package]] name = "packaging" -version = "25.0" +version = "26.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, ] [[package]] @@ -1505,11 +1500,11 @@ wheels = [ [[package]] name = "pyparsing" -version = "3.3.1" +version = "3.3.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/33/c1/1d9de9aeaa1b89b0186e5fe23294ff6517fce1bc69149185577cd31016b2/pyparsing-3.3.1.tar.gz", hash = "sha256:47fad0f17ac1e2cad3de3b458570fbc9b03560aa029ed5e16ee5554da9a2251c", size = 1550512, upload-time = "2025-12-23T03:14:04.391Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/40/2614036cdd416452f5bf98ec037f38a1afb17f327cb8e6b652d4729e0af8/pyparsing-3.3.1-py3-none-any.whl", hash = "sha256:023b5e7e5520ad96642e2c6db4cb683d3970bd640cdf7115049a6e9c3682df82", size = 121793, upload-time = "2025-12-23T03:14:02.103Z" }, + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, ] [[package]] @@ -1676,28 +1671,28 @@ wheels = [ [[package]] name = "ruff" -version = "0.14.12" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/85/81/5fd87d61352fb0f86b4436f278fe19b3770a8b73d42e8b3405d28df6b759/ruff-0.14.12.tar.gz", hash = "sha256:276b0821947f2afff8ee6da282bade96459d2e29f5a203eef04eb7b7a85b119f", size = 6055422, upload-time = "2026-01-15T16:22:12.371Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/de/58c5e3b4e6be8d0e007856128be59688cd4025a5345693dd169d61df8eb2/ruff-0.14.12-py3-none-linux_armv6l.whl", hash = "sha256:59434a99f0af57111f62cd77e86b4d4896a2c72bb90cc039d1ac501b151b798b", size = 13046871, upload-time = "2026-01-15T16:21:53.033Z" }, - { url = "https://files.pythonhosted.org/packages/dd/b3/d9710419b6aed406a41c7eb215d604a55d2137f2a60e24d0939dae081b1b/ruff-0.14.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a5912e8270c6b9ed28ae50b993032d195046293fefa56127a8dffbaa5e5bfc04", size = 13433477, upload-time = "2026-01-15T16:21:40.401Z" }, - { url = "https://files.pythonhosted.org/packages/da/4f/25cfc2c4b9fa22c90038bdd966e8e3aea92826790e5350f537f7b3f84609/ruff-0.14.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5f59c587a413c9bd3259ec2d006853671f823ac4b4f974653bbcc84c180271de", size = 12356766, upload-time = "2026-01-15T16:21:42.853Z" }, - { url = "https://files.pythonhosted.org/packages/dd/5d/8da8aaca205ad94c87848e5f67b2cd014c022a76ea485ef9f1004ebc5118/ruff-0.14.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd31832569b06c75fdbf6f5f63a3458b7038d97f6e908b6e08fd02d00c20a3ae", size = 12771650, upload-time = "2026-01-15T16:22:00.387Z" }, - { url = "https://files.pythonhosted.org/packages/78/cc/75bc23144392ed21cd3413baf2476b9cd48eb056d87080530a1fe748108e/ruff-0.14.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cc1ba7b0d74d14b75c3a9d7e7ca01040bd126bd6bde2cffe3c31c1defe649ec6", size = 12820601, upload-time = "2026-01-15T16:21:55.76Z" }, - { url = "https://files.pythonhosted.org/packages/fb/1b/66d03158d2c477da458d9ea22feed4d40609d596dd6fa10bf158039ce8f8/ruff-0.14.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c1a90feecc2e44c01737d9d879ba6fc84b5626a7bd31e81991443198596ebe22", size = 13671644, upload-time = "2026-01-15T16:22:17.561Z" }, - { url = "https://files.pythonhosted.org/packages/22/df/0fa8920f4f1ca00d48a6f9b0b2310c426f4fcc943967af2d2723f7476d04/ruff-0.14.12-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b4c3478b459b71940f49d4dac1f91efa3852bb566368ad9f94dae2c257b6e63e", size = 15147221, upload-time = "2026-01-15T16:21:32.998Z" }, - { url = "https://files.pythonhosted.org/packages/54/9a/64365c7be12c51100f4159d15a8b818e4ffdb6aa592080932aa954a59cbd/ruff-0.14.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c14202522c2887e6644308827e72a6a0561fa965854e42ef14b325fa617c2634", size = 14709017, upload-time = "2026-01-15T16:22:02.69Z" }, - { url = "https://files.pythonhosted.org/packages/a7/58/2696c48a5ac88e9716a25048895f450772ce2f3e4cb37d67efc1e11da884/ruff-0.14.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2e15471064058c5f22ad17aec4cab3920f71bf6b2dd8e1c8cfb05e8e9ee6d9d6", size = 14133819, upload-time = "2026-01-15T16:21:58.031Z" }, - { url = "https://files.pythonhosted.org/packages/74/81/ce6aea2dcb40cba3d865f4fda5b5058ddc7786002d5f8413e6bc542ed665/ruff-0.14.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:697c2e00f00cb1027b91fc8930e276bed580b2976a3e4aca50eae2b3db291f92", size = 13849027, upload-time = "2026-01-15T16:21:50.801Z" }, - { url = "https://files.pythonhosted.org/packages/db/42/402928ed9a377f15abea9331bc77315f1c83ecffa6477251cb435df674bb/ruff-0.14.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0864023ac5b2c90af354372529926b4fce4980bff828b722bedf115a0677131b", size = 14030353, upload-time = "2026-01-15T16:22:04.835Z" }, - { url = "https://files.pythonhosted.org/packages/02/1a/bc4ee626929034fa1f0d97f03edf334bfdc947cc2609baed7529a4adb352/ruff-0.14.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6372d9e07c0e61342582b29ba825c0712930c08ee72312d1764abfe761de2806", size = 12666546, upload-time = "2026-01-15T16:22:07.392Z" }, - { url = "https://files.pythonhosted.org/packages/f2/3f/e00efbf5ecf9279b06fcc26821178c46d89b402facdf9b7974b2a76f1da0/ruff-0.14.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:c59c4941400a86c60219fd1457012e6da23a92590755ab4ed1ae25bbe4aab948", size = 12802512, upload-time = "2026-01-15T16:21:48.13Z" }, - { url = "https://files.pythonhosted.org/packages/6f/05/610b462fc211eb877457ee08062af50c56d924849038ec1e6005d6e50791/ruff-0.14.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:65ce54567c42e3a6d2548b49b8e4e87148eaf455ad76d7a20f83c902bc7cc5e4", size = 13205003, upload-time = "2026-01-15T16:21:37.904Z" }, - { url = "https://files.pythonhosted.org/packages/43/87/68fb2335cf969f3b5a7808265e6998cc735bce9a8efc57f6a3c1c167b3bb/ruff-0.14.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:28255d5a1828bc1da67c8c5a2cea5c83de6f0b6185eef46f4fc12957396fb169", size = 13925981, upload-time = "2026-01-15T16:21:45.453Z" }, - { url = "https://files.pythonhosted.org/packages/cc/a7/2f02a56b457c7cf568778fbbc2302ce6aa00bd20f9dd6149329a376a6964/ruff-0.14.12-py3-none-win32.whl", hash = "sha256:a111f79ba789257177fe5971eea061c8545af3c0cab3529fa00155f6621c68da", size = 12897262, upload-time = "2026-01-15T16:22:15Z" }, - { url = "https://files.pythonhosted.org/packages/8b/fd/86c2309a254b4e0982b3423504614d9f05b077961167a31bdb5b533be8a9/ruff-0.14.12-py3-none-win_amd64.whl", hash = "sha256:8da366e9942b26ac1fb0df43264a3550655160b9536cdb66a2b070a22b6e5d6a", size = 14105034, upload-time = "2026-01-15T16:22:10.139Z" }, - { url = "https://files.pythonhosted.org/packages/5c/0a/8f0d458479113587b1dfd3fabedfd3f1df4653801273993b1f4dc31a935f/ruff-0.14.12-py3-none-win_arm64.whl", hash = "sha256:f0672b72872dc204580d2d3f8bdc4e922c88a0f93e2a3fb799ca085797a5a7c4", size = 13064991, upload-time = "2026-01-15T16:21:35.436Z" }, +version = "0.14.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/06/f71e3a86b2df0dfa2d2f72195941cd09b44f87711cb7fa5193732cb9a5fc/ruff-0.14.14.tar.gz", hash = "sha256:2d0f819c9a90205f3a867dbbd0be083bee9912e170fd7d9704cc8ae45824896b", size = 4515732, upload-time = "2026-01-22T22:30:17.527Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/89/20a12e97bc6b9f9f68343952da08a8099c57237aef953a56b82711d55edd/ruff-0.14.14-py3-none-linux_armv6l.whl", hash = "sha256:7cfe36b56e8489dee8fbc777c61959f60ec0f1f11817e8f2415f429552846aed", size = 10467650, upload-time = "2026-01-22T22:30:08.578Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b1/c5de3fd2d5a831fcae21beda5e3589c0ba67eec8202e992388e4b17a6040/ruff-0.14.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6006a0082336e7920b9573ef8a7f52eec837add1265cc74e04ea8a4368cd704c", size = 10883245, upload-time = "2026-01-22T22:30:04.155Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7c/3c1db59a10e7490f8f6f8559d1db8636cbb13dccebf18686f4e3c9d7c772/ruff-0.14.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:026c1d25996818f0bf498636686199d9bd0d9d6341c9c2c3b62e2a0198b758de", size = 10231273, upload-time = "2026-01-22T22:30:34.642Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6e/5e0e0d9674be0f8581d1f5e0f0a04761203affce3232c1a1189d0e3b4dad/ruff-0.14.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f666445819d31210b71e0a6d1c01e24447a20b85458eea25a25fe8142210ae0e", size = 10585753, upload-time = "2026-01-22T22:30:31.781Z" }, + { url = "https://files.pythonhosted.org/packages/23/09/754ab09f46ff1884d422dc26d59ba18b4e5d355be147721bb2518aa2a014/ruff-0.14.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c0f18b922c6d2ff9a5e6c3ee16259adc513ca775bcf82c67ebab7cbd9da5bc8", size = 10286052, upload-time = "2026-01-22T22:30:24.827Z" }, + { url = "https://files.pythonhosted.org/packages/c8/cc/e71f88dd2a12afb5f50733851729d6b571a7c3a35bfdb16c3035132675a0/ruff-0.14.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1629e67489c2dea43e8658c3dba659edbfd87361624b4040d1df04c9740ae906", size = 11043637, upload-time = "2026-01-22T22:30:13.239Z" }, + { url = "https://files.pythonhosted.org/packages/67/b2/397245026352494497dac935d7f00f1468c03a23a0c5db6ad8fc49ca3fb2/ruff-0.14.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:27493a2131ea0f899057d49d303e4292b2cae2bb57253c1ed1f256fbcd1da480", size = 12194761, upload-time = "2026-01-22T22:30:22.542Z" }, + { url = "https://files.pythonhosted.org/packages/5b/06/06ef271459f778323112c51b7587ce85230785cd64e91772034ddb88f200/ruff-0.14.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ff589aab3f5b539e35db38425da31a57521efd1e4ad1ae08fc34dbe30bd7df", size = 12005701, upload-time = "2026-01-22T22:30:20.499Z" }, + { url = "https://files.pythonhosted.org/packages/41/d6/99364514541cf811ccc5ac44362f88df66373e9fec1b9d1c4cc830593fe7/ruff-0.14.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc12d74eef0f29f51775f5b755913eb523546b88e2d733e1d701fe65144e89b", size = 11282455, upload-time = "2026-01-22T22:29:59.679Z" }, + { url = "https://files.pythonhosted.org/packages/ca/71/37daa46f89475f8582b7762ecd2722492df26421714a33e72ccc9a84d7a5/ruff-0.14.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb8481604b7a9e75eff53772496201690ce2687067e038b3cc31aaf16aa0b974", size = 11215882, upload-time = "2026-01-22T22:29:57.032Z" }, + { url = "https://files.pythonhosted.org/packages/2c/10/a31f86169ec91c0705e618443ee74ede0bdd94da0a57b28e72db68b2dbac/ruff-0.14.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:14649acb1cf7b5d2d283ebd2f58d56b75836ed8c6f329664fa91cdea19e76e66", size = 11180549, upload-time = "2026-01-22T22:30:27.175Z" }, + { url = "https://files.pythonhosted.org/packages/fd/1e/c723f20536b5163adf79bdd10c5f093414293cdf567eed9bdb7b83940f3f/ruff-0.14.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8058d2145566510790eab4e2fad186002e288dec5e0d343a92fe7b0bc1b3e13", size = 10543416, upload-time = "2026-01-22T22:30:01.964Z" }, + { url = "https://files.pythonhosted.org/packages/3e/34/8a84cea7e42c2d94ba5bde1d7a4fae164d6318f13f933d92da6d7c2041ff/ruff-0.14.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e651e977a79e4c758eb807f0481d673a67ffe53cfa92209781dfa3a996cf8412", size = 10285491, upload-time = "2026-01-22T22:30:29.51Z" }, + { url = "https://files.pythonhosted.org/packages/55/ef/b7c5ea0be82518906c978e365e56a77f8de7678c8bb6651ccfbdc178c29f/ruff-0.14.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cc8b22da8d9d6fdd844a68ae937e2a0adf9b16514e9a97cc60355e2d4b219fc3", size = 10733525, upload-time = "2026-01-22T22:30:06.499Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5b/aaf1dfbcc53a2811f6cc0a1759de24e4b03e02ba8762daabd9b6bd8c59e3/ruff-0.14.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:16bc890fb4cc9781bb05beb5ab4cd51be9e7cb376bf1dd3580512b24eb3fda2b", size = 11315626, upload-time = "2026-01-22T22:30:36.848Z" }, + { url = "https://files.pythonhosted.org/packages/2c/aa/9f89c719c467dfaf8ad799b9bae0df494513fb21d31a6059cb5870e57e74/ruff-0.14.14-py3-none-win32.whl", hash = "sha256:b530c191970b143375b6a68e6f743800b2b786bbcf03a7965b06c4bf04568167", size = 10502442, upload-time = "2026-01-22T22:30:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/87/44/90fa543014c45560cae1fffc63ea059fb3575ee6e1cb654562197e5d16fb/ruff-0.14.14-py3-none-win_amd64.whl", hash = "sha256:3dde1435e6b6fe5b66506c1dff67a421d0b7f6488d466f651c07f4cab3bf20fd", size = 11630486, upload-time = "2026-01-22T22:30:10.852Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6a/40fee331a52339926a92e17ae748827270b288a35ef4a15c9c8f2ec54715/ruff-0.14.14-py3-none-win_arm64.whl", hash = "sha256:56e6981a98b13a32236a72a8da421d7839221fa308b223b9283312312e5ac76c", size = 10920448, upload-time = "2026-01-22T22:30:15.417Z" }, ] [[package]] @@ -1773,14 +1768,14 @@ wheels = [ [[package]] name = "scipy-stubs" -version = "1.17.0.1" +version = "1.17.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "optype", extra = ["numpy"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ae/a2/7f52edf1185ffcbf26cae1adede995f923c60a3a1f366bd1cb4cbae41817/scipy_stubs-1.17.0.1.tar.gz", hash = "sha256:029ef77b3984be53a914ac90af3b78c5543af7275eb126c8cec09e7bc72f623c", size = 372323, upload-time = "2026-01-14T16:34:52.838Z" } +sdist = { url = "https://files.pythonhosted.org/packages/40/fe/5fa7da49821ea94d60629ae71277fa8d7e16eb20602f720062b6c30a644c/scipy_stubs-1.17.0.2.tar.gz", hash = "sha256:3981bd7fa4c189a8493307afadaee1a830d9a0de8e3ae2f4603f192b6260ef2a", size = 379897, upload-time = "2026-01-22T19:17:08Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/df/7f/6b99d2f0b75738487e3127dc8fbfff04214cd29900118f5a1f945c34271f/scipy_stubs-1.17.0.1-py3-none-any.whl", hash = "sha256:235bdebce396a9bb48236525aedf04a6efa66dcca8b46105549f35f1f5c4cbb7", size = 577357, upload-time = "2026-01-14T16:34:50.907Z" }, + { url = "https://files.pythonhosted.org/packages/51/e3/20233497e4a27956e7392c3f7879e6ee7f767f268079f24f4b089b70f563/scipy_stubs-1.17.0.2-py3-none-any.whl", hash = "sha256:99d1aa75b7d72a7ee36a68d18bcf1149f62ab577bbd1236c65c471b3b465d824", size = 586137, upload-time = "2026-01-22T19:17:05.802Z" }, ] [[package]] @@ -1803,38 +1798,22 @@ wheels = [ [[package]] name = "soupsieve" -version = "2.8.1" +version = "2.8.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/89/23/adf3796d740536d63a6fbda113d07e60c734b6ed5d3058d1e47fc0495e47/soupsieve-2.8.1.tar.gz", hash = "sha256:4cf733bc50fa805f5df4b8ef4740fc0e0fa6218cf3006269afd3f9d6d80fd350", size = 117856, upload-time = "2025-12-18T13:50:34.655Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/48/f3/b67d6ea49ca9154453b6d70b34ea22f3996b9fa55da105a79d8732227adc/soupsieve-2.8.1-py3-none-any.whl", hash = "sha256:a11fe2a6f3d76ab3cf2de04eb339c1be5b506a8a47f2ceb6d139803177f85434", size = 36710, upload-time = "2025-12-18T13:50:33.267Z" }, + { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, ] [[package]] name = "sourcery" -version = "1.40.0" +version = "1.43.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'linux'", - "python_full_version < '3.12' and sys_platform == 'linux'", - "python_full_version >= '3.12' and sys_platform == 'win32'", - "python_full_version < '3.12' and sys_platform == 'win32'", -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/73/ef/42fa2226110cba8f0b656a0f30cac9e66fe0285fa249698bc1d9c5d6b8b4/sourcery-1.40.0-py2.py3-none-manylinux1_x86_64.whl", hash = "sha256:19116cc91aed5db2badca22cd2d745b8e2151bf22bac037d4b00aa2edd52217c", size = 124203805, upload-time = "2025-10-28T11:37:28.078Z" }, - { url = "https://files.pythonhosted.org/packages/e3/3d/b5bd0bc987472a7adb11b6c2fa8612ba3e445d41fed8b473b156af09d519/sourcery-1.40.0-py2.py3-none-win_amd64.whl", hash = "sha256:7a45fd88840cd167747519984213091912fe3782bc167af0c2de1c78d3d9672d", size = 101322491, upload-time = "2025-10-28T11:37:33.595Z" }, -] - -[[package]] -name = "sourcery" -version = "1.41.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform != 'linux' and sys_platform != 'win32'", - "python_full_version < '3.12' and sys_platform != 'linux' and sys_platform != 'win32'", -] wheels = [ - { url = "https://files.pythonhosted.org/packages/85/92/8150f339ca39a3bbca83cb70a49b22e7c2234ec8d8129bc6bfbe1a6aaf47/sourcery-1.41.1-py2.py3-none-macosx_10_9_universal2.whl", hash = "sha256:9ecb7636301e9dea8934f897151e504127274ea60c7709a65bed7457850f994c", size = 101735565, upload-time = "2025-10-30T14:04:17.397Z" }, + { url = "https://files.pythonhosted.org/packages/c8/7a/624a3539c5355de0ee9cec4cb9c6d2be6d36ab916c190b365c8462f0f0d5/sourcery-1.43.0-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:6279f505406b9b3455dac2f9646c0e110fdc72857479dc05b788a2cfd28dfa4f", size = 107493269, upload-time = "2026-01-19T15:47:07.456Z" }, + { url = "https://files.pythonhosted.org/packages/78/f9/058e1026479d2af655be608803d75d96297f4d0a2e98bb510488e384f415/sourcery-1.43.0-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:89034e4a800294093614c8c01317e0b051c35d2e7abf089e78f1830984516e04", size = 93553650, upload-time = "2026-01-19T15:48:08.801Z" }, + { url = "https://files.pythonhosted.org/packages/86/18/4075fe0f6b6a3692d80c9c490792d66f839ed91111afa7aea7869f6f1682/sourcery-1.43.0-py2.py3-none-manylinux1_x86_64.whl", hash = "sha256:a142fb1156dfc205752b546fd24cd6e6bd405ebd7aec1097c339ed0e05724dcd", size = 132906772, upload-time = "2026-01-19T15:49:27.487Z" }, + { url = "https://files.pythonhosted.org/packages/68/cf/8ac262bd2d1c5527a9d8c437f5bdbabc5dc68690110dbf34b20be090798a/sourcery-1.43.0-py2.py3-none-win_amd64.whl", hash = "sha256:2bd293af561261ffc0bccff82c9f764cd654247aaa29a0b46d4927e4711b0570", size = 91181924, upload-time = "2026-01-19T15:50:24.796Z" }, ] [[package]] @@ -1842,9 +1821,7 @@ name = "sphinx" version = "9.0.4" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.12' and sys_platform != 'linux' and sys_platform != 'win32'", - "python_full_version < '3.12' and sys_platform == 'linux'", - "python_full_version < '3.12' and sys_platform == 'win32'", + "python_full_version < '3.12'", ] dependencies = [ { name = "alabaster", marker = "python_full_version < '3.12'" }, @@ -1875,9 +1852,7 @@ name = "sphinx" version = "9.1.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.12' and sys_platform != 'linux' and sys_platform != 'win32'", - "python_full_version >= '3.12' and sys_platform == 'linux'", - "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12'", ] dependencies = [ { name = "alabaster", marker = "python_full_version >= '3.12'" }, @@ -1921,9 +1896,7 @@ name = "sphinx-autodoc-typehints" version = "3.6.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.12' and sys_platform != 'linux' and sys_platform != 'win32'", - "python_full_version < '3.12' and sys_platform == 'linux'", - "python_full_version < '3.12' and sys_platform == 'win32'", + "python_full_version < '3.12'", ] dependencies = [ { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, @@ -1938,9 +1911,7 @@ name = "sphinx-autodoc-typehints" version = "3.6.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.12' and sys_platform != 'linux' and sys_platform != 'win32'", - "python_full_version >= '3.12' and sys_platform == 'linux'", - "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12'", ] dependencies = [ { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },