diff --git a/.gitattributes b/.gitattributes index 69bfb10f..76019862 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1 @@ -tests/test_data_temp/elev_lid792_1m.gtiff filter=lfs diff=lfs merge=lfs -text +tests/test_data/tutorial_files/elev_lid792_1m.gtiff filter=lfs diff=lfs merge=lfs -text diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e941b4f6..30f6040f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -18,9 +18,11 @@ jobs: - name: Install dependencies run: | apt-get update - apt-get install -y ca-certificates + apt-get install -y ca-certificates git-lfs - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + lfs: true - name: Install uv and Python run: | @@ -36,7 +38,7 @@ jobs: $HOME/.local/bin/uv run --all-extras --python ${{ matrix.python-version }} pytest --cov=src --junitxml=junit/test-temporal-overwrite-results.xml --cov-report=xml --cov-report=html tests/grass/test_temporal_overwrite.py - name: Upload coverage - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: coverage-report-${{ matrix.python-version }} path: | diff --git a/RELEASE.rst b/RELEASE.rst index 5598bd27..20e8f959 100644 --- a/RELEASE.rst +++ b/RELEASE.rst @@ -7,6 +7,17 @@ These are the major changes made in each release. For more details please see the commit log of the git repository. +Itzï 26.8 +--------- + +*Unreleased* + +**Internals** + +- Rely on itzi-core for the computing part. + Itzï is the user-facing interface, including GRASS. + + Itzï 26.6 --------- diff --git a/pyproject.toml b/pyproject.toml index 9bdaefa7..8514e6ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,13 +1,13 @@ [project] name = "itzi" -version = "26.7" +version = "26.8" description = "A distributed dynamic flood model." authors = [ {name = "Laurent Courty", email = "lrntct@gmail.com"}, ] dependencies = [ "numpy>=2.5", - "itzi-core>=0.2.0", + "itzi-core>=0.4.0", "bmipy>=2.0.1", "pydantic>=2.10" ] @@ -40,7 +40,6 @@ dev = [ "pytest>=9.1", "pytest-cov==6.*", "pandas>=3.0.3", - "requests>=2.34", "pre-commit>=4.6", "ruff>=0.15", "ty>=0.0.55", diff --git a/src/itzi/bmi_itzi.py b/src/itzi/bmi_itzi.py index d01fee9e..4e3e973a 100644 --- a/src/itzi/bmi_itzi.py +++ b/src/itzi/bmi_itzi.py @@ -75,7 +75,11 @@ def initialize(self, filename=None) -> None: self.grass_session_manager = GrassSessionManager(grass_params) self.grass_session_manager.open() - self.itzi = SimulationRunner(sim_params, grass_params) + self.itzi = SimulationRunner( + sim_params, + grass_params, + stats_file=conf_data.get_stats_file(), + ) def update(self): """Advance model by one time step.""" diff --git a/src/itzi/configreader.py b/src/itzi/configreader.py index c214d098..c74200d6 100644 --- a/src/itzi/configreader.py +++ b/src/itzi/configreader.py @@ -392,6 +392,9 @@ def __init__(self, filename: str | None) -> None: self.raw_input_times = _read_time_values(params) self.input_map_names = _read_input_map_names(params) self.out_prefix, self.out_values, self.output_map_names = _read_output_config(params) + self.stats_file = ( + _read_optional_value(params, "statistics", "stats_file", params.get) or None + ) self.sim_times = SimulationTimes.from_raw_values(self.raw_input_times) self._check_general_input(self.input_map_names) infiltration_model = self._resolve_infiltration_model(self.input_map_names) @@ -416,10 +419,6 @@ def __init__(self, filename: str | None) -> None: if self.hotstart_config is not None: simulation_kwargs["hotstart_config"] = self.hotstart_config - stats_file = _read_optional_value(params, "statistics", "stats_file", params.get) - if stats_file is not None: - simulation_kwargs["stats_file"] = stats_file - simulation_kwargs.update(_read_simulation_option_values(params)) simulation_kwargs.update(_read_simulation_drainage_values(params)) if "swmm_inp" in simulation_kwargs: @@ -473,3 +472,7 @@ def get_sim_params(self) -> SimulationConfig: def get_grass_params(self) -> GrassParams: """Return validated GRASS GIS session parameters.""" return self.grass_params + + def get_stats_file(self) -> str | None: + """Return the CSV statistics output file name, if configured.""" + return self.stats_file diff --git a/src/itzi/itzi.py b/src/itzi/itzi.py index d1fd319f..6ebc87a5 100644 --- a/src/itzi/itzi.py +++ b/src/itzi/itzi.py @@ -36,6 +36,7 @@ from typing import TYPE_CHECKING, Callable import numpy as np +from itzi_core.providers.csv_mass_balance_output import CSVMassBalanceOutputProvider from itzi_core.simulation_builder import SimulationBuilder from itzi.configreader import ConfigReader @@ -76,7 +77,8 @@ def __init__( sim_config: SimulationConfig, grass_params: GrassParams, hotstart_path: str | None = None, - ): + stats_file: str | None = None, + ) -> None: self.grass_required_version = "8.4.0" self.g_interface: GrassInterface self.sim: Simulation @@ -149,6 +151,8 @@ def __init__( .with_raster_output_provider(raster_output_provider) .with_vector_output_provider(vector_output_provider) ) + if stats_file: + sim_builder.with_mass_balance_output_provider(CSVMassBalanceOutputProvider(stats_file)) if hotstart_path: sim_builder.with_hotstart(hotstart_path) self.sim: Simulation = sim_builder.build() @@ -212,7 +216,8 @@ def sim_runner_worker(conf_file: str, hotstart_file: str | None) -> None: sim_runner = SimulationRunner( sim_params, grass_params, - hotstart_file, + hotstart_path=hotstart_file, + stats_file=conf_data.get_stats_file(), ) sim_runner.run().finalize() except msgr.FatalError: diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index ccf1c4b2..02f24cc9 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -135,6 +135,60 @@ def fail(_): assert "WARNING: Simulation terminated with exit status 1" in itzi_stderr.getvalue() +def test_worker_passes_statistics_file_to_simulation_runner(monkeypatch): + sim_params = object() + grass_params = object() + runner_arguments = {} + + class FakeConfigReader: + def __init__(self, _): + pass + + def get_sim_params(self): + return sim_params + + def get_grass_params(self): + return grass_params + + def get_stats_file(self): + return "statistics.csv" + + class FakeGrassSessionManager: + def __init__(self, received_grass_params): + assert received_grass_params is grass_params + + def __enter__(self): + return self + + def __exit__(self, *_): + pass + + class FakeSimulationRunner: + def __init__(self, *args, **kwargs): + runner_arguments["args"] = args + runner_arguments["kwargs"] = kwargs + + def run(self): + return self + + def finalize(self): + return self + + monkeypatch.setattr("itzi.itzi.ConfigReader", FakeConfigReader) + monkeypatch.setattr("itzi.itzi.GrassSessionManager", FakeGrassSessionManager) + monkeypatch.setattr("itzi.itzi.SimulationRunner", FakeSimulationRunner) + + sim_runner_worker("a.ini", "hotstart.zip") + + assert runner_arguments == { + "args": (sim_params, grass_params), + "kwargs": { + "hotstart_path": "hotstart.zip", + "stats_file": "statistics.csv", + }, + } + + def test_run_one_reports_worker_signal(monkeypatch, itzi_stderr): class FailedProcess: exitcode = -11 diff --git a/tests/cli/test_configreader.py b/tests/cli/test_configreader.py index a409548f..02e864ae 100644 --- a/tests/cli/test_configreader.py +++ b/tests/cli/test_configreader.py @@ -64,7 +64,7 @@ def test_reader_uses_defaults_when_optional_sections_are_missing(tmp_path): assert sim_config.hotstart_config is None assert sim_config.surface_flow_parameters == SurfaceFlowParameters() - assert sim_config.stats_file is None + assert reader.get_stats_file() is None assert sim_config.dtinf == DefaultValues.DTINF assert sim_config.swmm_inp is None assert sim_config.drainage_output is None @@ -81,6 +81,30 @@ def test_reader_uses_defaults_when_optional_sections_are_missing(tmp_path): } +@pytest.mark.parametrize( + ("statistics", "expected_stats_file"), + [ + (None, None), + ({"stats_file": ""}, None), + ({"stats_file": "statistics.csv"}, "statistics.csv"), + ], +) +def test_reader_exposes_stats_file_separately_from_simulation_config( + tmp_path, + statistics, + expected_stats_file, +): + config_file = write_config_file( + tmp_path, + make_config_dict(statistics=statistics), + ) + + reader = ConfigReader(config_file) + + assert reader.get_stats_file() == expected_stats_file + assert "stats_file" not in type(reader.get_sim_params()).model_fields + + def test_reader_normalizes_deprecated_aliases(tmp_path, caplog): config_file = write_config_file( tmp_path, diff --git a/tests/conftest.py b/tests/conftest.py index deaf2575..e420903f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,3 @@ -import hashlib import os import pytest @@ -14,14 +13,6 @@ def roughness(timeseries): normed_f = (f - f.mean()) / f.std() return (normed_f.diff() ** 2).sum() - @staticmethod - def md5(file_path): - hash_md5 = hashlib.md5() - with open(file_path, "rb") as f: - for chunk in iter(lambda: f.read(4096), b""): - hash_md5.update(chunk) - return hash_md5.hexdigest() - @pytest.fixture(scope="session") def helpers(): diff --git a/tests/grass/conftest.py b/tests/grass/conftest.py index bd5f042c..1fc5512f 100644 --- a/tests/grass/conftest.py +++ b/tests/grass/conftest.py @@ -160,7 +160,11 @@ def grass_5by5_sim(grass_5by5, test_data_path): """ """ config_file = os.path.join(test_data_path, "5by5", "5by5.ini") conf_data = ConfigReader(config_file) - sim_runner = SimulationRunner(conf_data.get_sim_params(), conf_data.get_grass_params()) + sim_runner = SimulationRunner( + conf_data.get_sim_params(), + conf_data.get_grass_params(), + stats_file=conf_data.get_stats_file(), + ) assert isinstance(sim_runner, SimulationRunner) sim_runner.run().finalize() return sim_runner diff --git a/tests/grass/test_itzi.py b/tests/grass/test_itzi.py index bcdec638..2d20e874 100644 --- a/tests/grass/test_itzi.py +++ b/tests/grass/test_itzi.py @@ -91,7 +91,11 @@ def _build_timed_rain_runner( parser.write(file_handle) conf_data = ConfigReader(config_file) - return SimulationRunner(conf_data.get_sim_params(), conf_data.get_grass_params()) + return SimulationRunner( + conf_data.get_sim_params(), + conf_data.get_grass_params(), + stats_file=conf_data.get_stats_file(), + ) @pytest.mark.forked @@ -143,7 +147,11 @@ def test_region_mask(test_data_path): conf_data = ConfigReader(config_file) sim_params = conf_data.get_sim_params() grass_params = conf_data.get_grass_params() - sim_runner = SimulationRunner(sim_params, grass_params) + sim_runner = SimulationRunner( + sim_params, + grass_params, + stats_file=conf_data.get_stats_file(), + ) # Run simulation sim_runner.run().finalize() # Check temporary mask and region @@ -192,7 +200,11 @@ def test_fails_when_region_has_no_dem_data(test_data_temp_path): conf_data = ConfigReader(config_file) with pytest.raises(RuntimeError, match=r"input map contains only NULL/NaN cells"): - SimulationRunner(conf_data.get_sim_params(), conf_data.get_grass_params()) + SimulationRunner( + conf_data.get_sim_params(), + conf_data.get_grass_params(), + stats_file=conf_data.get_stats_file(), + ) @pytest.mark.forked diff --git a/tests/grass/test_temporal_overwrite.py b/tests/grass/test_temporal_overwrite.py index 3dd33077..603d9c4f 100644 --- a/tests/grass/test_temporal_overwrite.py +++ b/tests/grass/test_temporal_overwrite.py @@ -64,7 +64,11 @@ def _build_runner( parser.write(file_handle) conf_data = ConfigReader(config_file) - return SimulationRunner(conf_data.get_sim_params(), conf_data.get_grass_params()) + return SimulationRunner( + conf_data.get_sim_params(), + conf_data.get_grass_params(), + stats_file=conf_data.get_stats_file(), + ) @pytest.mark.forked diff --git a/tests/grass/test_tutorial.py b/tests/grass/test_tutorial.py index 42b38c4f..21fce00c 100644 --- a/tests/grass/test_tutorial.py +++ b/tests/grass/test_tutorial.py @@ -2,7 +2,6 @@ import os from io import StringIO -import requests import pathlib import tempfile from configparser import ConfigParser @@ -16,31 +15,13 @@ from itzi.configreader import ConfigReader from itzi_core.data_containers import DrainageNodeAttributes, DrainageLinkAttributes -DEM_URL = "https://zenodo.org/api/records/15009114/files/elev_lid792_1m.gtiff/content" -MD5_SUM = "224f73dfa37244722b879a5f653682c9" DATA_EPSG = "3358" @pytest.fixture(scope="class") -def tutorial_test_file(test_data_temp_path, helpers): - """Download the tutorial main file.""" - file_name = "elev_lid792_1m.gtiff" - file_path = os.path.join(test_data_temp_path, file_name) - # Check if the file exists and has the right hash - try: - assert helpers.md5(file_path) == MD5_SUM - except Exception: - # Download the file - print("downloading file from Zenodo...") - file_response = requests.get(DEM_URL, stream=True, timeout=5) - if file_response.status_code == 200: - with open(file_path, "wb") as data_file: - for chunk in file_response.iter_content(chunk_size=8192): - data_file.write(chunk) - print(f"File successfully downloaded to {file_path}") - else: - print(f"Failed to download file: Status code {file_response.status_code}") - return file_path +def tutorial_test_file(test_data_path): + """Return the tutorial DEM managed by Git LFS.""" + return pathlib.Path(test_data_path) / "tutorial_files" / "elev_lid792_1m.gtiff" @pytest.fixture(scope="class") @@ -121,7 +102,11 @@ def test_tutorial(itzi_tutorial, test_data_path, test_data_temp_path): config_file = os.path.join(test_data_path, "tutorial_files", "tutorial.ini") conf_data = ConfigReader(config_file) - sim_runner = SimulationRunner(conf_data.get_sim_params(), conf_data.get_grass_params()) + sim_runner = SimulationRunner( + conf_data.get_sim_params(), + conf_data.get_grass_params(), + stats_file=conf_data.get_stats_file(), + ) sim_runner.run().finalize() # Check the results h_max_univar = gscript.parse_command( @@ -175,7 +160,11 @@ def test_tutorial_drainage(itzi_tutorial, test_data_path, test_data_temp_path, h # Run the simulation conf_data = ConfigReader(config_file) - sim_runner = SimulationRunner(conf_data.get_sim_params(), conf_data.get_grass_params()) + sim_runner = SimulationRunner( + conf_data.get_sim_params(), + conf_data.get_grass_params(), + stats_file=conf_data.get_stats_file(), + ) sim_runner.run().finalize() # Test consistency of stats file diff --git a/tests/test_data_temp/elev_lid792_1m.gtiff b/tests/test_data/tutorial_files/elev_lid792_1m.gtiff similarity index 100% rename from tests/test_data_temp/elev_lid792_1m.gtiff rename to tests/test_data/tutorial_files/elev_lid792_1m.gtiff diff --git a/uv.lock b/uv.lock index 6d7ea451..85f27fbe 100644 --- a/uv.lock +++ b/uv.lock @@ -274,7 +274,7 @@ wheels = [ [[package]] name = "itzi" -version = "26.7" +version = "26.8" source = { editable = "." } dependencies = [ { name = "bmipy" }, @@ -290,7 +290,6 @@ dev = [ { name = "pytest" }, { name = "pytest-cov" }, { name = "pytest-forked" }, - { name = "requests" }, { name = "ruff" }, { name = "sphinx" }, { name = "sphinx-argparse" }, @@ -301,7 +300,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "bmipy", specifier = ">=2.0.1" }, - { name = "itzi-core", specifier = ">=0.2.0" }, + { name = "itzi-core", specifier = ">=0.4.0" }, { name = "numpy", specifier = ">=2.5" }, { name = "pydantic", specifier = ">=2.10" }, ] @@ -313,7 +312,6 @@ dev = [ { name = "pytest", specifier = ">=9.1" }, { name = "pytest-cov", specifier = "==6.*" }, { name = "pytest-forked", specifier = ">=1.6.0" }, - { name = "requests", specifier = ">=2.34" }, { name = "ruff", specifier = ">=0.15" }, { name = "sphinx", specifier = ">=9.1.0" }, { name = "sphinx-argparse", specifier = ">=0.5.2" }, @@ -323,7 +321,7 @@ dev = [ [[package]] name = "itzi-core" -version = "0.2.0" +version = "0.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "bmipy" }, @@ -331,22 +329,22 @@ dependencies = [ { name = "pydantic" }, { name = "pyswmm" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d1/c3/9285c13d33d385e3f98a82197667092a88e8e23ab15972ec469a3ac276b3/itzi_core-0.2.0.tar.gz", hash = "sha256:51278ec283ac595325aac749158b43025c31654587a69cbb6d109c81bc6843c9", size = 993423, upload-time = "2026-07-15T18:01:35.86Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/a6/46adb9c26cd9e3b6eae8c239562d13df2291995b2dcb402cb82725f82b46/itzi_core-0.2.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:e58a80a5abaa16e3435f0e6c3467887c6405ef2fe6392062b097ac1c300a0e96", size = 1402963, upload-time = "2026-07-15T18:01:10.823Z" }, - { url = "https://files.pythonhosted.org/packages/c6/3c/7c8d746d9da646f00b72a2f4b3860e982f759e13e8c46819146213e0fbe6/itzi_core-0.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08734cf1b5fc0818bbc9d6ad3fdc875da3b85886dea98b02a69da1dc05043cc4", size = 4330922, upload-time = "2026-07-15T18:01:12.773Z" }, - { url = "https://files.pythonhosted.org/packages/6b/af/460673cfa7bc90681ad718887904ee18200856664586a3a5431edd588d09/itzi_core-0.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f10d4b4a95011b3a6e4836c89bec703deea80120b90fbe51bf91eb2ff831e744", size = 4408159, upload-time = "2026-07-15T18:01:14.997Z" }, - { url = "https://files.pythonhosted.org/packages/8e/cd/c4e20b769c885522bd7301c1705f52cef0cf623e01ff82d12d84bc416dfe/itzi_core-0.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5043b8459b5211394fae0603ba17575ec264b511dbe1e4dab25f3cd48825eb63", size = 4239638, upload-time = "2026-07-15T18:01:16.685Z" }, - { url = "https://files.pythonhosted.org/packages/45/8d/bd3d98eefa8d5f2c0b2a0878de55666ce6c1a45a30902f33a7a60455843a/itzi_core-0.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2e1313ceaac94a672c946c735d343b6df0d57cb070bebc587cf3d2f98f8d673c", size = 4374629, upload-time = "2026-07-15T18:01:18.768Z" }, - { url = "https://files.pythonhosted.org/packages/9f/b5/3d46b0dd2338744dc8b506f3fe765f569aecd318d1832210611cb5545871/itzi_core-0.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:220c52d01ae817a0dd7677ce0bfa6950970dc2f3e0c47266ba698783c245e635", size = 1527831, upload-time = "2026-07-15T18:01:20.489Z" }, - { url = "https://files.pythonhosted.org/packages/9f/2c/60370bdd801f3181e4babc7d2a9b488075cbbec4b1f101f1f4fae5033dd2/itzi_core-0.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:c794fa3b26d625c7939c0a8bbcd6d5c4146ff7d82e3fe17f6395b46990f5d0de", size = 1517400, upload-time = "2026-07-15T18:01:22.176Z" }, - { url = "https://files.pythonhosted.org/packages/56/ad/26224a18b4e953f5ba919cb0af5611c62b1762071071ff5d737a7dc3f966/itzi_core-0.2.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:3417bc3182b3bc37e61fd6aae1c6de32149bf5e5edc1b05ebd8ecc02f038598d", size = 1399440, upload-time = "2026-07-15T18:01:23.681Z" }, - { url = "https://files.pythonhosted.org/packages/1c/85/8ec4eae4a507e55b6aa057805f3512c2e02d9fc9dfcae87ba856f97827c7/itzi_core-0.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1b9829077ee1896cc0d399cd3617955c9c80e7935ffc372f1f5c3fbe915dd43", size = 4302064, upload-time = "2026-07-15T18:01:25.463Z" }, - { url = "https://files.pythonhosted.org/packages/4b/bd/5d96539786f56e37428d7ab488a644d87742e86c7a39c69a3d34ebd8a00b/itzi_core-0.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1845b2794c3a0ca78ba2b36ae81eb239d6abb3e65601147f3ebbbfe5948c4e4a", size = 4367559, upload-time = "2026-07-15T18:01:27.143Z" }, - { url = "https://files.pythonhosted.org/packages/74/0b/58bb1bbd63dfe81d0a3bebd1ef5a9f9729e9de56302591d19e50d739a4af/itzi_core-0.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f5dccb7fe029efe2b76a2527f939349e7d962b142f7310544242647f51c67c4b", size = 4220332, upload-time = "2026-07-15T18:01:28.813Z" }, - { url = "https://files.pythonhosted.org/packages/7e/36/5c80f097971d73f05d52b67afb1cae714d06c3d2342dd472ec14afd6c310/itzi_core-0.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5de3de93e0b9f9b25f42aa3dd805e2b65db5cd0044fa87ac279d618d3eb10f64", size = 4344764, upload-time = "2026-07-15T18:01:30.57Z" }, - { url = "https://files.pythonhosted.org/packages/19/bd/1d02b737f4881536dfeb20349f7bbbf93ad1309ce04f8ca566d9c57c2a19/itzi_core-0.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:d1f0ac02545f7a3b8dccf3f59f6336c0b703b42961cfa839558ed7d5e39bc263", size = 1526615, upload-time = "2026-07-15T18:01:32.407Z" }, - { url = "https://files.pythonhosted.org/packages/38/17/5718aa10d47125ace659f03e27eeed152f41015cee066bf4005585701499/itzi_core-0.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:694bd0663260a7672e9a8f081423cce09800baac5ecb37514a0e7bdf33a56752", size = 1517069, upload-time = "2026-07-15T18:01:34.01Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/ba/08/bf938aaa63599ec678896d7c81751ce15d23f778a223eed6ff890a913c28/itzi_core-0.4.0.tar.gz", hash = "sha256:699032e4dbd7ba361684aea7a4342f7ed88d3bd3fddb9945dd3c0068642b6cba", size = 1004292, upload-time = "2026-07-28T18:07:56.267Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/ef/bb182f56f0c2673a76d0d87f70bc5f9e37cc23991269b2469f14ad4973b2/itzi_core-0.4.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:567b7acabb926f524021c04dba55c093ace6e1a7cda42826dab280577cd91a87", size = 1407120, upload-time = "2026-07-28T18:07:29.097Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f6/b8d85d51191e28ab257fa077d4e9e7405c72be8daeedaffcbcd7edd18b20/itzi_core-0.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec9af82010b7f590b030d29457461d3578163aebf1c51b6dd207d7c4f6fbcc4c", size = 4341698, upload-time = "2026-07-28T18:07:31.445Z" }, + { url = "https://files.pythonhosted.org/packages/36/b3/554b5b81dc087927a60e11b65e2c2d65d9a9ecd2fc7dcb8dc822d514702f/itzi_core-0.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9cf1716daa1adab96c57d172f595fffaff3597ae8e8e560ebd36a5868a3c0c37", size = 4415325, upload-time = "2026-07-28T18:07:33.418Z" }, + { url = "https://files.pythonhosted.org/packages/15/02/3f52fc58ea9ed8a7e4a553763a2a2caeff28ab1597e356fc0022721299f5/itzi_core-0.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:34e526b54a16a92817aad78f073bd739c6090d4b59a05fe310166cba6395eb1f", size = 4248548, upload-time = "2026-07-28T18:07:35.292Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b5/4684bbf3b7299615344d93898e9a4aa4679d29ab6a0fc6664e9f4dbab121/itzi_core-0.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1087da46d6594135ae3c633e5d680b31a8317679cd954081d51aafa82950aeb3", size = 4371516, upload-time = "2026-07-28T18:07:37.387Z" }, + { url = "https://files.pythonhosted.org/packages/c8/6b/e3a1f8d513f49119ea697277bf2b8426a8254dbbdd4052f262f21a734979/itzi_core-0.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:95e065f4b250bb365451f569ce45bf8c0f5ad93f7011fd0f7951e4e18b7b41ca", size = 1531839, upload-time = "2026-07-28T18:07:39.108Z" }, + { url = "https://files.pythonhosted.org/packages/c4/00/bb276f3d0de30e40edad43959a0d24859d216baa5d908c18c8a599ee8fa1/itzi_core-0.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:96b1931bb4274c63a5380b45c62202e24f56cb33d10e2e5ea280c4447a8107c4", size = 1521742, upload-time = "2026-07-28T18:07:40.785Z" }, + { url = "https://files.pythonhosted.org/packages/78/08/fbd9c937dfa8c67f7cfda11243f0cd758d54407bb684933d81a1f066329a/itzi_core-0.4.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:b782354aed73668eda1f55e23800fb2efbb8a4e264867db8b3a63572d8398919", size = 1403702, upload-time = "2026-07-28T18:07:42.365Z" }, + { url = "https://files.pythonhosted.org/packages/44/9f/f6d20914a0dfd07e0eceb01c09d436321204740e2a1ea2157f656017e276/itzi_core-0.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9607c1b889885961472b7d14ef347510cc717882e6fccb7e774ae8d033b812", size = 4311272, upload-time = "2026-07-28T18:07:44.188Z" }, + { url = "https://files.pythonhosted.org/packages/76/70/e148f3cd9ab6c74fe1320d2c2df4cab2c32d6e343991390ca19c5ffae46f/itzi_core-0.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:764478f8fbfad1b6cc0bebfb1c8c7e57b6dae22622eb8f89ef585544f695d0c5", size = 4372757, upload-time = "2026-07-28T18:07:46Z" }, + { url = "https://files.pythonhosted.org/packages/db/58/2733752005b27473d9a52d1b5d50df6c353d0a38218da4707119e25766a6/itzi_core-0.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:65bcc935157623b48092ccc5be79ded893e0208e559e7f0b19056a603915c92b", size = 4232850, upload-time = "2026-07-28T18:07:47.995Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e0/64316942804477d247a2caebe01b2fd82c82127b53d4b17acbebbc1aaf3b/itzi_core-0.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18c2ad7f36ce8ee023c5f915096594e59c32e34de6dc42b900fa5fc2d41ca90e", size = 4343685, upload-time = "2026-07-28T18:07:51.117Z" }, + { url = "https://files.pythonhosted.org/packages/15/79/2b6adf1dedfbae3dff42afde7e7d16fd596b8e403087ec62c9fc3952a352/itzi_core-0.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:f4bc45cfa7d2ec9a06a48403d330f6101c7a43dff9159e6d3b21af300ebbb0de", size = 1530629, upload-time = "2026-07-28T18:07:53.033Z" }, + { url = "https://files.pythonhosted.org/packages/24/1a/609e66a46985f4839a5c533e92108def6eeb387e7938fded4e0f0956cdb0/itzi_core-0.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:cab8efd01099398dcf191e813ae4a421161115bc04600474a6cdc778f31cdee9", size = 1521294, upload-time = "2026-07-28T18:07:54.708Z" }, ] [[package]]