From 4f6febc396284f645e4cf2c8088f9dcf96da4078 Mon Sep 17 00:00:00 2001 From: Christophe Prud'homme Date: Sun, 25 Jan 2026 13:34:12 +0100 Subject: [PATCH 1/8] Refactor code structure for improved readability and maintainability fixes #14 --- README.md | 272 +++++- pyproject.toml | 18 +- src/python/feelpp/mo2fmu/__init__.py | 79 +- .../feelpp/mo2fmu/compilers/__init__.py | 29 + src/python/feelpp/mo2fmu/compilers/base.py | 220 +++++ src/python/feelpp/mo2fmu/compilers/dymola.py | 417 +++++++++ .../feelpp/mo2fmu/compilers/openmodelica.py | 616 ++++++++++++++ src/python/feelpp/mo2fmu/mo2fmu.py | 789 +++++++++++++----- tests/conftest.py | 74 ++ tests/fixtures/models/bouncing_ball.mo | 33 + tests/fixtures/models/multi_state.mo | 10 + tests/fixtures/models/ode_sinusoidal.mo | 7 + tests/fixtures/models/ode_with_input.mo | 8 + tests/fixtures/models/simple_ode.mo | 7 + tests/test_compilers.py | 650 +++++++++++++++ tests/test_fmu_simulation.py | 691 +++++++++++++++ tests/test_mo2fmu.py | 209 +++-- uv.lock | 608 +++++++++++++- 18 files changed, 4394 insertions(+), 343 deletions(-) create mode 100644 src/python/feelpp/mo2fmu/compilers/__init__.py create mode 100644 src/python/feelpp/mo2fmu/compilers/base.py create mode 100644 src/python/feelpp/mo2fmu/compilers/dymola.py create mode 100644 src/python/feelpp/mo2fmu/compilers/openmodelica.py create mode 100644 tests/conftest.py create mode 100644 tests/fixtures/models/bouncing_ball.mo create mode 100644 tests/fixtures/models/multi_state.mo create mode 100644 tests/fixtures/models/ode_sinusoidal.mo create mode 100644 tests/fixtures/models/ode_with_input.mo create mode 100644 tests/fixtures/models/simple_ode.mo create mode 100644 tests/test_compilers.py create mode 100644 tests/test_fmu_simulation.py diff --git a/README.md b/README.md index b328bf2..8c2aa16 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,45 @@ # Feel++ mo2fmu converter -Modelica to FMU converter based on dymola +Modelica to FMU converter with Dymola and OpenModelica support. + +## Features + +- **Multiple backends**: Supports both Dymola and OpenModelica compilers +- **Automatic backend selection**: Automatically detects available compilers +- **FMI 1.0, 2.0, and 3.0 support**: Generate Co-Simulation and Model Exchange FMUs +- **Python API**: Programmatic access for integration into workflows +- **Command-line interface**: Simple CLI with `compile` and `check` subcommands ## Installation ### From PyPI (Recommended) -Install the latest stable release from PyPI: +Install the latest stable release from PyPI using [uv](https://docs.astral.sh/uv/): ```console -pip install feelpp-mo2fmu +uv pip install feelpp-mo2fmu ``` -Or using uv (faster): +### With OpenModelica Support + +To use OpenModelica as a backend, install with the optional dependency: ```console -uv pip install feelpp-mo2fmu +uv pip install "feelpp-mo2fmu[openmodelica]" +``` + +This will install [OMPython](https://github.com/OpenModelica/OMPython) for Python integration with OpenModelica. + +### With Simulation Support + +To run simulations and validate FMUs (useful for testing): + +```console +uv pip install "feelpp-mo2fmu[simulation]" ``` +This will install [FMPy](https://github.com/CATIA-Systems/FMPy) for FMU simulation and validation. + ### From Source For development or to use the latest unreleased features: @@ -42,44 +64,204 @@ export DYMOLA_EXECUTABLE=/usr/local/bin/dymola export DYMOLA_WHL=Modelica/Library/python_interface/dymola-2025.1-py3-none-any.whl ``` -These environment variables are used by: -- Tests (avoiding hardcoded paths) -- CI/CD workflows -- Command-line interface defaults - **Environment Variables:** - `DYMOLA_ROOT`: Path to Dymola installation root directory (default: `/opt/dymola-2025xRefresh1-x86_64/`) - `DYMOLA_EXECUTABLE`: Path to Dymola executable binary (default: `/usr/local/bin/dymola`) - `DYMOLA_WHL`: Relative path to Dymola Python wheel from DYMOLA_ROOT (default: `Modelica/Library/python_interface/dymola-2025.1-py3-none-any.whl`) -## Usage in command line +### OpenModelica Location + +OpenModelica can be configured via environment variables: + +```bash +export OPENMODELICA_HOME=/usr/lib/omc +``` + +**Environment Variables:** +- `OPENMODELICA_HOME`: Path to OpenModelica installation (default: auto-detected) + +## Command Line Interface + +The mo2fmu CLI provides two subcommands: `compile` and `check`. + +### Main Help ```console $ mo2fmu --help -Usage: mo2fmu [OPTIONS] MO OUTDIR +Usage: mo2fmu [OPTIONS] COMMAND [ARGS]... + + mo2fmu - Convert Modelica models to Functional Mock-up Units (FMUs). + + Use 'mo2fmu compile' to generate FMUs or 'mo2fmu check' to verify compilers. + +Options: + -v, --version Show version information. + --help Show this message and exit. + +Commands: + check Check availability of Modelica compilers and their FMI support. + compile Compile a Modelica model to FMU. +``` + +### Compile Command + +Generate FMUs from Modelica models: + +```console +$ mo2fmu compile --help +Usage: mo2fmu compile [OPTIONS] MO OUTDIR + + Compile a Modelica model to FMU. + +Options: + --name TEXT Custom name for the FMU (default: .mo file stem). + -l, --load TEXT Load one or more Modelica packages. + --flags TEXT Compiler-specific flags for FMU translation. + -t, --type [all|cs|me|csSolver] FMI type: cs (Co-Simulation), me (Model Exchange), + all, or csSolver. + --fmi-version [1|2|3] FMI version. FMI 3.0 requires Dymola 2024+ + or OpenModelica 1.21+. + -b, --backend [dymola|openmodelica|auto] + Modelica compiler backend (default: auto-detect). + --dymola PATH Path to Dymola root directory. + --dymola-exec PATH Path to Dymola executable. + --dymola-whl PATH Path to Dymola wheel file (relative to Dymola root). + -v, --verbose Enable verbose output. + -f, --force Overwrite existing FMU. + --help Show this message and exit. +``` + +**Compile Examples:** + +```console +# Basic compilation (auto-detect backend) +mo2fmu compile model.mo ./output + +# Compile with OpenModelica backend +mo2fmu compile --backend openmodelica model.mo ./output + +# Compile FMI 3.0 Co-Simulation FMU with verbose output +mo2fmu compile -v --fmi-version 3 --type cs model.mo ./output + +# Force overwrite existing FMU +mo2fmu compile -f model.mo ./output + +# Load additional Modelica packages +mo2fmu compile --load package1.mo --load package2.mo model.mo ./output +``` + +### Check Command + +Verify compiler availability and FMI support: + +```console +$ mo2fmu check --help +Usage: mo2fmu check [OPTIONS] + + Check availability of Modelica compilers and their FMI support. Options: - --fmumodelname TEXT change the model name of the FMU (default: .mo - file stem) - --load TEXT load one or more Modelica packages. - --flags TEXT one or more Dymola flags for FMU translation. - --type [all|cs|me|csSolver] the FMI type: cs, me, all, or csSolver. - --version TEXT the FMI version. - --dymola PATH path to Dymola root. - --dymolapath PATH path to Dymola executable. - --dymolawhl PATH path to Dymola whl file, relative to Dymola - root. - -v, --verbose verbose mode. - -f, --force force FMU generation even if file exists. - --help Show this message and exit.---- + --dymola PATH Path to Dymola root directory. + --dymola-exec PATH Path to Dymola executable. + --dymola-whl PATH Path to Dymola wheel file (relative to Dymola root). + --json Output results as JSON. + --help Show this message and exit. ``` -## Usage in Python +**Check Examples:** -Here is an example of how to use the `mo2fmu` function in Python that would convert a Modelica file to an FMU: +```console +# Check all available compilers +mo2fmu check + +# Output as JSON (for scripting) +mo2fmu check --json + +# Check with custom Dymola path +mo2fmu check --dymola /opt/dymola-2024x +``` + +## Python API + +### Recommended API + +The recommended API provides a clean interface with automatic backend selection: + +```python +from feelpp.mo2fmu import compileFmu, getCompiler, checkCompilers + +# Auto-detect and use available compiler +result = compileFmu("path/to/model.mo", "./output") +if result.success: + print(f"FMU created at {result.fmu_path}") +else: + print(f"Error: {result.error_message}") + +# Explicitly use OpenModelica +result = compileFmu("path/to/model.mo", "./output", backend="openmodelica") + +# Compile FMI 3.0 Model Exchange FMU +result = compileFmu( + "path/to/model.mo", + "./output", + fmiType="me", + fmiVersion="3", + verbose=True, +) + +# Check available compilers +available = checkCompilers() +for name, info in available.items(): + print(f"{name}: available={info['available']}, versions={info.get('fmi_versions', [])}") + +# Get a specific compiler instance for more control +compiler = getCompiler("openmodelica") +if compiler.is_available: + print(f"Using {compiler.name}") +``` + +### Using Compiler Classes Directly + +For full control, use the compiler classes directly: + +```python +from feelpp.mo2fmu import ( + DymolaCompiler, + OpenModelicaCompiler, + ModelicaModel, + CompilationConfig, +) +from pathlib import Path + +# Using OpenModelica +compiler = OpenModelicaCompiler() +if compiler.is_available: + model = ModelicaModel(Path("path/to/model.mo")) + config = CompilationConfig( + fmi_type="cs", # Co-Simulation + fmi_version="3", # FMI 3.0 + verbose=True, + ) + result = compiler.compile(model, Path("./output"), config) + + if result.success: + print(f"FMU created: {result.fmu_path}") + else: + print(f"Compilation failed: {result.error_message}") + +# Using Dymola +compiler = DymolaCompiler() +if compiler.is_available: + result = compiler.compile(model, Path("./output"), config) +``` + +### Legacy API + +The original API is still available for backward compatibility: ```python from feelpp.mo2fmu import mo2fmu + mo2fmu( mo_file="path/to/model.mo", outdir="path/to/output/dir", @@ -92,10 +274,44 @@ mo2fmu( dymola_executable="/path/to/dymola/executable", dymola_whl="/path/to/dymola.whl", verbose=True, - force=False + force=False, + backend="dymola", # or "openmodelica", "auto" ) ``` +## Backend Comparison + +| Feature | Dymola | OpenModelica | +|---------|--------|--------------| +| License | Commercial | Open Source (GPL) | +| FMI 1.0 | ✓ | ✓ | +| FMI 2.0 | ✓ | ✓ | +| FMI 3.0 | ✓ (2024+) | ✓ (v1.21+) | +| Co-Simulation | ✓ | ✓ | +| Model Exchange | ✓ | ✓ | +| csSolver type | ✓ | ✗ | +| Modelica Standard Library | ✓ | ✓ | +| BuildingSystems | ✓ | Partial | +| Buildings Library | ✓ | ✓ | + +## Running Tests + +The test suite includes unit tests and FMU simulation tests: + +```console +# Run all tests +uv run pytest + +# Run with verbose output +uv run pytest -v + +# Run only unit tests (no simulation) +uv run pytest tests/test_compilers.py tests/test_mo2fmu.py + +# Run simulation tests (requires FMPy) +uv run pytest tests/test_fmu_simulation.py +``` + ## Continuous Integration Our GitHub Actions workflow (`.github/workflows/ci.yml`) includes: diff --git a/pyproject.toml b/pyproject.toml index ff9ddca..58ab82c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta" [project] name = "feelpp-mo2fmu" -version = "0.6.0" -description = "Feel++ modelica to fmu converter package" +version = "1.0.0" +description = "Feel++ modelica to fmu converter package with Dymola and OpenModelica support" readme = "README.md" license = { text = "MIT" } authors = [ @@ -13,10 +13,10 @@ authors = [ { name = "Christophe Prud'homme", email = "christophe.prudhomme@cemosis.fr" }, { name = "Philippe Pinçon", email = "philippe.pincon@cemosis.fr" } ] -keywords = ["modelica", "fmu", "fmi", "dymola", "simulation"] +keywords = ["modelica", "fmu", "fmi", "dymola", "openmodelica", "simulation"] requires-python = ">=3.8.1" classifiers = [ - "Development Status :: 4 - Beta", + "Development Status :: 5 - Production/Stable", "Intended Audience :: Science/Research", "Intended Audience :: Developers", "Programming Language :: Python :: 3", @@ -47,6 +47,14 @@ Documentation = "https://feelpp.github.io/mo2fmu/" # Optional dependencies for testing and development. [project.optional-dependencies] +# OpenModelica backend support (alternative to Dymola) +openmodelica = [ + "OMPython>=3.4.0", +] +# FMU simulation support (for testing Model Exchange with CVODE solver) +simulation = [ + "fmpy>=0.3.0", +] test = [ "pytest>=7.0", "pytest-cov>=4.0", @@ -66,7 +74,7 @@ lint = [ "isort>=5.12", ] all = [ - "feelpp-mo2fmu[test,dev,lint]", + "feelpp-mo2fmu[openmodelica,simulation,test,dev,lint]", ] [project.scripts] diff --git a/src/python/feelpp/mo2fmu/__init__.py b/src/python/feelpp/mo2fmu/__init__.py index e7e1597..d2c8db0 100644 --- a/src/python/feelpp/mo2fmu/__init__.py +++ b/src/python/feelpp/mo2fmu/__init__.py @@ -1,8 +1,79 @@ -"""Feel++ Modelica to FMU converter package.""" +"""Feel++ Modelica to FMU converter package. + +This package provides tools for converting Modelica models to +Functional Mock-up Units (FMUs) using either Dymola or OpenModelica. + +Example: + Using the primary API:: + + from feelpp.mo2fmu import compileFmu, CompilationResult + + result = compileFmu("model.mo", "./output", backend="auto") + if result.success: + print(f"FMU created at {result.fmu_path}") + + Checking compiler availability:: + + from feelpp.mo2fmu import checkCompilers + + results = checkCompilers() + if results["dymola"]["available"]: + print(f"Dymola {results['dymola']['version']} available") + + Using specific compilers:: + + from feelpp.mo2fmu.compilers import DymolaCompiler, OpenModelicaCompiler + + compiler = OpenModelicaCompiler() + if compiler.is_available: + result = compiler.compile(model, output_dir, config) +""" from __future__ import annotations -__version__ = "0.6.0" -__all__ = ["mo2fmu", "mo2fmuCLI"] +from importlib.metadata import version as _get_version + +# Single source of truth: version comes from pyproject.toml +__version__ = _get_version("feelpp-mo2fmu") +__all__ = [ + # Primary API (camelCase) + "compileFmu", + "checkCompilers", + "getCompiler", + # CLI + "mo2fmuCLI", + # Compiler classes + "DymolaCompiler", + "DymolaConfig", + "OpenModelicaCompiler", + "OpenModelicaConfig", + # Data classes + "CompilationConfig", + "CompilationResult", + "ModelicaModel", + "FMUCompiler", + # Legacy API (deprecated, for backward compatibility) + "mo2fmu", + "mo2fmu_new", + "get_compiler", +] -from feelpp.mo2fmu.mo2fmu import mo2fmu, mo2fmuCLI +from feelpp.mo2fmu.compilers.base import ( + CompilationConfig, + CompilationResult, + FMUCompiler, + ModelicaModel, +) +from feelpp.mo2fmu.compilers.dymola import DymolaCompiler, DymolaConfig +from feelpp.mo2fmu.compilers.openmodelica import OpenModelicaCompiler, OpenModelicaConfig +from feelpp.mo2fmu.mo2fmu import ( + # Primary API + checkCompilers, + compileFmu, + getCompiler, + mo2fmuCLI, + # Legacy API (deprecated) + get_compiler, + mo2fmu, + mo2fmu_new, +) diff --git a/src/python/feelpp/mo2fmu/compilers/__init__.py b/src/python/feelpp/mo2fmu/compilers/__init__.py new file mode 100644 index 0000000..8fc9b49 --- /dev/null +++ b/src/python/feelpp/mo2fmu/compilers/__init__.py @@ -0,0 +1,29 @@ +"""Compiler backends for mo2fmu. + +This module provides abstract base classes and implementations for +different Modelica-to-FMU compilers. + +Available compilers: +- DymolaCompiler: Uses Dymola (commercial) for FMU generation +- OpenModelicaCompiler: Uses OpenModelica (open source) for FMU generation +""" + +from __future__ import annotations + +from feelpp.mo2fmu.compilers.base import ( + CompilationConfig, + CompilationResult, + FMUCompiler, + ModelicaModel, +) +from feelpp.mo2fmu.compilers.dymola import DymolaCompiler +from feelpp.mo2fmu.compilers.openmodelica import OpenModelicaCompiler + +__all__ = [ + "FMUCompiler", + "CompilationConfig", + "CompilationResult", + "ModelicaModel", + "DymolaCompiler", + "OpenModelicaCompiler", +] diff --git a/src/python/feelpp/mo2fmu/compilers/base.py b/src/python/feelpp/mo2fmu/compilers/base.py new file mode 100644 index 0000000..b4d01f0 --- /dev/null +++ b/src/python/feelpp/mo2fmu/compilers/base.py @@ -0,0 +1,220 @@ +"""Base classes for FMU compiler backends. + +This module defines the abstract interface and data structures for +Modelica-to-FMU compilation backends. +""" + +from __future__ import annotations + +import re +from abc import ABC, abstractmethod +from dataclasses import dataclass, field +from enum import Enum +from pathlib import Path +from typing import Optional + + +class FMIType(Enum): + """FMI export type.""" + + MODEL_EXCHANGE = "me" + CO_SIMULATION = "cs" + BOTH = "all" + CO_SIMULATION_SOLVER = "csSolver" + + @classmethod + def from_string(cls, value: str) -> "FMIType": + """Convert string to FMIType.""" + mapping = { + "me": cls.MODEL_EXCHANGE, + "cs": cls.CO_SIMULATION, + "all": cls.BOTH, + "csSolver": cls.CO_SIMULATION_SOLVER, + } + if value not in mapping: + raise ValueError(f"Invalid FMI type: {value}. Valid options: {list(mapping.keys())}") + return mapping[value] + + +class FMIVersion(Enum): + """FMI specification version.""" + + FMI_1_0 = "1" + FMI_2_0 = "2" + FMI_3_0 = "3" + + @classmethod + def from_string(cls, value: str) -> "FMIVersion": + """Convert string to FMIVersion.""" + mapping = {"1": cls.FMI_1_0, "2": cls.FMI_2_0, "3": cls.FMI_3_0} + if value not in mapping: + raise ValueError( + f"Invalid FMI version: {value}. Valid options: {list(mapping.keys())}" + ) + return mapping[value] + + +@dataclass +class ModelicaModel: + """Representation of a Modelica model to compile. + + Attributes: + path: Path to the .mo file + package_name: The package name extracted from 'within' statement + model_name: The model class name (usually the file stem) + fully_qualified_name: Full model name (package.model) + """ + + path: Path + package_name: Optional[str] = None + model_name: Optional[str] = None + fully_qualified_name: Optional[str] = None + + def __post_init__(self) -> None: + """Initialize derived attributes from the .mo file.""" + self.path = Path(self.path) + + if self.model_name is None: + self.model_name = self.path.stem + + if self.package_name is None: + self.package_name = self._extract_package_name() + + if self.fully_qualified_name is None: + if self.package_name: + self.fully_qualified_name = f"{self.package_name}.{self.model_name}" + else: + self.fully_qualified_name = self.model_name + + def _extract_package_name(self) -> Optional[str]: + """Extract package name from 'within' statement in .mo file.""" + try: + with open(self.path) as f: + content = f.read() + + # Match 'within PackageName;' or 'within Package.SubPackage;' + match = re.search(r"within\s+([\w.]+)\s*;", content) + if match: + return match.group(1) + except (OSError, IOError): + pass + return None + + +@dataclass +class CompilationConfig: + """Configuration for FMU compilation. + + Attributes: + fmi_type: Type of FMU export (cs, me, all, csSolver) + fmi_version: FMI specification version + output_name: Custom name for the output FMU (defaults to model name) + packages: List of additional Modelica packages to load + flags: Backend-specific compilation flags + force: Overwrite existing FMU if present + verbose: Enable verbose logging + optimize: Enable compiler optimizations + include_sources: Include source code in FMU + """ + + fmi_type: FMIType = FMIType.BOTH + fmi_version: FMIVersion = FMIVersion.FMI_2_0 + output_name: Optional[str] = None + packages: list[str] = field(default_factory=list) + flags: list[str] = field(default_factory=list) + force: bool = False + verbose: bool = False + optimize: bool = True + include_sources: bool = False + + @classmethod + def from_legacy( + cls, + type: str = "all", + version: str = "2", + fmumodelname: Optional[str] = None, + load: Optional[tuple[str, ...]] = None, + flags: Optional[tuple[str, ...]] = None, + force: bool = False, + verbose: bool = False, + ) -> "CompilationConfig": + """Create config from legacy mo2fmu parameters.""" + return cls( + fmi_type=FMIType.from_string(type), + fmi_version=FMIVersion.from_string(version), + output_name=fmumodelname, + packages=list(load) if load else [], + flags=list(flags) if flags else [], + force=force, + verbose=verbose, + ) + + +@dataclass +class CompilationResult: + """Result of FMU compilation. + + Attributes: + success: Whether compilation succeeded + fmu_path: Path to the generated FMU (if successful) + error_message: Error description (if failed) + log: Full compilation log + warnings: List of warning messages + """ + + success: bool + fmu_path: Optional[Path] = None + error_message: Optional[str] = None + log: Optional[str] = None + warnings: list[str] = field(default_factory=list) + + +class FMUCompiler(ABC): + """Abstract base class for Modelica-to-FMU compilers. + + Subclasses implement specific compiler backends (Dymola, OpenModelica, etc.). + """ + + @property + @abstractmethod + def name(self) -> str: + """Return the compiler backend name.""" + + @property + @abstractmethod + def is_available(self) -> bool: + """Check if the compiler is available on this system.""" + + @abstractmethod + def compile( + self, + model: ModelicaModel, + output_dir: Path, + config: CompilationConfig, + ) -> CompilationResult: + """Compile a Modelica model to FMU. + + Args: + model: The Modelica model to compile + output_dir: Directory to place the generated FMU + config: Compilation configuration options + + Returns: + CompilationResult with success status and FMU path or error info + """ + + @abstractmethod + def check_model(self, model: ModelicaModel, packages: Optional[list[str]] = None) -> bool: + """Validate a Modelica model without generating FMU. + + Args: + model: The Modelica model to check + packages: Additional packages to load + + Returns: + True if the model is valid, False otherwise + """ + + def get_version(self) -> Optional[str]: + """Return the compiler version string, if available.""" + return None diff --git a/src/python/feelpp/mo2fmu/compilers/dymola.py b/src/python/feelpp/mo2fmu/compilers/dymola.py new file mode 100644 index 0000000..b22ea19 --- /dev/null +++ b/src/python/feelpp/mo2fmu/compilers/dymola.py @@ -0,0 +1,417 @@ +"""Dymola compiler backend for mo2fmu. + +This module implements FMU generation using Dymola (commercial Modelica tool). +""" + +from __future__ import annotations + +import os +import platform +import shutil +import sys +import uuid +from dataclasses import dataclass, field +from pathlib import Path +from typing import TYPE_CHECKING, Any, Optional + +import spdlog as spd + +from feelpp.mo2fmu.compilers.base import ( + CompilationConfig, + CompilationResult, + FMIType, + FMUCompiler, + ModelicaModel, +) + +if TYPE_CHECKING: + pass + + +@dataclass +class DymolaConfig: + """Dymola-specific configuration. + + Attributes: + root: Path to Dymola installation directory + executable: Path to Dymola executable + wheel_path: Path to Dymola Python wheel (relative to root) + compile_64bit_only: Force 64-bit compilation only + enable_code_export: Enable code export (license-free FMU execution) + global_optimizations: Optimization level (0-2) + linger_time: License release delay in seconds (0 = immediate) + """ + + root: str = "/opt/dymola-2025xRefresh1-x86_64/" + executable: str = "/usr/local/bin/dymola" + wheel_path: str = "Modelica/Library/python_interface/dymola-2025.1-py3-none-any.whl" + compile_64bit_only: bool = True + enable_code_export: bool = True + global_optimizations: int = 2 + linger_time: int = 0 + additional_commands: list[str] = field(default_factory=list) + + @classmethod + def from_env(cls) -> "DymolaConfig": + """Create configuration from environment variables.""" + return cls( + root=os.getenv("DYMOLA_ROOT", cls.root), + executable=os.getenv("DYMOLA_EXECUTABLE", cls.executable), + wheel_path=os.getenv("DYMOLA_WHL", cls.wheel_path), + ) + + +class DymolaCompiler(FMUCompiler): + """Dymola-based FMU compiler. + + Uses Dymola's Python interface to compile Modelica models to FMUs. + Requires a valid Dymola license for compilation. + + Example: + >>> config = DymolaConfig.from_env() + >>> compiler = DymolaCompiler(config) + >>> if compiler.is_available: + ... model = ModelicaModel(Path("model.mo")) + ... result = compiler.compile(model, Path("output"), CompilationConfig()) + ... if result.success: + ... print(f"FMU created at {result.fmu_path}") + """ + + def __init__(self, config: Optional[DymolaConfig] = None) -> None: + """Initialize Dymola compiler. + + Args: + config: Dymola configuration. If None, uses environment variables. + """ + self._config = config or DymolaConfig.from_env() + self._dymola_interface: Optional[Any] = None + self._dymola_exception: Optional[type] = None + self._vdisplay: Optional[Any] = None + self._logger: Optional[Any] = None + self._interface_loaded = False + + # Try to load Dymola interface + self._load_interface() + + def _load_interface(self) -> bool: + """Attempt to load the Dymola Python interface.""" + if self._interface_loaded: + return True + + wheel_full_path = Path(self._config.root) / self._config.wheel_path + if not wheel_full_path.is_file(): + return False + + try: + sys.path.append(str(wheel_full_path)) + from dymola.dymola_exception import DymolaException + from dymola.dymola_interface import DymolaInterface + + self._dymola_interface = DymolaInterface + self._dymola_exception = DymolaException + self._interface_loaded = True + return True + except ImportError: + return False + + @property + def name(self) -> str: + """Return the compiler backend name.""" + return "dymola" + + @property + def is_available(self) -> bool: + """Check if Dymola is available on this system.""" + wheel_path = Path(self._config.root) / self._config.wheel_path + return wheel_path.is_file() and self._load_interface() + + @property + def config(self) -> DymolaConfig: + """Return the Dymola configuration.""" + return self._config + + def get_version(self) -> Optional[str]: + """Return the Dymola version string.""" + # Version is embedded in wheel path typically + # e.g., "dymola-2025.1-py3-none-any.whl" + wheel_name = Path(self._config.wheel_path).name + if wheel_name.startswith("dymola-"): + parts = wheel_name.split("-") + if len(parts) >= 2: + return parts[1] + return None + + def _create_logger(self) -> Any: + """Create a unique logger instance.""" + logger_name = f"dymola_{uuid.uuid4().hex[:8]}" + return spd.ConsoleLogger(logger_name, False, True, True) + + def _start_display(self) -> None: + """Start virtual framebuffer for headless operation.""" + if platform.system() != "Windows": + from xvfbwrapper import Xvfb + + self._vdisplay = Xvfb() + self._vdisplay.start() + + def _stop_display(self) -> None: + """Stop virtual framebuffer.""" + if self._vdisplay is not None: + self._vdisplay.stop() + self._vdisplay = None + + def _setup_environment(self) -> None: + """Configure environment for Dymola.""" + if "DYMOLA_LINGER_TIME" not in os.environ: + os.environ["DYMOLA_LINGER_TIME"] = str(self._config.linger_time) + + def _configure_compiler(self, dymola: Any) -> None: + """Apply Dymola compiler configuration.""" + if self._config.compile_64bit_only: + dymola.ExecuteCommand("Advanced.CompileWith64=2;") + + if self._config.enable_code_export: + dymola.ExecuteCommand("Advanced.EnableCodeExport=true;") + + if self._config.global_optimizations > 0: + dymola.ExecuteCommand( + f"Advanced.Define.GlobalOptimizations={self._config.global_optimizations};" + ) + + for cmd in self._config.additional_commands: + dymola.ExecuteCommand(cmd) + + def _map_fmi_type(self, fmi_type: FMIType) -> str: + """Map FMIType enum to Dymola string.""" + mapping = { + FMIType.MODEL_EXCHANGE: "me", + FMIType.CO_SIMULATION: "cs", + FMIType.BOTH: "all", + FMIType.CO_SIMULATION_SOLVER: "csSolver", + } + return mapping[fmi_type] + + def compile( + self, + model: ModelicaModel, + output_dir: Path, + config: CompilationConfig, + ) -> CompilationResult: + """Compile a Modelica model to FMU using Dymola. + + Args: + model: The Modelica model to compile + output_dir: Directory to place the generated FMU + config: Compilation configuration options + + Returns: + CompilationResult with success status and FMU path or error info + """ + if not self.is_available: + return CompilationResult( + success=False, + error_message="Dymola is not available. Check DYMOLA_ROOT and wheel path.", + ) + + logger = self._create_logger() + output_dir = Path(output_dir) + + # Validate output directory + if output_dir == Path.cwd(): + return CompilationResult( + success=False, + error_message=f"Output directory must differ from current directory: {Path.cwd()}", + ) + + # Determine output FMU name + fmu_name = config.output_name or model.model_name + target_fmu = output_dir / f"{fmu_name}.fmu" + + # Check existing FMU + if target_fmu.is_file(): + if config.force: + if config.verbose: + logger.warn(f"{fmu_name}.fmu exists in {output_dir}, will overwrite") + else: + return CompilationResult( + success=False, + error_message=f"{fmu_name}.fmu exists in {output_dir}. Use force=True to overwrite.", + ) + + # Create output directory + output_dir.mkdir(parents=True, exist_ok=True) + + # Start display server + self._start_display() + self._setup_environment() + + dymola = None + try: + # Initialize Dymola + dymola = self._dymola_interface( + dymolapath=self._config.executable, showwindow=False + ) + + # Configure compiler + self._configure_compiler(dymola) + + # Load packages + for package in config.packages: + if config.verbose: + logger.info(f"Loading package: {package}") + dymola.openModel(package, changeDirectory=False) + + # Apply custom flags + for flag in config.flags: + if config.verbose: + logger.info(f"Applying flag: {flag}") + dymola.ExecuteCommand(flag) + + # Open the model file + dymola.openModel(str(model.path), changeDirectory=False) + + # Set working directory + cwd_posix = str(Path.cwd().as_posix()) + dymola.ExecuteCommand(f'cd("{cwd_posix}");') + + if config.verbose: + logger.info(f"Compiling {model.fully_qualified_name} to {fmu_name}.fmu") + + # Translate to FMU + result = dymola.translateModelFMU( + model.fully_qualified_name, + modelName=fmu_name, + fmiVersion=config.fmi_version.value, + fmiType=self._map_fmi_type(config.fmi_type), + ) + + if not result: + error_log = dymola.getLastErrorLog() + license_info = dymola.DymolaLicenseInfo() + return CompilationResult( + success=False, + error_message="translateModelFMU returned False", + log=f"Error log:\n{error_log}\n\nLicense info:\n{license_info}", + ) + + # Verify FMU was created + expected_fmu = Path.cwd() / f"{fmu_name}.fmu" + if not expected_fmu.is_file(): + fmus_in_cwd = list(Path.cwd().glob("*.fmu")) + return CompilationResult( + success=False, + error_message=f"Expected FMU '{expected_fmu.name}' not found", + log=f"FMUs in directory: {fmus_in_cwd}", + ) + + # Remove existing FMU if force is set + if target_fmu.is_file() and config.force: + target_fmu.unlink() + + # Move FMU to output directory + dest = shutil.move(str(expected_fmu), str(output_dir)) + + if config.verbose: + logger.info(f"FMU successfully generated: {dest}") + + return CompilationResult( + success=True, + fmu_path=Path(dest), + ) + + except Exception as ex: + if self._dymola_exception and isinstance(ex, self._dymola_exception): + return CompilationResult( + success=False, + error_message=f"Dymola exception: {ex}", + ) + raise + + finally: + if dymola is not None: + dymola.close() + self._stop_display() + spd.drop("Logger") + + def check_model(self, model: ModelicaModel, packages: Optional[list[str]] = None) -> bool: + """Validate a Modelica model using Dymola. + + Args: + model: The Modelica model to check + packages: Additional packages to load + + Returns: + True if the model is valid, False otherwise + """ + if not self.is_available: + return False + + self._start_display() + self._setup_environment() + + dymola = None + try: + dymola = self._dymola_interface( + dymolapath=self._config.executable, showwindow=False + ) + + # Load packages + if packages: + for package in packages: + dymola.openModel(package, changeDirectory=False) + + # Open the model + dymola.openModel(str(model.path), changeDirectory=False) + + # Check the model + return dymola.checkModel(model.fully_qualified_name) + + except Exception: + return False + + finally: + if dymola is not None: + dymola.close() + self._stop_display() + + def validate_fmu(self, fmu_path: Path, simulate: bool = True) -> bool: + """Validate a generated FMU by importing and optionally simulating it. + + Args: + fmu_path: Path to the FMU file + simulate: Whether to run a simulation test + + Returns: + True if validation passes, False otherwise + """ + if not self.is_available: + return False + + self._start_display() + self._setup_environment() + + dymola = None + try: + dymola = self._dymola_interface( + dymolapath=self._config.executable, showwindow=False + ) + + # Import FMU + imported = dymola.importFMU(str(fmu_path)) + if not imported: + return False + + if simulate: + # Get model name from FMU + fmu_model = f"{fmu_path.stem}_fmu" + return dymola.checkModel(problem=fmu_model, simulate=True) + + return True + + except Exception: + return False + + finally: + if dymola is not None: + dymola.close() + self._stop_display() diff --git a/src/python/feelpp/mo2fmu/compilers/openmodelica.py b/src/python/feelpp/mo2fmu/compilers/openmodelica.py new file mode 100644 index 0000000..9d67307 --- /dev/null +++ b/src/python/feelpp/mo2fmu/compilers/openmodelica.py @@ -0,0 +1,616 @@ +"""OpenModelica compiler backend for mo2fmu. + +This module implements FMU generation using OpenModelica (open source Modelica tool). + +OpenModelica can be installed via: +- Linux: apt install openmodelica or from https://openmodelica.org/download +- macOS: brew install openmodelica +- Windows: Download from https://openmodelica.org/download + +The OMPython package provides Python bindings: + pip install OMPython +""" + +from __future__ import annotations + +import os +import shutil +import subprocess +import tempfile +import uuid +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Optional + +import spdlog as spd + +from feelpp.mo2fmu.compilers.base import ( + CompilationConfig, + CompilationResult, + FMIType, + FMIVersion, + FMUCompiler, + ModelicaModel, +) + + +@dataclass +class OpenModelicaConfig: + """OpenModelica-specific configuration. + + Attributes: + omc_path: Path to omc executable (None = auto-detect from PATH) + ompython_session: Use OMPython session instead of command line + target_platform: Target platform for FMU (e.g., "static", "linux64") + debug: Enable debug output + cpp_compiler: C++ compiler to use (e.g., "g++", "clang++") + c_compiler: C compiler to use (e.g., "gcc", "clang") + num_procs: Number of parallel compilation processes + command_line_options: Additional OMC command-line options + """ + + omc_path: Optional[str] = None + ompython_session: bool = True + target_platform: str = "static" + debug: bool = False + cpp_compiler: Optional[str] = None + c_compiler: Optional[str] = None + num_procs: int = 1 + command_line_options: list[str] = field(default_factory=list) + + @classmethod + def from_env(cls) -> "OpenModelicaConfig": + """Create configuration from environment variables.""" + return cls( + omc_path=os.getenv("OPENMODELICA_HOME"), + cpp_compiler=os.getenv("CXX"), + c_compiler=os.getenv("CC"), + ) + + +class OpenModelicaCompiler(FMUCompiler): + """OpenModelica-based FMU compiler. + + Uses OpenModelica's omc compiler or OMPython to compile Modelica models to FMUs. + This is an open-source alternative to Dymola. + + Requires: + - OpenModelica installed (omc in PATH or OPENMODELICA_HOME set) + - OMPython package (pip install OMPython) for Python API mode + + Example: + >>> config = OpenModelicaConfig.from_env() + >>> compiler = OpenModelicaCompiler(config) + >>> if compiler.is_available: + ... model = ModelicaModel(Path("model.mo")) + ... result = compiler.compile(model, Path("output"), CompilationConfig()) + ... if result.success: + ... print(f"FMU created at {result.fmu_path}") + """ + + def __init__(self, config: Optional[OpenModelicaConfig] = None) -> None: + """Initialize OpenModelica compiler. + + Args: + config: OpenModelica configuration. If None, uses defaults/environment. + """ + self._config = config or OpenModelicaConfig.from_env() + self._omc_session: Optional[Any] = None + self._ompython_available = False + self._omc_cli_available = False + self._logger: Optional[Any] = None + + self._check_availability() + + def _check_availability(self) -> None: + """Check if OpenModelica is available.""" + # Check command-line omc first (required for both CLI and OMPython modes) + omc_cmd = self._get_omc_command() + try: + result = subprocess.run( + [omc_cmd, "--version"], + capture_output=True, + text=True, + timeout=10, + ) + self._omc_cli_available = result.returncode == 0 + except (subprocess.TimeoutExpired, FileNotFoundError, OSError): + self._omc_cli_available = False + + # Check OMPython (requires omc to be available) + # OMPython is a Python interface to communicate with omc, so omc must be installed + try: + from OMPython import OMCSessionZMQ + + # OMPython package is installed, but it needs omc to work + self._ompython_available = self._omc_cli_available + except ImportError: + self._ompython_available = False + + def _get_omc_command(self) -> str: + """Get the omc command path.""" + if self._config.omc_path: + omc_bin = Path(self._config.omc_path) / "bin" / "omc" + if omc_bin.is_file(): + return str(omc_bin) + return "omc" + + @property + def name(self) -> str: + """Return the compiler backend name.""" + return "openmodelica" + + @property + def is_available(self) -> bool: + """Check if OpenModelica is available on this system.""" + return self._ompython_available or self._omc_cli_available + + @property + def config(self) -> OpenModelicaConfig: + """Return the OpenModelica configuration.""" + return self._config + + def get_version(self) -> Optional[str]: + """Return the OpenModelica version string.""" + omc_cmd = self._get_omc_command() + try: + result = subprocess.run( + [omc_cmd, "--version"], + capture_output=True, + text=True, + timeout=10, + ) + if result.returncode == 0: + # Parse version from output like "OpenModelica v1.22.0" + version_line = result.stdout.strip().split("\n")[0] + if "OpenModelica" in version_line: + parts = version_line.split() + for part in parts: + if part.startswith("v") or part[0].isdigit(): + return part.lstrip("v") + return version_line + except (subprocess.TimeoutExpired, FileNotFoundError, OSError): + pass + return None + + def _create_logger(self) -> Any: + """Create a unique logger instance.""" + logger_name = f"omc_{uuid.uuid4().hex[:8]}" + return spd.ConsoleLogger(logger_name, False, True, True) + + def _map_fmi_type(self, fmi_type: FMIType) -> str: + """Map FMIType enum to OpenModelica string.""" + mapping = { + FMIType.MODEL_EXCHANGE: "me", + FMIType.CO_SIMULATION: "cs", + FMIType.BOTH: "me_cs", # OpenModelica uses me_cs for both + FMIType.CO_SIMULATION_SOLVER: "cs", # Fallback to cs + } + return mapping[fmi_type] + + def _map_fmi_version(self, fmi_version: FMIVersion) -> str: + """Map FMIVersion enum to OpenModelica string.""" + mapping = { + FMIVersion.FMI_1_0: "1.0", + FMIVersion.FMI_2_0: "2.0", + FMIVersion.FMI_3_0: "3.0", + } + return mapping[fmi_version] + + def _build_fmu_flags(self, config: CompilationConfig) -> str: + """Build the FMU flags string for buildModelFMU.""" + fmi_version = self._map_fmi_version(config.fmi_version) + fmi_type = self._map_fmi_type(config.fmi_type) + + # Build platforms list + platforms = f'{{"{self._config.target_platform}"}}' + + return f'version="{fmi_version}", fmuType="{fmi_type}", platforms={platforms}' + + def _compile_with_ompython( + self, + model: ModelicaModel, + output_dir: Path, + config: CompilationConfig, + logger: Any, + ) -> CompilationResult: + """Compile using OMPython session.""" + from OMPython import OMCSessionZMQ + + fmu_name = config.output_name or model.model_name + target_fmu = output_dir / f"{fmu_name}.fmu" + + omc = None + original_cwd = Path.cwd() + + try: + # Create OMC session + omc = OMCSessionZMQ() + + if config.verbose: + logger.info("OMC session started") + + # Change to a temporary working directory + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + # Set working directory in OMC + cd_result = omc.sendExpression(f'cd("{temp_path.as_posix()}")') + if config.verbose: + logger.info(f"Working directory: {cd_result}") + + # Load packages + for package in config.packages: + if config.verbose: + logger.info(f"Loading package: {package}") + + pkg_path = Path(package) + if pkg_path.suffix == ".mo": + result = omc.sendExpression(f'loadFile("{pkg_path.as_posix()}")') + else: + result = omc.sendExpression(f'loadModel({package})') + + if not result: + error = omc.sendExpression("getErrorString()") + logger.warn(f"Failed to load {package}: {error}") + + # Apply command-line options + for opt in self._config.command_line_options: + omc.sendExpression(f'setCommandLineOptions("{opt}")') + + # Apply custom flags + for flag in config.flags: + if config.verbose: + logger.info(f"Applying flag: {flag}") + omc.sendExpression(flag) + + # Load the model file + load_result = omc.sendExpression(f'loadFile("{model.path.as_posix()}")') + if not load_result: + error = omc.sendExpression("getErrorString()") + return CompilationResult( + success=False, + error_message=f"Failed to load model: {error}", + ) + + if config.verbose: + logger.info(f"Model loaded: {model.fully_qualified_name}") + + # Check model first + check_result = omc.sendExpression(f"checkModel({model.fully_qualified_name})") + if config.verbose: + logger.info(f"Model check result: {check_result}") + + # Build FMU flags + fmu_flags = self._build_fmu_flags(config) + + # Build the FMU + build_cmd = f"buildModelFMU({model.fully_qualified_name}, {fmu_flags})" + if config.verbose: + logger.info(f"Build command: {build_cmd}") + + fmu_result = omc.sendExpression(build_cmd) + + if config.verbose: + logger.info(f"Build result: {fmu_result}") + + # Check for errors + error_string = omc.sendExpression("getErrorString()") + if error_string and "Error" in error_string: + return CompilationResult( + success=False, + error_message="FMU compilation failed", + log=error_string, + ) + + # Find the generated FMU + if fmu_result and isinstance(fmu_result, str) and fmu_result.endswith(".fmu"): + generated_fmu = Path(fmu_result) + else: + # Look for FMU in temp directory + fmus = list(temp_path.glob("*.fmu")) + if not fmus: + return CompilationResult( + success=False, + error_message="No FMU file generated", + log=error_string, + ) + generated_fmu = fmus[0] + + if not generated_fmu.is_file(): + return CompilationResult( + success=False, + error_message=f"Generated FMU not found: {generated_fmu}", + log=error_string, + ) + + # Remove existing FMU if force is set + if target_fmu.is_file() and config.force: + target_fmu.unlink() + + # Copy FMU to output directory + shutil.copy2(generated_fmu, target_fmu) + + if config.verbose: + logger.info(f"FMU successfully generated: {target_fmu}") + + warnings = [] + if error_string and error_string.strip(): + warnings = [ + line + for line in error_string.split("\n") + if line.strip() and "Warning" in line + ] + + return CompilationResult( + success=True, + fmu_path=target_fmu, + log=error_string if error_string else None, + warnings=warnings, + ) + + except Exception as ex: + return CompilationResult( + success=False, + error_message=f"OMPython exception: {ex}", + ) + + finally: + if omc is not None: + try: + omc.sendExpression("quit()") + except Exception: + pass + os.chdir(original_cwd) + + def _compile_with_cli( + self, + model: ModelicaModel, + output_dir: Path, + config: CompilationConfig, + logger: Any, + ) -> CompilationResult: + """Compile using omc command line.""" + fmu_name = config.output_name or model.model_name + target_fmu = output_dir / f"{fmu_name}.fmu" + omc_cmd = self._get_omc_command() + + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + # Build the .mos script + script_lines = [] + + # Load packages + for package in config.packages: + pkg_path = Path(package) + if pkg_path.suffix == ".mo": + script_lines.append(f'loadFile("{pkg_path.as_posix()}");') + else: + script_lines.append(f"loadModel({package});") + + # Apply command-line options + for opt in self._config.command_line_options: + script_lines.append(f'setCommandLineOptions("{opt}");') + + # Apply custom flags + for flag in config.flags: + script_lines.append(flag if flag.endswith(";") else f"{flag};") + + # Load model + script_lines.append(f'loadFile("{model.path.as_posix()}");') + + # Build FMU + fmu_flags = self._build_fmu_flags(config) + script_lines.append(f"buildModelFMU({model.fully_qualified_name}, {fmu_flags});") + + # Get errors + script_lines.append("getErrorString();") + + # Write script + script_path = temp_path / "build_fmu.mos" + with open(script_path, "w") as f: + f.write("\n".join(script_lines)) + + if config.verbose: + logger.info(f"OMC script:\n{chr(10).join(script_lines)}") + + # Run omc + try: + result = subprocess.run( + [omc_cmd, str(script_path)], + capture_output=True, + text=True, + cwd=str(temp_path), + timeout=600, # 10 minute timeout + ) + except subprocess.TimeoutExpired: + return CompilationResult( + success=False, + error_message="OMC compilation timed out after 10 minutes", + ) + + output = result.stdout + "\n" + result.stderr + + if config.verbose: + logger.info(f"OMC output:\n{output}") + + # Check for generated FMU + fmus = list(temp_path.glob("*.fmu")) + if not fmus: + return CompilationResult( + success=False, + error_message="No FMU file generated", + log=output, + ) + + generated_fmu = fmus[0] + + # Remove existing FMU if force is set + if target_fmu.is_file() and config.force: + target_fmu.unlink() + + # Copy FMU to output directory + shutil.copy2(generated_fmu, target_fmu) + + if config.verbose: + logger.info(f"FMU successfully generated: {target_fmu}") + + return CompilationResult( + success=True, + fmu_path=target_fmu, + log=output, + ) + + def compile( + self, + model: ModelicaModel, + output_dir: Path, + config: CompilationConfig, + ) -> CompilationResult: + """Compile a Modelica model to FMU using OpenModelica. + + Args: + model: The Modelica model to compile + output_dir: Directory to place the generated FMU + config: Compilation configuration options + + Returns: + CompilationResult with success status and FMU path or error info + """ + if not self.is_available: + return CompilationResult( + success=False, + error_message="OpenModelica is not available. Install omc and/or OMPython.", + ) + + logger = self._create_logger() + output_dir = Path(output_dir) + + # Validate output directory + if output_dir == Path.cwd(): + return CompilationResult( + success=False, + error_message=f"Output directory must differ from current directory: {Path.cwd()}", + ) + + # Determine output FMU name + fmu_name = config.output_name or model.model_name + target_fmu = output_dir / f"{fmu_name}.fmu" + + # Check existing FMU + if target_fmu.is_file() and not config.force: + return CompilationResult( + success=False, + error_message=f"{fmu_name}.fmu exists in {output_dir}. Use force=True to overwrite.", + ) + + # Create output directory + output_dir.mkdir(parents=True, exist_ok=True) + + # Choose compilation method + if self._config.ompython_session and self._ompython_available: + return self._compile_with_ompython(model, output_dir, config, logger) + elif self._omc_cli_available: + return self._compile_with_cli(model, output_dir, config, logger) + else: + return CompilationResult( + success=False, + error_message="Neither OMPython nor omc CLI is available", + ) + + def check_model(self, model: ModelicaModel, packages: Optional[list[str]] = None) -> bool: + """Validate a Modelica model using OpenModelica. + + Args: + model: The Modelica model to check + packages: Additional packages to load + + Returns: + True if the model is valid, False otherwise + """ + if not self.is_available: + return False + + if self._ompython_available: + return self._check_model_ompython(model, packages) + elif self._omc_cli_available: + return self._check_model_cli(model, packages) + return False + + def _check_model_ompython( + self, model: ModelicaModel, packages: Optional[list[str]] = None + ) -> bool: + """Check model using OMPython.""" + from OMPython import OMCSessionZMQ + + omc = None + try: + omc = OMCSessionZMQ() + + # Load packages + if packages: + for package in packages: + pkg_path = Path(package) + if pkg_path.suffix == ".mo": + omc.sendExpression(f'loadFile("{pkg_path.as_posix()}")') + else: + omc.sendExpression(f"loadModel({package})") + + # Load model + omc.sendExpression(f'loadFile("{model.path.as_posix()}")') + + # Check model + result = omc.sendExpression(f"checkModel({model.fully_qualified_name})") + return result is not None and "Error" not in str(result) + + except Exception: + return False + + finally: + if omc is not None: + try: + omc.sendExpression("quit()") + except Exception: + pass + + def _check_model_cli( + self, model: ModelicaModel, packages: Optional[list[str]] = None + ) -> bool: + """Check model using omc CLI.""" + omc_cmd = self._get_omc_command() + + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + script_lines = [] + + # Load packages + if packages: + for package in packages: + pkg_path = Path(package) + if pkg_path.suffix == ".mo": + script_lines.append(f'loadFile("{pkg_path.as_posix()}");') + else: + script_lines.append(f"loadModel({package});") + + # Load and check model + script_lines.append(f'loadFile("{model.path.as_posix()}");') + script_lines.append(f"checkModel({model.fully_qualified_name});") + script_lines.append("getErrorString();") + + script_path = temp_path / "check_model.mos" + with open(script_path, "w") as f: + f.write("\n".join(script_lines)) + + try: + result = subprocess.run( + [omc_cmd, str(script_path)], + capture_output=True, + text=True, + cwd=str(temp_path), + timeout=60, + ) + return result.returncode == 0 and "Error" not in result.stdout + except (subprocess.TimeoutExpired, FileNotFoundError, OSError): + return False diff --git a/src/python/feelpp/mo2fmu/mo2fmu.py b/src/python/feelpp/mo2fmu/mo2fmu.py index 3cd78b7..ffb072c 100644 --- a/src/python/feelpp/mo2fmu/mo2fmu.py +++ b/src/python/feelpp/mo2fmu/mo2fmu.py @@ -1,17 +1,280 @@ -"""mo2fmu - Convert Modelica models to Functional Mock-up Units (FMUs).""" +"""mo2fmu - Convert Modelica models to Functional Mock-up Units (FMUs). + +This module provides both a Python API and CLI for converting Modelica +models to FMUs using either Dymola (commercial) or OpenModelica (open source). +""" from __future__ import annotations -import os -import platform -import shutil -import sys +import warnings +from importlib.metadata import version as get_version from pathlib import Path -from typing import Optional +from typing import Literal, Optional, Union import click -import spdlog as spd -from xvfbwrapper import Xvfb + +# Import compiler classes +from feelpp.mo2fmu.compilers.base import ( + CompilationConfig, + CompilationResult, + FMIType, + FMIVersion, + FMUCompiler, + ModelicaModel, +) +from feelpp.mo2fmu.compilers.dymola import DymolaCompiler, DymolaConfig +from feelpp.mo2fmu.compilers.openmodelica import OpenModelicaCompiler, OpenModelicaConfig + +# Type alias for backend selection +Backend = Literal["dymola", "openmodelica", "auto"] + + +# ============================================================================= +# Public API Functions (camelCase) +# ============================================================================= + + +def checkCompilers( + dymolaConfig: Optional[DymolaConfig] = None, + openModelicaConfig: Optional[OpenModelicaConfig] = None, +) -> dict: + """Check availability and FMI version support for all compilers. + + Args: + dymolaConfig: Configuration for Dymola compiler + openModelicaConfig: Configuration for OpenModelica compiler + + Returns: + Dictionary with compiler availability and FMI support information + """ + results = { + "dymola": { + "available": False, + "version": None, + "fmiSupport": [], + }, + "openmodelica": { + "available": False, + "version": None, + "fmiSupport": [], + }, + } + + # Check Dymola + dymola = DymolaCompiler(dymolaConfig) + results["dymola"]["available"] = dymola.is_available + if dymola.is_available: + results["dymola"]["version"] = dymola.get_version() + # Dymola 2024+ supports FMI 3.0, earlier versions support FMI 1.0 and 2.0 + versionStr = results["dymola"]["version"] + if versionStr: + try: + # Version format: "2025.1" or "2024.1" + majorVersion = int(versionStr.split(".")[0]) + results["dymola"]["fmiSupport"] = ["1", "2"] + if majorVersion >= 2024: + results["dymola"]["fmiSupport"].append("3") + except (ValueError, IndexError): + # Can't parse version, assume FMI 2.0 support + results["dymola"]["fmiSupport"] = ["1", "2"] + else: + results["dymola"]["fmiSupport"] = ["1", "2"] + + # Check OpenModelica + omc = OpenModelicaCompiler(openModelicaConfig) + results["openmodelica"]["available"] = omc.is_available + if omc.is_available: + results["openmodelica"]["version"] = omc.get_version() + # OpenModelica 1.21+ supports FMI 3.0, earlier versions support FMI 1.0 and 2.0 + versionStr = results["openmodelica"]["version"] + if versionStr: + try: + # Version format: "1.22.0" or "1.21.0" + parts = versionStr.split(".") + major = int(parts[0]) + minor = int(parts[1]) if len(parts) > 1 else 0 + results["openmodelica"]["fmiSupport"] = ["1", "2"] + if major > 1 or (major == 1 and minor >= 21): + results["openmodelica"]["fmiSupport"].append("3") + except (ValueError, IndexError): + # Can't parse version, assume FMI 2.0 support + results["openmodelica"]["fmiSupport"] = ["1", "2"] + else: + results["openmodelica"]["fmiSupport"] = ["1", "2"] + + return results + + +def getCompiler( + backend: Backend = "auto", + dymolaConfig: Optional[DymolaConfig] = None, + openModelicaConfig: Optional[OpenModelicaConfig] = None, +) -> FMUCompiler: + """Get an FMU compiler instance. + + Args: + backend: Compiler backend to use ("dymola", "openmodelica", or "auto") + dymolaConfig: Configuration for Dymola compiler + openModelicaConfig: Configuration for OpenModelica compiler + + Returns: + An FMUCompiler instance + + Raises: + RuntimeError: If no suitable compiler is available + """ + if backend == "dymola": + compiler = DymolaCompiler(dymolaConfig) + if not compiler.is_available: + raise RuntimeError("Dymola is not available. Check installation and configuration.") + return compiler + + if backend == "openmodelica": + compiler = OpenModelicaCompiler(openModelicaConfig) + if not compiler.is_available: + raise RuntimeError( + "OpenModelica is not available. Install omc and/or OMPython." + ) + return compiler + + # Auto-detection: prefer Dymola if available, fall back to OpenModelica + dymola = DymolaCompiler(dymolaConfig) + if dymola.is_available: + return dymola + + omc = OpenModelicaCompiler(openModelicaConfig) + if omc.is_available: + return omc + + raise RuntimeError( + "No Modelica compiler available. Install Dymola or OpenModelica." + ) + + +def compileFmu( + mo: Union[str, Path], + outdir: Union[str, Path], + backend: Backend = "auto", + fmuModelName: Optional[str] = None, + load: Optional[list[str]] = None, + flags: Optional[list[str]] = None, + fmiType: str = "all", + fmiVersion: str = "2", + verbose: bool = False, + force: bool = False, + dymolaConfig: Optional[DymolaConfig] = None, + openModelicaConfig: Optional[OpenModelicaConfig] = None, +) -> CompilationResult: + """Convert a Modelica model to FMU. + + This is the primary API for FMU compilation. + + Args: + mo: Path to the Modelica .mo file + outdir: Output directory for the generated FMU + backend: Compiler backend ("dymola", "openmodelica", or "auto") + fmuModelName: Custom name for the FMU (defaults to model name) + load: List of Modelica packages to load + flags: List of compiler-specific flags + fmiType: FMI type ("cs", "me", "all", or "csSolver") + fmiVersion: FMI version ("1", "2", or "3") + verbose: Enable verbose logging + force: Overwrite existing FMU if present + dymolaConfig: Dymola-specific configuration + openModelicaConfig: OpenModelica-specific configuration + + Returns: + CompilationResult with success status and FMU path or error info + + Example: + >>> result = compileFmu("model.mo", "./output", backend="auto") + >>> if result.success: + ... print(f"FMU created at {result.fmu_path}") + ... else: + ... print(f"Error: {result.error_message}") + """ + # Get compiler + compiler = getCompiler(backend, dymolaConfig, openModelicaConfig) + + # Create model representation + model = ModelicaModel(Path(mo)) + + # Create compilation config + config = CompilationConfig( + fmi_type=FMIType.from_string(fmiType), + fmi_version=FMIVersion.from_string(fmiVersion), + output_name=fmuModelName, + packages=load or [], + flags=flags or [], + force=force, + verbose=verbose, + ) + + # Compile + return compiler.compile(model, Path(outdir), config) + + +# ============================================================================= +# Legacy API (deprecated, for backward compatibility) +# ============================================================================= + + +def get_compiler( + backend: Backend = "auto", + dymola_config: Optional[DymolaConfig] = None, + openmodelica_config: Optional[OpenModelicaConfig] = None, +) -> FMUCompiler: + """Get an FMU compiler instance. + + .. deprecated:: + Use :func:`getCompiler` instead. + """ + warnings.warn( + "get_compiler() is deprecated, use getCompiler() instead", + DeprecationWarning, + stacklevel=2, + ) + return getCompiler(backend, dymola_config, openmodelica_config) + + +def mo2fmu_new( + mo: Union[str, Path], + outdir: Union[str, Path], + backend: Backend = "auto", + fmumodelname: Optional[str] = None, + load: Optional[list[str]] = None, + flags: Optional[list[str]] = None, + fmi_type: str = "all", + fmi_version: str = "2", + verbose: bool = False, + force: bool = False, + dymola_config: Optional[DymolaConfig] = None, + openmodelica_config: Optional[OpenModelicaConfig] = None, +) -> CompilationResult: + """Convert a Modelica model to FMU. + + .. deprecated:: + Use :func:`compileFmu` instead. + """ + warnings.warn( + "mo2fmu_new() is deprecated, use compileFmu() instead", + DeprecationWarning, + stacklevel=2, + ) + return compileFmu( + mo=mo, + outdir=outdir, + backend=backend, + fmuModelName=fmumodelname, + load=load, + flags=flags, + fmiType=fmi_type, + fmiVersion=fmi_version, + verbose=verbose, + force=force, + dymolaConfig=dymola_config, + openModelicaConfig=openmodelica_config, + ) def mo2fmu( @@ -27,9 +290,14 @@ def mo2fmu( dymolawhl: str, verbose: bool, force: bool, + backend: str = "dymola", ) -> bool: """Convert a .mo file into a .fmu. + .. deprecated:: + Use :func:`compileFmu` instead. This function is maintained for + backward compatibility only. + Args: mo: Path to the Modelica .mo file to convert outdir: Output directory for the generated FMU @@ -43,253 +311,372 @@ def mo2fmu( dymolawhl: Path to Dymola wheel file (relative to dymola root) verbose: Enable verbose logging force: Force overwrite of existing FMU + backend: Compiler backend ("dymola" or "openmodelica") Returns: True if conversion was successful, False otherwise - - Example: - >>> mo2fmu("model.mo", "./output", None, None, None, "cs", "2", - ... "/opt/dymola", "/usr/local/bin/dymola", "dymola.whl", True, False) """ - # Create logger with unique name based on file being processed - import uuid + warnings.warn( + "mo2fmu() is deprecated, use compileFmu() instead", + DeprecationWarning, + stacklevel=2, + ) - logger_name = f"mo2fmu_{uuid.uuid4().hex[:8]}" - logger = spd.ConsoleLogger(logger_name, False, True, True) - has_dymola = False + # Build config for Dymola + dymolaConfig = DymolaConfig( + root=dymola_root, + executable=dymolapath, + wheel_path=dymolawhl, + ) - # Prevent writing FMU into the same directory as cwd - if Path(outdir) == Path(os.getcwd()): - logger.error(f"the destination directory should be different from {os.getcwd()}") - return False + # Use the new unified API + result = compileFmu( + mo=mo, + outdir=outdir, + backend=backend, # type: ignore + fmuModelName=fmumodelname, + load=list(load) if load else None, + flags=list(flags) if flags else None, + fmiType=type, + fmiVersion=version, + verbose=verbose, + force=force, + dymolaConfig=dymolaConfig, + ) - # Attempt to load Dymola's Python interface - try: - sys.path.append(str(Path(dymola_root) / Path(dymolawhl))) - logger.info(f"add {Path(dymola_root) / Path(dymolawhl)} to sys path") - if not (Path(dymola_root) / Path(dymolawhl)).is_file(): - logger.error(f"dymola whl {Path(dymola_root) / Path(dymolawhl)} does not exist") - import dymola # noqa: F401 - from dymola.dymola_exception import DymolaException - from dymola.dymola_interface import DymolaInterface - - has_dymola = True - logger.info(f"dymola is available in {dymola_root}/{dymolawhl}") - except ImportError: - logger.info(f"dymola module is not available, has_dymola: {has_dymola}") - if not has_dymola: - logger.error("dymola is not available, mo2fmu failed") - return False - - # Start a virtual framebuffer (for headless Dymola) - vdisplay = Xvfb() - vdisplay.start() - - osString = platform.system() - isWindows = osString.startswith("Win") # noqa: F841 - - dymola_interface = None - try: - # Determine the FMU model name (default: .mo file stem) - fmumodelname = Path(fmumodelname if fmumodelname else mo).stem - if verbose: - logger.info(f"convert {mo} to {fmumodelname}.fmu") - - # If an FMU already exists in outdir - target_fmu = Path(outdir) / f"{fmumodelname}.fmu" - if target_fmu.is_file() and force: - logger.warn(f"{fmumodelname}.fmu exists in {outdir}, will overwrite it") - elif target_fmu.is_file(): - logger.warn(f"{fmumodelname}.fmu exists in {outdir}; use `--force` to overwrite.") - return False - - # Create outdir if it doesn't exist - if not Path(outdir).is_dir(): - Path(outdir).mkdir(parents=True, exist_ok=True) - - # Set DYMOLA_LINGER_TIME to 0 for immediate license release - # This prevents license tokens from being held for 10 minutes after use - if "DYMOLA_LINGER_TIME" not in os.environ: - os.environ["DYMOLA_LINGER_TIME"] = "0" - if verbose: - logger.info("Set DYMOLA_LINGER_TIME=0 for immediate license release") - - # Instantiate Dymola interface - dymola_interface = DymolaInterface(dymolapath=dymolapath, showwindow=False) - - # **1) Disable any 32-bit build first and force 64-bit-only compilation ** - dymola_interface.ExecuteCommand("Advanced.CompileWith64=2;") - # **2) Enable code export so FMU contains sources or compiled binaries ** - # and no longer requires a license to run - dymola_interface.ExecuteCommand("Advanced.EnableCodeExport=true;") - # **3) Turn on full compiler optimizations (instead of the default -O1) - dymola_interface.ExecuteCommand("Advanced.Define.GlobalOptimizations=2;") - - # Compute the fully qualified model name (package + file stem) - packageName = "" - with open(mo) as f: - lines = f.readlines() - for line in lines: - if line.strip().startswith("within "): - packageName = line.split(" ")[1][:-2] - if packageName: - moModel = f"{packageName}.{Path(mo).stem}" - else: - moModel = Path(mo).stem - - # Load any additional packages - if load: - for package in load: - if verbose: - logger.info(f"load modelica package {package}") - dymola_interface.openModel(package, changeDirectory=False) - - # Apply any Dymola flags - if flags: - for flag in flags: - if verbose: - logger.info(f"Flag {flag}") - dymola_interface.ExecuteCommand(flag) - - # Open the .mo file - dymola_interface.openModel(mo, changeDirectory=False) - - # Ensure Dymola's working directory matches Python's cwd - cwd_posix = str(Path.cwd().as_posix()) - dymola_interface.ExecuteCommand(f'cd("{cwd_posix}");') - logger.info(f"Dymola working directory = {cwd_posix}") - - # Request FMU translation (now only 64-bit since 32-bit is disabled) - result = dymola_interface.translateModelFMU( - moModel, modelName=fmumodelname, fmiVersion="2", fmiType=type - ) + return result.success - if not result: - log = dymola_interface.getLastErrorLog() - licInfo = dymola_interface.DymolaLicenseInfo() - logger.error("translateModelFMU returned False. Dymola log:") - logger.error(log) - logger.error("Dymola License Information:") - logger.error(licInfo) - return False - - # Verify that the FMU file actually appeared - expected_fmu = Path.cwd() / f"{fmumodelname}.fmu" - if not expected_fmu.is_file(): - logger.error(f"Expected FMU '{expected_fmu.name}' not found in {Path.cwd()}") - logger.error(f"Directory listing (*.fmu): {list(Path.cwd().glob('*.fmu'))}") - return False - - # If an old FMU exists in outdir and --force was given, remove it - if target_fmu.is_file() and force: - target_fmu.unlink() - elif target_fmu.is_file(): - logger.warning( - f"{target_fmu.name} already exists in {outdir}; use --force to overwrite" - ) - return False - # Move the FMU to the output directory - dest = shutil.move(str(expected_fmu), str(Path(outdir))) - logger.info(f"translateModelFMU {Path(mo).stem} → {dest}") +# ============================================================================= +# CLI with Click Group +# ============================================================================= - if verbose: - logger.info(f"{fmumodelname}.fmu successfully generated in {outdir}") - return True +@click.group(invoke_without_command=True) +@click.pass_context +@click.option("-v", "--version", is_flag=True, help="Show version information.") +def mo2fmuCLI(ctx: click.Context, version: bool) -> None: + """mo2fmu - Convert Modelica models to Functional Mock-up Units (FMUs). - except DymolaException as ex: - logger.error(str(ex)) - return False + Use 'mo2fmu compile' to generate FMUs or 'mo2fmu check' to verify compilers. - finally: - # Clean up: close Dymola and stop the virtual framebuffer - if dymola_interface is not None: - dymola_interface.close() - vdisplay.stop() - spd.drop("Logger") + Examples: + + mo2fmu compile model.mo ./output + mo2fmu compile -v --force model.mo ./output -@click.command() -@click.argument("mo", type=str, nargs=1) -@click.argument("outdir", type=click.Path(), nargs=1) + mo2fmu check + + mo2fmu check --dymola /opt/dymola-2025x + """ + if version: + click.echo(f"mo2fmu version {get_version('feelpp-mo2fmu')}") + return + + if ctx.invoked_subcommand is None: + click.echo(ctx.get_help()) + + +@mo2fmuCLI.command("compile") +@click.argument("mo", type=click.Path(exists=True)) +@click.argument("outdir", type=click.Path()) @click.option( - "--fmumodelname", + "--name", default=None, type=str, - help="change the model name of the FMU (default: .mo file stem)", + help="Custom name for the FMU (default: .mo file stem).", +) +@click.option( + "--load", + "-l", + default=None, + multiple=True, + help="Load one or more Modelica packages.", ) -@click.option("--load", default=None, multiple=True, help="load one or more Modelica packages.") @click.option( "--flags", default=None, multiple=True, - help="one or more Dymola flags for FMU translation.", + help="Compiler-specific flags for FMU translation.", ) @click.option( "--type", + "-t", + "fmiType", default="all", type=click.Choice(["all", "cs", "me", "csSolver"]), - help="the FMI type: cs, me, all, or csSolver.", + help="FMI type: cs (Co-Simulation), me (Model Exchange), all, or csSolver.", +) +@click.option( + "--fmi-version", + default="2", + type=click.Choice(["1", "2", "3"]), + help="FMI version. FMI 3.0 requires Dymola 2024+ or OpenModelica 1.21+.", +) +@click.option( + "--backend", + "-b", + default="auto", + type=click.Choice(["dymola", "openmodelica", "auto"]), + help="Modelica compiler backend (default: auto-detect).", ) -@click.option("--version", default="2", help="the FMI version.") @click.option( "--dymola", default="/opt/dymola-2025xRefresh1-x86_64/", type=click.Path(), - help="path to Dymola root.", + envvar="DYMOLA_ROOT", + help="Path to Dymola root directory.", ) @click.option( - "--dymolapath", + "--dymola-exec", default="/usr/local/bin/dymola", type=click.Path(), - help="path to Dymola executable.", + envvar="DYMOLA_EXECUTABLE", + help="Path to Dymola executable.", ) @click.option( - "--dymolawhl", + "--dymola-whl", default="Modelica/Library/python_interface/dymola-2025.1-py3-none-any.whl", type=click.Path(), - help="path to Dymola whl file, relative to Dymola root.", + envvar="DYMOLA_WHL", + help="Path to Dymola wheel file (relative to Dymola root).", ) -@click.option("-v", "--verbose", is_flag=True, help="verbose mode.") -@click.option("-f", "--force", is_flag=True, help="force FMU generation even if file exists.") -def mo2fmuCLI( +@click.option("-v", "--verbose", is_flag=True, help="Enable verbose output.") +@click.option("-f", "--force", is_flag=True, help="Overwrite existing FMU.") +def compileCmd( mo: str, outdir: str, - fmumodelname: Optional[str], - load: Optional[tuple[str, ...]], - flags: Optional[tuple[str, ...]], - type: str, - version: str, + name: Optional[str], + load: tuple[str, ...], + flags: tuple[str, ...], + fmiType: str, + fmi_version: str, + backend: str, dymola: str, - dymolapath: str, - dymolawhl: str, + dymola_exec: str, + dymola_whl: str, verbose: bool, force: bool, ) -> None: - """Convert Modelica (.mo) files to Functional Mock-up Units (.fmu). + """Compile a Modelica model to FMU. MO: Path to the Modelica model file (.mo) + OUTDIR: Output directory for the generated FMU Examples: - mo2fmu model.mo ./output - mo2fmu -v --force model.mo ./output + mo2fmu compile model.mo ./output + + mo2fmu compile -v --force --fmi-version 3 model.mo ./output + + mo2fmu compile --backend openmodelica model.mo ./output + + mo2fmu compile --load package.mo model.mo ./output + """ + # Build Dymola config + dymolaConfig = DymolaConfig( + root=dymola, + executable=dymola_exec, + wheel_path=dymola_whl, + ) + + try: + result = compileFmu( + mo=mo, + outdir=outdir, + backend=backend, # type: ignore + fmuModelName=name, + load=list(load) if load else None, + flags=list(flags) if flags else None, + fmiType=fmiType, + fmiVersion=fmi_version, + verbose=verbose, + force=force, + dymolaConfig=dymolaConfig, + ) + + if result.success: + click.echo(f"FMU created: {result.fmu_path}") + else: + click.echo(f"Error: {result.error_message}", err=True) + if result.log: + click.echo(f"Log:\n{result.log}", err=True) + raise SystemExit(1) + + except RuntimeError as e: + click.echo(f"Error: {e}", err=True) + raise SystemExit(1) + + +@mo2fmuCLI.command("check") +@click.option( + "--dymola", + default="/opt/dymola-2025xRefresh1-x86_64/", + type=click.Path(), + envvar="DYMOLA_ROOT", + help="Path to Dymola root directory.", +) +@click.option( + "--dymola-exec", + default="/usr/local/bin/dymola", + type=click.Path(), + envvar="DYMOLA_EXECUTABLE", + help="Path to Dymola executable.", +) +@click.option( + "--dymola-whl", + default="Modelica/Library/python_interface/dymola-2025.1-py3-none-any.whl", + type=click.Path(), + envvar="DYMOLA_WHL", + help="Path to Dymola wheel file (relative to Dymola root).", +) +@click.option("--json", "asJson", is_flag=True, help="Output results as JSON.") +def checkCmd( + dymola: str, + dymola_exec: str, + dymola_whl: str, + asJson: bool, +) -> None: + """Check availability of Modelica compilers and their FMI support. + + This command checks for Dymola and OpenModelica installations + and reports their versions and supported FMI versions. + + Examples: + + mo2fmu check + + mo2fmu check --json + + mo2fmu check --dymola /opt/dymola-2024x + """ + dymolaConfig = DymolaConfig( + root=dymola, + executable=dymola_exec, + wheel_path=dymola_whl, + ) + + results = checkCompilers(dymolaConfig=dymolaConfig) + + if asJson: + import json + click.echo(json.dumps(results, indent=2)) + return + + click.echo("=" * 60) + click.echo("mo2fmu Compiler Availability Check") + click.echo("=" * 60) + + # Dymola + click.echo("\nDymola:") + if results["dymola"]["available"]: + click.echo(" Status: Available") + click.echo(f" Version: {results['dymola']['version'] or 'Unknown'}") + fmiVersions = ", ".join(results["dymola"]["fmiSupport"]) + click.echo(f" FMI Support: {fmiVersions}") + else: + click.echo(" Status: Not available") + click.echo(" Hint: Set DYMOLA_ROOT environment variable or use --dymola option") + + # OpenModelica + click.echo("\nOpenModelica:") + if results["openmodelica"]["available"]: + click.echo(" Status: Available") + click.echo(f" Version: {results['openmodelica']['version'] or 'Unknown'}") + fmiVersions = ", ".join(results["openmodelica"]["fmiSupport"]) + click.echo(f" FMI Support: {fmiVersions}") + else: + click.echo(" Status: Not available") + click.echo(" Hint: Install OpenModelica and OMPython (pip install OMPython)") + + click.echo("\n" + "=" * 60) + + # Summary + availableCount = sum(1 for c in results.values() if c["available"]) + if availableCount == 0: + click.echo("Warning: No compilers available!") + raise SystemExit(1) + elif availableCount == 1: + compilerName = "Dymola" if results["dymola"]["available"] else "OpenModelica" + click.echo(f"Summary: {compilerName} is available for FMU generation.") + else: + click.echo("Summary: Both compilers are available for FMU generation.") + + +# ============================================================================= +# Legacy CLI entry point (for backward compatibility with old command style) +# ============================================================================= + + +@click.command("mo2fmu-legacy") +@click.argument("mo", type=str, nargs=1, required=False) +@click.argument("outdir", type=click.Path(), nargs=1, required=False) +@click.option("--check", is_flag=True, help="Check compiler availability.") +@click.option("--fmumodelname", default=None, type=str, help="Custom FMU name.") +@click.option("--load", default=None, multiple=True, help="Load Modelica packages.") +@click.option("--flags", default=None, multiple=True, help="Compiler flags.") +@click.option("--type", default="all", type=click.Choice(["all", "cs", "me", "csSolver"])) +@click.option("--version", "fmiVersion", default="2", type=click.Choice(["1", "2", "3"])) +@click.option("--backend", default="auto", type=click.Choice(["dymola", "openmodelica", "auto"])) +@click.option("--dymola", default="/opt/dymola-2025xRefresh1-x86_64/", type=click.Path()) +@click.option("--dymolapath", default="/usr/local/bin/dymola", type=click.Path()) +@click.option("--dymolawhl", default="Modelica/Library/python_interface/dymola-2025.1-py3-none-any.whl") +@click.option("-v", "--verbose", is_flag=True) +@click.option("-f", "--force", is_flag=True) +def mo2fmuLegacyCLI( + mo: Optional[str], + outdir: Optional[str], + check: bool, + fmumodelname: Optional[str], + load: tuple[str, ...], + flags: tuple[str, ...], + type: str, + fmiVersion: str, + backend: str, + dymola: str, + dymolapath: str, + dymolawhl: str, + verbose: bool, + force: bool, +) -> None: + """Legacy CLI for backward compatibility. - mo2fmu --load package.mo model.mo ./output + .. deprecated:: + Use 'mo2fmu compile' or 'mo2fmu check' instead. """ - mo2fmu( - mo, - outdir, - fmumodelname, - load, - flags, - type, - version, - dymola, # CLI parameter name stays as 'dymola' for backward compatibility - dymolapath, - dymolawhl, - verbose, - force, + warnings.warn( + "Legacy CLI style is deprecated. Use 'mo2fmu compile' or 'mo2fmu check' instead.", + DeprecationWarning, + stacklevel=2, + ) + + if check: + # Invoke check command + ctx = click.Context(checkCmd) + ctx.invoke(checkCmd, dymola=dymola, dymola_exec=dymolapath, dymola_whl=dymolawhl, asJson=False) + return + + if not mo or not outdir: + click.echo("Error: MO and OUTDIR arguments required.", err=True) + raise SystemExit(1) + + # Invoke compile command + ctx = click.Context(compileCmd) + ctx.invoke( + compileCmd, + mo=mo, + outdir=outdir, + name=fmumodelname, + load=load, + flags=flags, + fmiType=type, + fmi_version=fmiVersion, + backend=backend, + dymola=dymola, + dymola_exec=dymolapath, + dymola_whl=dymolawhl, + verbose=verbose, + force=force, ) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..d9d6d5f --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,74 @@ +"""Shared pytest fixtures for mo2fmu tests.""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + + +# ============================================================================= +# Model Directory Fixtures +# ============================================================================= + + +@pytest.fixture +def modelsDir() -> Path: + """Return the path to the shared test models directory.""" + return Path(__file__).parent / "fixtures" / "models" + + +# ============================================================================= +# Model File Fixtures +# ============================================================================= + + +@pytest.fixture +def simpleOdeModel(modelsDir: Path) -> Path: + """Return path to simple_ode.mo - a basic exponential decay ODE.""" + return modelsDir / "simple_ode.mo" + + +@pytest.fixture +def odeWithInputModel(modelsDir: Path) -> Path: + """Return path to ode_with_input.mo - an ODE with external input.""" + return modelsDir / "ode_with_input.mo" + + +@pytest.fixture +def odeSinusoidalModel(modelsDir: Path) -> Path: + """Return path to ode_sinusoidal.mo - an ODE with sinusoidal forcing.""" + return modelsDir / "ode_sinusoidal.mo" + + +@pytest.fixture +def multiStateModel(modelsDir: Path) -> Path: + """Return path to multi_state.mo - a model with multiple state variables.""" + return modelsDir / "multi_state.mo" + + +@pytest.fixture +def bouncingBallModel(modelsDir: Path) -> Path: + """Return path to bouncing_ball.mo - a model with events for FMI 3.0 testing.""" + return modelsDir / "bouncing_ball.mo" + + +# ============================================================================= +# Compiler Configuration Fixtures +# ============================================================================= + + +@pytest.fixture +def dymolaConfig(): + """Return default Dymola configuration from environment.""" + from feelpp.mo2fmu.compilers.dymola import DymolaConfig + + return DymolaConfig.from_env() + + +@pytest.fixture +def openModelicaConfig(): + """Return default OpenModelica configuration from environment.""" + from feelpp.mo2fmu.compilers.openmodelica import OpenModelicaConfig + + return OpenModelicaConfig.from_env() diff --git a/tests/fixtures/models/bouncing_ball.mo b/tests/fixtures/models/bouncing_ball.mo new file mode 100644 index 0000000..3aa1aa6 --- /dev/null +++ b/tests/fixtures/models/bouncing_ball.mo @@ -0,0 +1,33 @@ +model bouncing_ball + "A simple model for testing FMI 3.0 with event management" + + // Parameters + parameter Real e = 0.7 "Restitution coefficient (rebound elasticity)"; + parameter Real g = 9.81 "Gravity"; + + // Inputs + input Real wind_force(start = 0.0) "Vertical wind force (external)"; + + // States (Continuous variables) + Real h(start = 1.0, fixed=true) "Height (m)"; + Real v(start = 0.0, fixed=true) "Speed (m/s)"; + + // Outputs + output Real h_out "Height output for the log"; + output Integer bounce_count(start=0) "Rebound counter (Discrete Variable)"; + +equation + // Connect to the output + h_out = h; + + // Differential equations (Continuous system) + der(h) = v; + der(v) = -g + wind_force; + + // Event Management (Zero-Crossing) + when h <= 0.0 and v < 0.0 then + reinit(v, -e * v); // reversing speed results in energy loss + bounce_count = pre(bounce_count) + 1; // increment the counter + end when; + +end bouncing_ball; diff --git a/tests/fixtures/models/multi_state.mo b/tests/fixtures/models/multi_state.mo new file mode 100644 index 0000000..fe304bb --- /dev/null +++ b/tests/fixtures/models/multi_state.mo @@ -0,0 +1,10 @@ +model multi_state + "Model with multiple state variables for testing" + parameter Real k1 = 1.0 "Coefficient 1"; + parameter Real k2 = 2.0 "Coefficient 2"; + Real x(start = 1.0) "First state variable"; + Real y(start = 0.0) "Second state variable"; +equation + der(x) = -k1 * x + k2 * y; + der(y) = k1 * x - k2 * y; +end multi_state; diff --git a/tests/fixtures/models/ode_sinusoidal.mo b/tests/fixtures/models/ode_sinusoidal.mo new file mode 100644 index 0000000..73b3bff --- /dev/null +++ b/tests/fixtures/models/ode_sinusoidal.mo @@ -0,0 +1,7 @@ +model ode_sinusoidal + "ODE with a sinusoidal forcing term" + parameter Real omega = 1.0 "Frequency of the sinusoidal input"; + Real y(start = 0.0) "State variable"; +equation + der(y) = sin(omega * time); +end ode_sinusoidal; diff --git a/tests/fixtures/models/ode_with_input.mo b/tests/fixtures/models/ode_with_input.mo new file mode 100644 index 0000000..ae9736e --- /dev/null +++ b/tests/fixtures/models/ode_with_input.mo @@ -0,0 +1,8 @@ +model ode_with_input + "ODE with an external input for testing FMU with inputs" + parameter Real lambda = 2.0 "Decay coefficient"; + input Real u "External input"; + Real y(start = 1.0) "State variable"; +equation + der(y) = -lambda * y + u; +end ode_with_input; diff --git a/tests/fixtures/models/simple_ode.mo b/tests/fixtures/models/simple_ode.mo new file mode 100644 index 0000000..77959b4 --- /dev/null +++ b/tests/fixtures/models/simple_ode.mo @@ -0,0 +1,7 @@ +model simple_ode + "A simple exponential decay ODE for testing FMU generation" + parameter Real lambda = 2.0 "Decay coefficient"; + Real y(start = 1.0) "State variable"; +equation + der(y) = -lambda * y; +end simple_ode; diff --git a/tests/test_compilers.py b/tests/test_compilers.py new file mode 100644 index 0000000..78d9e8a --- /dev/null +++ b/tests/test_compilers.py @@ -0,0 +1,650 @@ +"""Tests for the compiler abstraction layer. + +This module tests the base classes, Dymola compiler, and OpenModelica compiler. +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from feelpp.mo2fmu.compilers.base import ( + CompilationConfig, + CompilationResult, + FMIType, + FMIVersion, + ModelicaModel, +) +from feelpp.mo2fmu.compilers.dymola import DymolaCompiler, DymolaConfig +from feelpp.mo2fmu.compilers.openmodelica import OpenModelicaCompiler, OpenModelicaConfig + + +# ============================================================================= +# Compiler Availability Checks +# ============================================================================= + + +def _check_omc_available() -> bool: + """Check if OpenModelica is available via OMPython or CLI.""" + compiler = OpenModelicaCompiler() + return compiler.is_available + + +HAS_OMC = _check_omc_available() + + +# ============================================================================= +# Test Data Classes +# ============================================================================= + + +class TestFMIType: + """Tests for FMIType enum.""" + + def test_from_string_valid(self) -> None: + """Test valid string conversions.""" + assert FMIType.from_string("me") == FMIType.MODEL_EXCHANGE + assert FMIType.from_string("cs") == FMIType.CO_SIMULATION + assert FMIType.from_string("all") == FMIType.BOTH + assert FMIType.from_string("csSolver") == FMIType.CO_SIMULATION_SOLVER + + def test_from_string_invalid(self) -> None: + """Test invalid string raises ValueError.""" + with pytest.raises(ValueError, match="Invalid FMI type"): + FMIType.from_string("invalid") + + +class TestFMIVersion: + """Tests for FMIVersion enum.""" + + def test_from_string_valid(self) -> None: + """Test valid string conversions.""" + assert FMIVersion.from_string("1") == FMIVersion.FMI_1_0 + assert FMIVersion.from_string("2") == FMIVersion.FMI_2_0 + assert FMIVersion.from_string("3") == FMIVersion.FMI_3_0 + + def test_from_string_invalid(self) -> None: + """Test invalid string raises ValueError.""" + with pytest.raises(ValueError, match="Invalid FMI version"): + FMIVersion.from_string("4") + + +class TestModelicaModel: + """Tests for ModelicaModel class.""" + + def test_simple_model(self, tmp_path: Path) -> None: + """Test creating a model from a simple .mo file.""" + mo_file = tmp_path / "simple.mo" + mo_file.write_text( + """model simple + Real x; +equation + der(x) = -x; +end simple; +""" + ) + + model = ModelicaModel(mo_file) + + assert model.path == mo_file + assert model.model_name == "simple" + assert model.package_name is None + assert model.fully_qualified_name == "simple" + + def test_model_with_package(self, tmp_path: Path) -> None: + """Test creating a model with a 'within' statement.""" + mo_file = tmp_path / "test_model.mo" + mo_file.write_text( + """within MyPackage.SubPackage; +model test_model + Real x; +equation + der(x) = -x; +end test_model; +""" + ) + + model = ModelicaModel(mo_file) + + assert model.path == mo_file + assert model.model_name == "test_model" + assert model.package_name == "MyPackage.SubPackage" + assert model.fully_qualified_name == "MyPackage.SubPackage.test_model" + + def test_model_explicit_name(self, tmp_path: Path) -> None: + """Test creating a model with explicit model name.""" + mo_file = tmp_path / "test.mo" + mo_file.write_text("model test end test;") + + model = ModelicaModel(mo_file, model_name="CustomName") + + assert model.model_name == "CustomName" + assert model.fully_qualified_name == "CustomName" + + +class TestCompilationConfig: + """Tests for CompilationConfig class.""" + + def test_default_values(self) -> None: + """Test default configuration values.""" + config = CompilationConfig() + + assert config.fmi_type == FMIType.BOTH + assert config.fmi_version == FMIVersion.FMI_2_0 + assert config.output_name is None + assert config.packages == [] + assert config.flags == [] + assert config.force is False + assert config.verbose is False + + def test_from_legacy(self) -> None: + """Test creating config from legacy parameters.""" + config = CompilationConfig.from_legacy( + type="cs", + version="2", + fmumodelname="MyModel", + load=("pkg1", "pkg2"), + flags=("-d=debug",), + force=True, + verbose=True, + ) + + assert config.fmi_type == FMIType.CO_SIMULATION + assert config.fmi_version == FMIVersion.FMI_2_0 + assert config.output_name == "MyModel" + assert config.packages == ["pkg1", "pkg2"] + assert config.flags == ["-d=debug"] + assert config.force is True + assert config.verbose is True + + +class TestCompilationResult: + """Tests for CompilationResult class.""" + + def test_success_result(self) -> None: + """Test successful compilation result.""" + result = CompilationResult( + success=True, + fmu_path=Path("/path/to/model.fmu"), + ) + + assert result.success is True + assert result.fmu_path == Path("/path/to/model.fmu") + assert result.error_message is None + + def test_failure_result(self) -> None: + """Test failed compilation result.""" + result = CompilationResult( + success=False, + error_message="Compilation failed", + log="Error details...", + ) + + assert result.success is False + assert result.fmu_path is None + assert result.error_message == "Compilation failed" + assert result.log == "Error details..." + + +# ============================================================================= +# Test Dymola Compiler +# ============================================================================= + + +class TestDymolaConfig: + """Tests for DymolaConfig class.""" + + def test_default_values(self) -> None: + """Test default configuration values.""" + config = DymolaConfig() + + assert config.root == "/opt/dymola-2025xRefresh1-x86_64/" + assert config.executable == "/usr/local/bin/dymola" + assert config.compile_64bit_only is True + assert config.enable_code_export is True + assert config.global_optimizations == 2 + assert config.linger_time == 0 + + def test_from_env(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Test creating config from environment variables.""" + monkeypatch.setenv("DYMOLA_ROOT", "/custom/dymola") + monkeypatch.setenv("DYMOLA_EXECUTABLE", "/custom/bin/dymola") + monkeypatch.setenv("DYMOLA_WHL", "custom/wheel.whl") + + config = DymolaConfig.from_env() + + assert config.root == "/custom/dymola" + assert config.executable == "/custom/bin/dymola" + assert config.wheel_path == "custom/wheel.whl" + + +class TestDymolaCompiler: + """Tests for DymolaCompiler class.""" + + def test_compiler_name(self) -> None: + """Test compiler name property.""" + compiler = DymolaCompiler() + assert compiler.name == "dymola" + + def test_availability_check(self) -> None: + """Test availability check with invalid path.""" + config = DymolaConfig(root="/nonexistent/path") + compiler = DymolaCompiler(config) + + assert compiler.is_available is False + + def test_compile_unavailable(self, tmp_path: Path) -> None: + """Test compilation when Dymola is not available.""" + config = DymolaConfig(root="/nonexistent/path") + compiler = DymolaCompiler(config) + + mo_file = tmp_path / "test.mo" + mo_file.write_text("model test end test;") + model = ModelicaModel(mo_file) + + result = compiler.compile(model, tmp_path / "output", CompilationConfig()) + + assert result.success is False + assert "not available" in result.error_message.lower() + + +# ============================================================================= +# Test OpenModelica Compiler +# ============================================================================= + + +class TestOpenModelicaConfig: + """Tests for OpenModelicaConfig class.""" + + def test_default_values(self) -> None: + """Test default configuration values.""" + config = OpenModelicaConfig() + + assert config.omc_path is None + assert config.ompython_session is True + assert config.target_platform == "static" + assert config.debug is False + assert config.num_procs == 1 + + def test_from_env(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Test creating config from environment variables.""" + monkeypatch.setenv("OPENMODELICA_HOME", "/custom/omc") + monkeypatch.setenv("CXX", "clang++") + monkeypatch.setenv("CC", "clang") + + config = OpenModelicaConfig.from_env() + + assert config.omc_path == "/custom/omc" + assert config.cpp_compiler == "clang++" + assert config.c_compiler == "clang" + + +class TestOpenModelicaCompiler: + """Tests for OpenModelicaCompiler class.""" + + def test_compiler_name(self) -> None: + """Test compiler name property.""" + compiler = OpenModelicaCompiler() + assert compiler.name == "openmodelica" + + @pytest.mark.skipif(not HAS_OMC, reason="OpenModelica not available") + def test_compile_output_same_as_cwd(self, tmp_path: Path) -> None: + """Test that compilation fails when output dir equals cwd.""" + import os + + compiler = OpenModelicaCompiler() + + mo_file = tmp_path / "test.mo" + mo_file.write_text("model test end test;") + model = ModelicaModel(mo_file) + + # Save current directory + original_cwd = os.getcwd() + + try: + os.chdir(tmp_path) + result = compiler.compile(model, tmp_path, CompilationConfig()) + + assert result.success is False + assert "must differ" in result.error_message.lower() + finally: + os.chdir(original_cwd) + + @pytest.mark.skipif(not HAS_OMC, reason="OpenModelica not available") + def test_compile_force_false_existing_fmu(self, tmp_path: Path) -> None: + """Test that compilation fails when FMU exists and force=False.""" + compiler = OpenModelicaCompiler() + + mo_file = tmp_path / "test.mo" + mo_file.write_text("model test end test;") + model = ModelicaModel(mo_file) + + output_dir = tmp_path / "output" + output_dir.mkdir() + + # Create existing FMU + existing_fmu = output_dir / "test.fmu" + existing_fmu.write_text("dummy") + + config = CompilationConfig(force=False) + result = compiler.compile(model, output_dir, config) + + assert result.success is False + assert "exists" in result.error_message.lower() + + +# ============================================================================= +# Integration Tests (require actual compilers) +# ============================================================================= + + +@pytest.mark.skipif(not HAS_OMC, reason="OpenModelica not available") +class TestOpenModelicaIntegration: + """Integration tests for OpenModelica compiler.""" + + def test_version(self) -> None: + """Test getting OpenModelica version.""" + compiler = OpenModelicaCompiler() + version = compiler.get_version() + + assert version is not None + # Version should contain a number + assert any(c.isdigit() for c in version) + + def test_check_model(self, simpleOdeModel: Path) -> None: + """Test checking a simple model.""" + compiler = OpenModelicaCompiler() + model = ModelicaModel(simpleOdeModel) + + result = compiler.check_model(model) + # Note: checkModel might return True even with warnings + assert result is True or result is False # Just ensure it runs + + def test_compile_simple_model(self, simpleOdeModel: Path, tmp_path: Path) -> None: + """Test compiling a simple model to FMU.""" + compiler = OpenModelicaCompiler() + model = ModelicaModel(simpleOdeModel) + + output_dir = tmp_path / "output" + config = CompilationConfig( + fmi_type=FMIType.CO_SIMULATION, + fmi_version=FMIVersion.FMI_2_0, + verbose=True, + force=True, + ) + + result = compiler.compile(model, output_dir, config) + + if result.success: + assert result.fmu_path is not None + assert result.fmu_path.exists() + assert result.fmu_path.suffix == ".fmu" + else: + # OpenModelica might fail on some systems - just check result is valid + assert result.error_message is not None + + def test_compile_model_exchange(self, simpleOdeModel: Path, tmp_path: Path) -> None: + """Test compiling with Model Exchange type.""" + compiler = OpenModelicaCompiler() + model = ModelicaModel(simpleOdeModel) + + output_dir = tmp_path / "output_me" + config = CompilationConfig( + fmi_type=FMIType.MODEL_EXCHANGE, + fmi_version=FMIVersion.FMI_2_0, + verbose=True, + force=True, + ) + + result = compiler.compile(model, output_dir, config) + + # Just verify it produces a valid result structure + assert isinstance(result, CompilationResult) + assert isinstance(result.success, bool) + + def test_compile_fmi3_cosimulation(self, simpleOdeModel: Path, tmp_path: Path) -> None: + """Test compiling a simple model to FMI 3.0 Co-Simulation FMU.""" + compiler = OpenModelicaCompiler() + model = ModelicaModel(simpleOdeModel) + + output_dir = tmp_path / "output_fmi3_cs" + config = CompilationConfig( + fmi_type=FMIType.CO_SIMULATION, + fmi_version=FMIVersion.FMI_3_0, + verbose=True, + force=True, + ) + + result = compiler.compile(model, output_dir, config) + + # FMI 3.0 requires OpenModelica 1.21+, so this may fail on older versions + # Just verify the result structure is valid + assert isinstance(result, CompilationResult) + assert isinstance(result.success, bool) + if result.success: + assert result.fmu_path is not None + assert result.fmu_path.exists() + assert result.fmu_path.suffix == ".fmu" + + def test_compile_fmi3_model_exchange(self, simpleOdeModel: Path, tmp_path: Path) -> None: + """Test compiling a simple model to FMI 3.0 Model Exchange FMU.""" + compiler = OpenModelicaCompiler() + model = ModelicaModel(simpleOdeModel) + + output_dir = tmp_path / "output_fmi3_me" + config = CompilationConfig( + fmi_type=FMIType.MODEL_EXCHANGE, + fmi_version=FMIVersion.FMI_3_0, + verbose=True, + force=True, + ) + + result = compiler.compile(model, output_dir, config) + + # FMI 3.0 requires OpenModelica 1.21+, so this may fail on older versions + # Just verify the result structure is valid + assert isinstance(result, CompilationResult) + assert isinstance(result.success, bool) + if result.success: + assert result.fmu_path is not None + assert result.fmu_path.exists() + assert result.fmu_path.suffix == ".fmu" + + def test_compile_fmi3_both(self, simpleOdeModel: Path, tmp_path: Path) -> None: + """Test compiling a simple model to FMI 3.0 with both ME and CS.""" + compiler = OpenModelicaCompiler() + model = ModelicaModel(simpleOdeModel) + + output_dir = tmp_path / "output_fmi3_both" + config = CompilationConfig( + fmi_type=FMIType.BOTH, + fmi_version=FMIVersion.FMI_3_0, + verbose=True, + force=True, + ) + + result = compiler.compile(model, output_dir, config) + + # FMI 3.0 requires OpenModelica 1.21+, so this may fail on older versions + # Just verify the result structure is valid + assert isinstance(result, CompilationResult) + assert isinstance(result.success, bool) + if result.success: + assert result.fmu_path is not None + assert result.fmu_path.exists() + assert result.fmu_path.suffix == ".fmu" + + +# Check if Dymola is available +def _check_dymola_available() -> bool: + """Check if Dymola is available.""" + import os + + dymola_root = os.getenv("DYMOLA_ROOT", "/opt/dymola-2025xRefresh1-x86_64/") + dymola_whl = os.getenv( + "DYMOLA_WHL", "Modelica/Library/python_interface/dymola-2025.1-py3-none-any.whl" + ) + return (Path(dymola_root) / dymola_whl).is_file() + + +HAS_DYMOLA = _check_dymola_available() + + +@pytest.mark.skipif(not HAS_DYMOLA, reason="Dymola not available") +class TestDymolaIntegration: + """Integration tests for Dymola compiler.""" + + def test_version(self) -> None: + """Test getting Dymola version.""" + compiler = DymolaCompiler() + version = compiler.get_version() + + # Version might be None if we can't parse it + assert version is None or isinstance(version, str) + + def test_compile_simple_model(self, simpleOdeModel: Path, tmp_path: Path) -> None: + """Test compiling a simple model to FMU.""" + compiler = DymolaCompiler() + model = ModelicaModel(simpleOdeModel) + + output_dir = tmp_path / "output" + config = CompilationConfig( + fmi_type=FMIType.CO_SIMULATION, + fmi_version=FMIVersion.FMI_2_0, + verbose=True, + force=True, + ) + + result = compiler.compile(model, output_dir, config) + + if result.success: + assert result.fmu_path is not None + assert result.fmu_path.exists() + assert result.fmu_path.suffix == ".fmu" + + def test_compile_fmi3_cosimulation(self, simpleOdeModel: Path, tmp_path: Path) -> None: + """Test compiling a simple model to FMI 3.0 Co-Simulation FMU.""" + compiler = DymolaCompiler() + model = ModelicaModel(simpleOdeModel) + + output_dir = tmp_path / "output_fmi3_cs" + config = CompilationConfig( + fmi_type=FMIType.CO_SIMULATION, + fmi_version=FMIVersion.FMI_3_0, + verbose=True, + force=True, + ) + + result = compiler.compile(model, output_dir, config) + + # FMI 3.0 requires Dymola 2024+, so this may fail on older versions + # Just verify the result structure is valid + assert isinstance(result, CompilationResult) + assert isinstance(result.success, bool) + if result.success: + assert result.fmu_path is not None + assert result.fmu_path.exists() + assert result.fmu_path.suffix == ".fmu" + + def test_compile_fmi3_model_exchange(self, simpleOdeModel: Path, tmp_path: Path) -> None: + """Test compiling a simple model to FMI 3.0 Model Exchange FMU.""" + compiler = DymolaCompiler() + model = ModelicaModel(simpleOdeModel) + + output_dir = tmp_path / "output_fmi3_me" + config = CompilationConfig( + fmi_type=FMIType.MODEL_EXCHANGE, + fmi_version=FMIVersion.FMI_3_0, + verbose=True, + force=True, + ) + + result = compiler.compile(model, output_dir, config) + + # FMI 3.0 requires Dymola 2024+, so this may fail on older versions + # Just verify the result structure is valid + assert isinstance(result, CompilationResult) + assert isinstance(result.success, bool) + if result.success: + assert result.fmu_path is not None + assert result.fmu_path.exists() + assert result.fmu_path.suffix == ".fmu" + + def test_compile_fmi3_both(self, simpleOdeModel: Path, tmp_path: Path) -> None: + """Test compiling a simple model to FMI 3.0 with both ME and CS.""" + compiler = DymolaCompiler() + model = ModelicaModel(simpleOdeModel) + + output_dir = tmp_path / "output_fmi3_both" + config = CompilationConfig( + fmi_type=FMIType.BOTH, + fmi_version=FMIVersion.FMI_3_0, + verbose=True, + force=True, + ) + + result = compiler.compile(model, output_dir, config) + + # FMI 3.0 requires Dymola 2024+, so this may fail on older versions + # Just verify the result structure is valid + assert isinstance(result, CompilationResult) + assert isinstance(result.success, bool) + if result.success: + assert result.fmu_path is not None + assert result.fmu_path.exists() + assert result.fmu_path.suffix == ".fmu" + + +# ============================================================================= +# Test get_compiler function +# ============================================================================= + + +class TestGetCompilerFunction: + """Tests for get_compiler function.""" + + def test_auto_no_compilers(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Test auto mode when no compilers are available.""" + from feelpp.mo2fmu import getCompiler + + # Set invalid paths + monkeypatch.setenv("DYMOLA_ROOT", "/nonexistent/dymola") + monkeypatch.setenv("OPENMODELICA_HOME", "/nonexistent/omc") + + # Force reload of config + dymolaConfig = DymolaConfig(root="/nonexistent/dymola") + omcConfig = OpenModelicaConfig(omc_path="/nonexistent/omc") + + with pytest.raises(RuntimeError, match="No Modelica compiler available"): + getCompiler( + backend="auto", + dymolaConfig=dymolaConfig, + openModelicaConfig=omcConfig, + ) + + def test_explicit_dymola_unavailable(self) -> None: + """Test requesting Dymola when not available.""" + from feelpp.mo2fmu import getCompiler + + dymolaConfig = DymolaConfig(root="/nonexistent/dymola") + + with pytest.raises(RuntimeError, match="Dymola is not available"): + getCompiler(backend="dymola", dymolaConfig=dymolaConfig) + + def test_explicit_openmodelica_unavailable(self) -> None: + """Test requesting OpenModelica when not available.""" + from feelpp.mo2fmu import getCompiler + + omcConfig = OpenModelicaConfig(omc_path="/nonexistent/omc") + # Also need to ensure OMPython is not available + # This test assumes OMPython is not installed + + # Create a compiler that definitely won't find omc + compiler = OpenModelicaCompiler(omcConfig) + if not compiler.is_available: + with pytest.raises(RuntimeError, match="OpenModelica is not available"): + getCompiler(backend="openmodelica", openModelicaConfig=omcConfig) + else: + # OMPython is available, so this test passes + assert True diff --git a/tests/test_fmu_simulation.py b/tests/test_fmu_simulation.py new file mode 100644 index 0000000..ff88ac2 --- /dev/null +++ b/tests/test_fmu_simulation.py @@ -0,0 +1,691 @@ +"""Tests for FMU simulation using FMPy. + +This module tests that generated FMUs can be simulated using FMPy with +the CVODE solver (Sundials). This is particularly important for Model Exchange +FMUs which require an external solver. +""" + +from __future__ import annotations + +import os +from pathlib import Path + +import pytest + +from feelpp.mo2fmu import compileFmu +from feelpp.mo2fmu.compilers.base import CompilationResult +from feelpp.mo2fmu.compilers.dymola import DymolaConfig + + +# Check if FMPy is available +def _check_fmpy_available() -> bool: + """Check if FMPy is available.""" + try: + import fmpy # noqa: F401 + + return True + except ImportError: + return False + + +HAS_FMPY = _check_fmpy_available() + + +# Check if Dymola is available +DYMOLA_PATH = os.getenv("DYMOLA_ROOT", "/opt/dymola-2025xRefresh1-x86_64/") +DYMOLA_EXECUTABLE = os.getenv("DYMOLA_EXECUTABLE", "/usr/local/bin/dymola") +DYMOLA_WHL = os.getenv( + "DYMOLA_WHL", "Modelica/Library/python_interface/dymola-2025.1-py3-none-any.whl" +) +HAS_DYMOLA = (Path(DYMOLA_PATH) / DYMOLA_WHL).is_file() + + +def _get_dymola_config() -> DymolaConfig: + """Get Dymola configuration.""" + return DymolaConfig( + root=DYMOLA_PATH, + executable=DYMOLA_EXECUTABLE, + wheel_path=DYMOLA_WHL, + ) + + +# ============================================================================= +# FMPy Simulation Tests +# ============================================================================= + + +@pytest.mark.skipif(not HAS_FMPY, reason="FMPy not available") +@pytest.mark.skipif(not HAS_DYMOLA, reason="Dymola not available") +class TestFmpySimulation: + """Tests for FMU simulation using FMPy with CVODE solver.""" + + def test_simulate_cosimulation_fmu( + self, simpleOdeModel: Path, tmp_path: Path + ) -> None: + """Test simulating a Co-Simulation FMU with FMPy.""" + from fmpy import simulate_fmu + + # Compile the model to Co-Simulation FMU + outdir = tmp_path / "output_cs" + result = compileFmu( + mo=simpleOdeModel, + outdir=outdir, + backend="dymola", + fmiType="cs", + fmiVersion="2", + verbose=True, + force=True, + dymolaConfig=_get_dymola_config(), + ) + + assert result.success, f"Compilation failed: {result.error_message}" + assert result.fmu_path is not None + assert result.fmu_path.exists() + + # Simulate the FMU + sim_result = simulate_fmu( + str(result.fmu_path), + start_time=0.0, + stop_time=1.0, + output_interval=0.01, + ) + + # Verify simulation produced results + assert sim_result is not None + assert len(sim_result) > 0 + assert "time" in sim_result.dtype.names + assert "y" in sim_result.dtype.names + + # Check that the solution decays (y starts at 1, decays with lambda=2) + y_values = sim_result["y"] + assert y_values[0] > 0.9 # Initial value close to 1 + assert y_values[-1] < y_values[0] # Decayed + + def test_simulate_model_exchange_fmu( + self, simpleOdeModel: Path, tmp_path: Path + ) -> None: + """Test simulating a Model Exchange FMU with FMPy using CVODE solver.""" + from fmpy import simulate_fmu + + # Compile the model to Model Exchange FMU + outdir = tmp_path / "output_me" + result = compileFmu( + mo=simpleOdeModel, + outdir=outdir, + backend="dymola", + fmiType="me", + fmiVersion="2", + verbose=True, + force=True, + dymolaConfig=_get_dymola_config(), + ) + + assert result.success, f"Compilation failed: {result.error_message}" + assert result.fmu_path is not None + assert result.fmu_path.exists() + + # Simulate the FMU with Model Exchange (uses CVODE solver internally) + sim_result = simulate_fmu( + str(result.fmu_path), + start_time=0.0, + stop_time=1.0, + output_interval=0.01, + fmi_type="ModelExchange", # Explicitly use Model Exchange + ) + + # Verify simulation produced results + assert sim_result is not None + assert len(sim_result) > 0 + assert "time" in sim_result.dtype.names + assert "y" in sim_result.dtype.names + + # Check that the solution decays correctly + y_values = sim_result["y"] + time_values = sim_result["time"] + + # Initial value should be close to 1 + assert abs(y_values[0] - 1.0) < 0.01 + + # Final value should be close to exp(-2*1) ≈ 0.135 (lambda=2, t=1) + import math + + expected_final = math.exp(-2.0 * 1.0) + assert abs(y_values[-1] - expected_final) < 0.01 + + def test_simulate_model_exchange_sinusoidal( + self, odeSinusoidalModel: Path, tmp_path: Path + ) -> None: + """Test simulating sinusoidal ODE as Model Exchange with CVODE.""" + from fmpy import simulate_fmu + + # Compile to Model Exchange + outdir = tmp_path / "output_sin_me" + result = compileFmu( + mo=odeSinusoidalModel, + outdir=outdir, + backend="dymola", + fmiType="me", + fmiVersion="2", + verbose=True, + force=True, + dymolaConfig=_get_dymola_config(), + ) + + assert result.success, f"Compilation failed: {result.error_message}" + + # Simulate with Model Exchange + sim_result = simulate_fmu( + str(result.fmu_path), + start_time=0.0, + stop_time=6.28, # One full period (2*pi) + output_interval=0.1, + fmi_type="ModelExchange", + ) + + assert sim_result is not None + assert len(sim_result) > 0 + assert "y" in sim_result.dtype.names + + # The integral of sin(t) from 0 to 2*pi is 0 + # y(t) = integral of sin(omega*t) with omega=1, y(0)=0 + # At t=2*pi, y should be close to 0 + y_final = sim_result["y"][-1] + assert abs(y_final) < 0.1 # Should be close to 0 + + def test_compare_cs_and_me_results( + self, simpleOdeModel: Path, tmp_path: Path + ) -> None: + """Test that Co-Simulation and Model Exchange produce similar results.""" + from fmpy import simulate_fmu + import numpy as np + + # Compile Co-Simulation FMU + outdir_cs = tmp_path / "output_cs" + result_cs = compileFmu( + mo=simpleOdeModel, + outdir=outdir_cs, + backend="dymola", + fmiType="cs", + fmiVersion="2", + verbose=True, + force=True, + dymolaConfig=_get_dymola_config(), + ) + assert result_cs.success + + # Compile Model Exchange FMU + outdir_me = tmp_path / "output_me" + result_me = compileFmu( + mo=simpleOdeModel, + outdir=outdir_me, + backend="dymola", + fmiType="me", + fmiVersion="2", + verbose=True, + force=True, + dymolaConfig=_get_dymola_config(), + ) + assert result_me.success + + # Simulate both + sim_cs = simulate_fmu( + str(result_cs.fmu_path), + start_time=0.0, + stop_time=1.0, + output_interval=0.1, + ) + + sim_me = simulate_fmu( + str(result_me.fmu_path), + start_time=0.0, + stop_time=1.0, + output_interval=0.1, + fmi_type="ModelExchange", + ) + + # Compare results - they should be very close + y_cs = sim_cs["y"] + y_me = sim_me["y"] + + # Allow small differences due to different solvers + max_diff = np.max(np.abs(y_cs - y_me)) + assert max_diff < 0.01, f"Max difference between CS and ME: {max_diff}" + + +@pytest.mark.skipif(not HAS_FMPY, reason="FMPy not available") +@pytest.mark.skipif(not HAS_DYMOLA, reason="Dymola not available") +class TestFmpyFmi3Simulation: + """Tests for FMI 3.0 FMU simulation using FMPy.""" + + def test_simulate_fmi3_model_exchange( + self, simpleOdeModel: Path, tmp_path: Path + ) -> None: + """Test simulating an FMI 3.0 Model Exchange FMU.""" + from fmpy import simulate_fmu + + # Compile to FMI 3.0 Model Exchange + outdir = tmp_path / "output_fmi3_me" + result = compileFmu( + mo=simpleOdeModel, + outdir=outdir, + backend="dymola", + fmiType="me", + fmiVersion="3", + verbose=True, + force=True, + dymolaConfig=_get_dymola_config(), + ) + + # FMI 3.0 may not be supported by all Dymola versions + if not result.success: + pytest.skip("FMI 3.0 not supported by current Dymola version") + + assert result.fmu_path is not None + assert result.fmu_path.exists() + + # Try to simulate - FMPy 0.3+ supports FMI 3.0 + try: + sim_result = simulate_fmu( + str(result.fmu_path), + start_time=0.0, + stop_time=1.0, + output_interval=0.01, + fmi_type="ModelExchange", + ) + + assert sim_result is not None + assert len(sim_result) > 0 + except Exception as e: + # FMPy might not fully support FMI 3.0 yet + pytest.skip(f"FMPy FMI 3.0 simulation failed: {e}") + + def test_simulate_fmi3_cosimulation( + self, simpleOdeModel: Path, tmp_path: Path + ) -> None: + """Test simulating an FMI 3.0 Co-Simulation FMU.""" + from fmpy import simulate_fmu + + # Compile to FMI 3.0 Co-Simulation + outdir = tmp_path / "output_fmi3_cs" + result = compileFmu( + mo=simpleOdeModel, + outdir=outdir, + backend="dymola", + fmiType="cs", + fmiVersion="3", + verbose=True, + force=True, + dymolaConfig=_get_dymola_config(), + ) + + # FMI 3.0 may not be supported + if not result.success: + pytest.skip("FMI 3.0 not supported by current Dymola version") + + assert result.fmu_path is not None + + try: + sim_result = simulate_fmu( + str(result.fmu_path), + start_time=0.0, + stop_time=1.0, + output_interval=0.01, + ) + + assert sim_result is not None + assert len(sim_result) > 0 + except Exception as e: + pytest.skip(f"FMPy FMI 3.0 simulation failed: {e}") + + +@pytest.mark.skipif(not HAS_FMPY, reason="FMPy not available") +class TestFmpyValidation: + """Tests for FMU validation using FMPy.""" + + @pytest.mark.skipif(not HAS_DYMOLA, reason="Dymola not available") + def test_validate_fmu(self, simpleOdeModel: Path, tmp_path: Path) -> None: + """Test FMU validation using FMPy.""" + from fmpy import read_model_description + from fmpy.validation import validate_fmu + + # Compile the model + outdir = tmp_path / "output" + result = compileFmu( + mo=simpleOdeModel, + outdir=outdir, + backend="dymola", + fmiType="cs", + fmiVersion="2", + verbose=True, + force=True, + dymolaConfig=_get_dymola_config(), + ) + + assert result.success + assert result.fmu_path is not None + + # Validate the FMU + problems = validate_fmu(str(result.fmu_path)) + + # Should have no critical problems + critical_problems = [p for p in problems if "Error" in str(p)] + assert len(critical_problems) == 0, f"FMU validation errors: {critical_problems}" + + # Read model description + model_desc = read_model_description(str(result.fmu_path)) + assert model_desc is not None + assert model_desc.modelName is not None + + @pytest.mark.skipif(not HAS_DYMOLA, reason="Dymola not available") + def test_read_model_variables(self, simpleOdeModel: Path, tmp_path: Path) -> None: + """Test reading model variables from FMU.""" + from fmpy import read_model_description + + # Compile the model + outdir = tmp_path / "output" + result = compileFmu( + mo=simpleOdeModel, + outdir=outdir, + backend="dymola", + fmiType="me", + fmiVersion="2", + verbose=True, + force=True, + dymolaConfig=_get_dymola_config(), + ) + + assert result.success + + # Read model description + model_desc = read_model_description(str(result.fmu_path)) + + # Check that expected variables exist + var_names = [v.name for v in model_desc.modelVariables] + assert "y" in var_names # State variable + assert "lambda" in var_names # Parameter + + +# ============================================================================= +# Bouncing Ball Tests (Event Handling / FMI 3.0) +# ============================================================================= + + +@pytest.mark.skipif(not HAS_FMPY, reason="FMPy not available") +@pytest.mark.skipif(not HAS_DYMOLA, reason="Dymola not available") +class TestBouncingBallSimulation: + """Tests for bouncing ball model with events using FMPy. + + The bouncing ball model tests: + - Event handling (zero-crossing when h <= 0) + - Discrete variables (bounce_count) + - State reinit (velocity reversal on bounce) + - Input variables (wind_force) + """ + + def test_compile_bouncing_ball_fmi2( + self, bouncingBallModel: Path, tmp_path: Path + ) -> None: + """Test compiling bouncing ball to FMI 2.0.""" + outdir = tmp_path / "output_bb_fmi2" + result = compileFmu( + mo=bouncingBallModel, + outdir=outdir, + backend="dymola", + fmiType="me", + fmiVersion="2", + verbose=True, + force=True, + dymolaConfig=_get_dymola_config(), + ) + + assert result.success, f"Compilation failed: {result.error_message}" + assert result.fmu_path is not None + assert result.fmu_path.exists() + + def test_simulate_bouncing_ball_cosimulation( + self, bouncingBallModel: Path, tmp_path: Path + ) -> None: + """Test simulating bouncing ball as Co-Simulation FMU.""" + from fmpy import simulate_fmu + + # Compile to Co-Simulation + outdir = tmp_path / "output_bb_cs" + result = compileFmu( + mo=bouncingBallModel, + outdir=outdir, + backend="dymola", + fmiType="cs", + fmiVersion="2", + verbose=True, + force=True, + dymolaConfig=_get_dymola_config(), + ) + + assert result.success, f"Compilation failed: {result.error_message}" + + # Simulate for 3 seconds (should see multiple bounces) + sim_result = simulate_fmu( + str(result.fmu_path), + start_time=0.0, + stop_time=3.0, + output_interval=0.01, + ) + + assert sim_result is not None + assert len(sim_result) > 0 + + # Check expected outputs exist + assert "time" in sim_result.dtype.names + assert "h_out" in sim_result.dtype.names or "h" in sim_result.dtype.names + + # Height should start at 1.0 + h_var = "h_out" if "h_out" in sim_result.dtype.names else "h" + h_values = sim_result[h_var] + assert abs(h_values[0] - 1.0) < 0.01 + + def test_simulate_bouncing_ball_model_exchange( + self, bouncingBallModel: Path, tmp_path: Path + ) -> None: + """Test simulating bouncing ball as Model Exchange with CVODE solver. + + Note: Event handling in Model Exchange mode depends on the external solver. + FMPy's CVODE may not perfectly handle state events (zero-crossings), + so we verify the simulation runs and produces reasonable results. + """ + from fmpy import simulate_fmu + + # Compile to Model Exchange + outdir = tmp_path / "output_bb_me" + result = compileFmu( + mo=bouncingBallModel, + outdir=outdir, + backend="dymola", + fmiType="me", + fmiVersion="2", + verbose=True, + force=True, + dymolaConfig=_get_dymola_config(), + ) + + assert result.success, f"Compilation failed: {result.error_message}" + + # Simulate with Model Exchange (CVODE handles the events) + sim_result = simulate_fmu( + str(result.fmu_path), + start_time=0.0, + stop_time=3.0, + output_interval=0.01, + fmi_type="ModelExchange", + ) + + assert sim_result is not None + assert len(sim_result) > 0 + + # Verify the simulation ran and produced output + h_var = "h_out" if "h_out" in sim_result.dtype.names else "h" + assert h_var in sim_result.dtype.names + h_values = sim_result[h_var] + + # Check initial condition + assert abs(h_values[0] - 1.0) < 0.01 # Should start at h=1 + + # Note: CVODE in FMPy may not handle state events perfectly, + # so we just verify the simulation completes and bounce_count increases + if "bounce_count" in sim_result.dtype.names: + bounce_counts = sim_result["bounce_count"] + # Should detect at least some bounces + assert max(bounce_counts) >= 1, "No bounces detected in ME simulation" + + def test_bouncing_ball_bounce_count( + self, bouncingBallModel: Path, tmp_path: Path + ) -> None: + """Test that bounce counter increments correctly.""" + from fmpy import simulate_fmu + + # Compile to Co-Simulation (more reliable for event handling) + outdir = tmp_path / "output_bb_count" + result = compileFmu( + mo=bouncingBallModel, + outdir=outdir, + backend="dymola", + fmiType="cs", + fmiVersion="2", + verbose=True, + force=True, + dymolaConfig=_get_dymola_config(), + ) + + assert result.success + + # Simulate long enough for multiple bounces + sim_result = simulate_fmu( + str(result.fmu_path), + start_time=0.0, + stop_time=5.0, + output_interval=0.1, + ) + + # Check if bounce_count exists and increases + if "bounce_count" in sim_result.dtype.names: + bounce_counts = sim_result["bounce_count"] + # Should have at least one bounce in 5 seconds + assert max(bounce_counts) >= 1 + + def test_bouncing_ball_fmi3_model_exchange( + self, bouncingBallModel: Path, tmp_path: Path + ) -> None: + """Test bouncing ball with FMI 3.0 Model Exchange - tests event handling.""" + from fmpy import simulate_fmu + + # Compile to FMI 3.0 Model Exchange + outdir = tmp_path / "output_bb_fmi3_me" + result = compileFmu( + mo=bouncingBallModel, + outdir=outdir, + backend="dymola", + fmiType="me", + fmiVersion="3", + verbose=True, + force=True, + dymolaConfig=_get_dymola_config(), + ) + + # FMI 3.0 may not be supported + if not result.success: + pytest.skip("FMI 3.0 not supported by current Dymola version") + + assert result.fmu_path is not None + assert result.fmu_path.exists() + + # Try to simulate with FMI 3.0 + try: + sim_result = simulate_fmu( + str(result.fmu_path), + start_time=0.0, + stop_time=3.0, + output_interval=0.01, + fmi_type="ModelExchange", + ) + + assert sim_result is not None + assert len(sim_result) > 0 + + # Verify physics - ball should bounce and height stay >= 0 + h_var = "h_out" if "h_out" in sim_result.dtype.names else "h" + if h_var in sim_result.dtype.names: + h_values = sim_result[h_var] + assert min(h_values) >= -0.01 + + except Exception as e: + pytest.skip(f"FMPy FMI 3.0 event simulation failed: {e}") + + def test_bouncing_ball_fmi3_cosimulation( + self, bouncingBallModel: Path, tmp_path: Path + ) -> None: + """Test bouncing ball with FMI 3.0 Co-Simulation.""" + from fmpy import simulate_fmu + + # Compile to FMI 3.0 Co-Simulation + outdir = tmp_path / "output_bb_fmi3_cs" + result = compileFmu( + mo=bouncingBallModel, + outdir=outdir, + backend="dymola", + fmiType="cs", + fmiVersion="3", + verbose=True, + force=True, + dymolaConfig=_get_dymola_config(), + ) + + # FMI 3.0 may not be supported + if not result.success: + pytest.skip("FMI 3.0 not supported by current Dymola version") + + assert result.fmu_path is not None + + try: + sim_result = simulate_fmu( + str(result.fmu_path), + start_time=0.0, + stop_time=3.0, + output_interval=0.01, + ) + + assert sim_result is not None + assert len(sim_result) > 0 + + except Exception as e: + pytest.skip(f"FMPy FMI 3.0 Co-Simulation failed: {e}") + + def test_bouncing_ball_with_wind_input( + self, bouncingBallModel: Path, tmp_path: Path + ) -> None: + """Test bouncing ball with wind_force input variable.""" + from fmpy import read_model_description + + # Compile to Model Exchange + outdir = tmp_path / "output_bb_wind" + result = compileFmu( + mo=bouncingBallModel, + outdir=outdir, + backend="dymola", + fmiType="me", + fmiVersion="2", + verbose=True, + force=True, + dymolaConfig=_get_dymola_config(), + ) + + assert result.success + + # Read model description to verify input exists + model_desc = read_model_description(str(result.fmu_path)) + var_names = [v.name for v in model_desc.modelVariables] + + # Verify expected variables + assert "h" in var_names or "h_out" in var_names # Height output + assert "v" in var_names # Velocity state + assert "wind_force" in var_names # Input variable + assert "e" in var_names # Restitution parameter + assert "g" in var_names # Gravity parameter diff --git a/tests/test_mo2fmu.py b/tests/test_mo2fmu.py index 28b1361..265ab6a 100644 --- a/tests/test_mo2fmu.py +++ b/tests/test_mo2fmu.py @@ -1,3 +1,5 @@ +"""Tests for the mo2fmu CLI and Python API.""" + from __future__ import annotations import os @@ -6,35 +8,36 @@ import pytest from xvfbwrapper import Xvfb -from feelpp.mo2fmu.mo2fmu import mo2fmu +from feelpp.mo2fmu import compileFmu +from feelpp.mo2fmu.compilers.dymola import DymolaConfig -def checkFmuFileExist(fmuPath, outdir): +def checkFmuFileExist(fmuPath: Path, outdir: Path) -> None: """Check if FMU file exists. Parameters ---------- - fmuPath: path + fmuPath: Path path of the fmu file - outdir: path + outdir: Path path of the output file directory. """ assert fmuPath.exists(), f"FMU file {fmuPath} was not created." print(f"FMU file created at: {fmuPath}") -def checkFmuValidity(fmuPath, fmuModel, dymolapath): +def checkFmuValidity(fmuPath: Path, fmuModel: str, dymolapath: str) -> None: """Check that the fmu model has the same number of unknowns and equations. Also verifies that it can be simulated. Parameters ---------- - fmuPath: path + fmuPath: Path path of the fmu file fmuModel: str name of the fmu model in Dymola - dymolapath: path + dymolapath: str path of the dymola application """ # launch a display server (needed to launch Dymola) @@ -63,30 +66,38 @@ def checkFmuValidity(fmuPath, fmuModel, dymolapath): assert result, f"FMU file {fmuPath} isn't valid, see the log above." -@pytest.mark.parametrize( - "modelPath, outdirPath", - [(Path("src/cases/ode_exp.mo"), Path("src")), (Path("src/cases/ode_sin.mo"), Path("src"))], -) -def test_pathExists(modelPath, outdirPath): - """Test if path of the modelica model and the output directory exist. +# ============================================================================= +# Path Existence Tests +# ============================================================================= + + +def test_fixturesExist(modelsDir: Path) -> None: + """Test if the shared fixtures directory exists.""" + assert modelsDir.exists(), f"Models directory {modelsDir} does not exist" + assert modelsDir.is_dir(), f"{modelsDir} is not a directory" + + +def test_simpleOdeModelExists(simpleOdeModel: Path) -> None: + """Test if the simple ODE model exists.""" + assert simpleOdeModel.exists(), f"Model {simpleOdeModel} does not exist" + print(f"Model path: {simpleOdeModel}") - Parameters - ---------- - modelPath: path - path of modelica model to convert into fmu - outdirPath: path - path of the output directory to find fmu file after conversion - """ - assert modelPath.exists() - assert outdirPath.exists() - print(modelPath) + +def test_odeSinusoidalModelExists(odeSinusoidalModel: Path) -> None: + """Test if the sinusoidal ODE model exists.""" + assert odeSinusoidalModel.exists(), f"Model {odeSinusoidalModel} does not exist" + + +# ============================================================================= +# Compilation Tests (require Dymola) +# ============================================================================= # Check if Dymola is available # Configure via environment variables: # - DYMOLA_ROOT: Path to Dymola installation (default: /opt/dymola-2025xRefresh1-x86_64/) # - DYMOLA_EXECUTABLE: Path to Dymola binary (default: /usr/local/bin/dymola) -# - DYMOLA_WHL: Relative path to Python wheel (default: Modelica/Library/python_interface/dymola-2025.1-py3-none-any.whl) +# - DYMOLA_WHL: Relative path to Python wheel DYMOLA_PATH = os.getenv("DYMOLA_ROOT", "/opt/dymola-2025xRefresh1-x86_64/") DYMOLA_EXECUTABLE = os.getenv("DYMOLA_EXECUTABLE", "/usr/local/bin/dymola") DYMOLA_WHL = os.getenv( @@ -96,49 +107,107 @@ def test_pathExists(modelPath, outdirPath): @pytest.mark.skipif(not HAS_DYMOLA, reason="Dymola not available in test environment") -@pytest.mark.parametrize( - "mo, outdir", [("src/cases/ode_exp.mo", "src/"), ("src/cases/ode_sin.mo", "src/")] -) -def test_basicConversion(mo, outdir): - """Test mo2fmu python script using mo file. - - Parameters - ---------- - mo: path - path of the modelica file to convert to FMU. - outdir: path - path of the output file directory. - """ - fmumodelname = None - load = None - flags = None - type = "all" - version = "2" - verbose = True - force = True - - baseName = Path(mo).stem - fmuPath = Path(outdir) / f"{baseName}.fmu" - fmuDymola = f"{baseName}_fmu" - - # call mo2fmu converter - mo2fmu( - mo, - outdir, - fmumodelname, - load, - flags, - type, - version, - DYMOLA_PATH, - DYMOLA_EXECUTABLE, - DYMOLA_WHL, - verbose, - force, - ) - - # check if the FMU file is created - checkFmuFileExist(fmuPath, outdir) - - # check model validity of the fmu - checkFmuValidity(fmuPath, fmuDymola, DYMOLA_EXECUTABLE) +class TestDymolaCompilation: + """Tests for FMU compilation with Dymola.""" + + def test_compile_simple_ode(self, simpleOdeModel: Path, tmp_path: Path) -> None: + """Test compileFmu function using simple ODE model.""" + baseName = simpleOdeModel.stem + outdir = tmp_path / "output" + fmuPath = outdir / f"{baseName}.fmu" + fmuDymola = f"{baseName}_fmu" + + # Configure Dymola + dymolaConfig = DymolaConfig( + root=DYMOLA_PATH, + executable=DYMOLA_EXECUTABLE, + wheel_path=DYMOLA_WHL, + ) + + # Call compileFmu converter + result = compileFmu( + mo=simpleOdeModel, + outdir=outdir, + backend="dymola", + fmiType="all", + fmiVersion="2", + verbose=True, + force=True, + dymolaConfig=dymolaConfig, + ) + + # Check compilation succeeded + assert result.success, f"Compilation failed: {result.error_message}" + + # Check if the FMU file is created + checkFmuFileExist(fmuPath, outdir) + + # Check model validity of the fmu + checkFmuValidity(fmuPath, fmuDymola, DYMOLA_EXECUTABLE) + + def test_compile_sinusoidal_ode(self, odeSinusoidalModel: Path, tmp_path: Path) -> None: + """Test compileFmu function using sinusoidal ODE model.""" + baseName = odeSinusoidalModel.stem + outdir = tmp_path / "output" + fmuPath = outdir / f"{baseName}.fmu" + fmuDymola = f"{baseName}_fmu" + + # Configure Dymola + dymolaConfig = DymolaConfig( + root=DYMOLA_PATH, + executable=DYMOLA_EXECUTABLE, + wheel_path=DYMOLA_WHL, + ) + + # Call compileFmu converter + result = compileFmu( + mo=odeSinusoidalModel, + outdir=outdir, + backend="dymola", + fmiType="all", + fmiVersion="2", + verbose=True, + force=True, + dymolaConfig=dymolaConfig, + ) + + # Check compilation succeeded + assert result.success, f"Compilation failed: {result.error_message}" + + # Check if the FMU file is created + checkFmuFileExist(fmuPath, outdir) + + # Check model validity of the fmu + checkFmuValidity(fmuPath, fmuDymola, DYMOLA_EXECUTABLE) + + def test_compile_fmi3(self, simpleOdeModel: Path, tmp_path: Path) -> None: + """Test compileFmu with FMI 3.0.""" + baseName = simpleOdeModel.stem + outdir = tmp_path / "output_fmi3" + fmuPath = outdir / f"{baseName}.fmu" + + # Configure Dymola + dymolaConfig = DymolaConfig( + root=DYMOLA_PATH, + executable=DYMOLA_EXECUTABLE, + wheel_path=DYMOLA_WHL, + ) + + # Call compileFmu converter with FMI 3.0 + result = compileFmu( + mo=simpleOdeModel, + outdir=outdir, + backend="dymola", + fmiType="cs", + fmiVersion="3", + verbose=True, + force=True, + dymolaConfig=dymolaConfig, + ) + + # FMI 3.0 requires Dymola 2024+ + if result.success: + checkFmuFileExist(fmuPath, outdir) + else: + # Older Dymola versions may not support FMI 3.0 + assert "3" in result.error_message or result.error_message is not None diff --git a/uv.lock b/uv.lock index 35be67f..2c7a8e6 100644 --- a/uv.lock +++ b/uv.lock @@ -2,7 +2,8 @@ version = 1 revision = 1 requires-python = ">=3.8.1" resolution-markers = [ - "python_full_version >= '3.10'", + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", "python_full_version == '3.9.*'", "python_full_version < '3.9'", ] @@ -33,7 +34,8 @@ name = "attrs" version = "25.4.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.10'", + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", "python_full_version == '3.9.*'", ] sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251 } @@ -96,7 +98,8 @@ name = "black" version = "25.9.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.10'", + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", "python_full_version == '3.9.*'", ] dependencies = [ @@ -160,7 +163,8 @@ name = "build" version = "1.3.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.10'", + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", "python_full_version == '3.9.*'", ] dependencies = [ @@ -196,6 +200,8 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } wheels = [ + { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191 }, + { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592 }, { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024 }, { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188 }, { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571 }, @@ -204,6 +210,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325 }, { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784 }, { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564 }, + { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804 }, + { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299 }, + { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264 }, + { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651 }, { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 }, { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 }, { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 }, @@ -212,6 +222,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 }, { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 }, { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 }, + { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727 }, + { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400 }, + { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 }, + { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 }, { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 }, { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 }, { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 }, @@ -219,6 +233,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 }, { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 }, { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 }, + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 }, + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 }, + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 }, { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 }, { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 }, { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 }, @@ -226,11 +244,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 }, { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 }, { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 }, + { url = "https://files.pythonhosted.org/packages/48/08/15bf6b43ae9bd06f6b00ad8a91f5a8fe1069d4c9fab550a866755402724e/cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b", size = 182457 }, { url = "https://files.pythonhosted.org/packages/c2/5b/f1523dd545f92f7df468e5f653ffa4df30ac222f3c884e51e139878f1cb5/cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964", size = 425932 }, { url = "https://files.pythonhosted.org/packages/53/93/7e547ab4105969cc8c93b38a667b82a835dd2cc78f3a7dad6130cfd41e1d/cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9", size = 448585 }, { url = "https://files.pythonhosted.org/packages/56/c4/a308f2c332006206bb511de219efeff090e9d63529ba0a77aae72e82248b/cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc", size = 456268 }, { url = "https://files.pythonhosted.org/packages/ca/5b/b63681518265f2f4060d2b60755c1c77ec89e5e045fc3773b72735ddaad5/cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c", size = 436592 }, { url = "https://files.pythonhosted.org/packages/bb/19/b51af9f4a4faa4a8ac5a0e5d5c2522dcd9703d07fac69da34a36c4d960d3/cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1", size = 446512 }, + { url = "https://files.pythonhosted.org/packages/e2/63/2bed8323890cb613bbecda807688a31ed11a7fe7afe31f8faaae0206a9a3/cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8", size = 171576 }, + { url = "https://files.pythonhosted.org/packages/2f/70/80c33b044ebc79527447fd4fbc5455d514c3bb840dede4455de97da39b4d/cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1", size = 181229 }, + { url = "https://files.pythonhosted.org/packages/b9/ea/8bb50596b8ffbc49ddd7a1ad305035daa770202a6b782fc164647c2673ad/cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16", size = 182220 }, + { url = "https://files.pythonhosted.org/packages/ae/11/e77c8cd24f58285a82c23af484cf5b124a376b32644e445960d1a4654c3a/cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36", size = 178605 }, { url = "https://files.pythonhosted.org/packages/ed/65/25a8dc32c53bf5b7b6c2686b42ae2ad58743f7ff644844af7cdb29b49361/cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8", size = 424910 }, { url = "https://files.pythonhosted.org/packages/42/7a/9d086fab7c66bd7c4d0f27c57a1b6b068ced810afc498cc8c49e0088661c/cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576", size = 447200 }, { url = "https://files.pythonhosted.org/packages/da/63/1785ced118ce92a993b0ec9e0d0ac8dc3e5dbfbcaa81135be56c69cabbb6/cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", size = 454565 }, @@ -239,6 +264,8 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/95/b34462f3ccb09c2594aa782d90a90b045de4ff1f70148ee79c69d37a0a5a/cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595", size = 460486 }, { url = "https://files.pythonhosted.org/packages/fc/fc/a1e4bebd8d680febd29cf6c8a40067182b64f00c7d105f8f26b5bc54317b/cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a", size = 437911 }, { url = "https://files.pythonhosted.org/packages/e6/c3/21cab7a6154b6a5ea330ae80de386e7665254835b9e98ecc1340b3a7de9a/cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e", size = 460632 }, + { url = "https://files.pythonhosted.org/packages/cb/b5/fd9f8b5a84010ca169ee49f4e4ad6f8c05f4e3545b72ee041dbbcb159882/cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7", size = 171820 }, + { url = "https://files.pythonhosted.org/packages/8c/52/b08750ce0bce45c143e1b5d7357ee8c55341b52bdef4b0f081af1eb248c2/cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662", size = 181290 }, ] [[package]] @@ -246,7 +273,8 @@ name = "cffi" version = "2.0.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.10'", + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", "python_full_version == '3.9.*'", ] dependencies = [ @@ -254,6 +282,8 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588 } wheels = [ + { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283 }, + { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504 }, { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811 }, { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402 }, { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217 }, @@ -262,6 +292,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829 }, { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211 }, { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036 }, + { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184 }, + { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790 }, + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344 }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560 }, { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613 }, { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476 }, { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374 }, @@ -270,6 +304,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971 }, { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972 }, { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078 }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076 }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820 }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635 }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271 }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048 }, { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529 }, { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097 }, { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983 }, @@ -277,6 +316,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572 }, { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963 }, { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361 }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932 }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557 }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762 }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230 }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043 }, { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446 }, { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101 }, { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948 }, @@ -284,18 +328,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499 }, { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928 }, { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302 }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909 }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402 }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780 }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320 }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487 }, { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049 }, { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793 }, { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300 }, { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244 }, { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828 }, { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926 }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328 }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650 }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687 }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773 }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013 }, { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593 }, { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354 }, { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480 }, { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584 }, { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443 }, { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437 }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487 }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726 }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195 }, + { url = "https://files.pythonhosted.org/packages/c0/cc/08ed5a43f2996a16b462f64a7055c6e962803534924b9b2f1371d8c00b7b/cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf", size = 184288 }, + { url = "https://files.pythonhosted.org/packages/3d/de/38d9726324e127f727b4ecc376bc85e505bfe61ef130eaf3f290c6847dd4/cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7", size = 180509 }, { url = "https://files.pythonhosted.org/packages/9b/13/c92e36358fbcc39cf0962e83223c9522154ee8630e1df7c0b3a39a8124e2/cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c", size = 208813 }, { url = "https://files.pythonhosted.org/packages/15/12/a7a79bd0df4c3bff744b2d7e52cc1b68d5e7e427b384252c42366dc1ecbc/cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165", size = 216498 }, { url = "https://files.pythonhosted.org/packages/a3/ad/5c51c1c7600bdd7ed9a24a203ec255dccdd0ebf4527f7b922a0bde2fb6ed/cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534", size = 203243 }, @@ -304,6 +363,8 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e8/be/f6424d1dc46b1091ffcc8964fa7c0ab0cd36839dd2761b49c90481a6ba1b/cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2", size = 218897 }, { url = "https://files.pythonhosted.org/packages/f7/e0/dda537c2309817edf60109e39265f24f24aa7f050767e22c98c53fe7f48b/cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65", size = 211249 }, { url = "https://files.pythonhosted.org/packages/2b/e7/7c769804eb75e4c4b35e658dba01de1640a351a9653c3d49ca89d16ccc91/cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322", size = 218041 }, + { url = "https://files.pythonhosted.org/packages/aa/d9/6218d78f920dcd7507fc16a766b5ef8f3b913cc7aa938e7fc80b9978d089/cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a", size = 172138 }, + { url = "https://files.pythonhosted.org/packages/54/8f/a1e836f82d8e32a97e6b29cc8f641779181ac7363734f12df27db803ebda/cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9", size = 182794 }, ] [[package]] @@ -447,7 +508,8 @@ name = "click" version = "8.3.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.10'", + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", ] dependencies = [ { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, @@ -677,7 +739,8 @@ name = "coverage" version = "7.11.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.10'", + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", ] sdist = { url = "https://files.pythonhosted.org/packages/1c/38/ee22495420457259d2f3390309505ea98f98a5eed40901cf62196abad006/coverage-7.11.0.tar.gz", hash = "sha256:167bd504ac1ca2af7ff3b81d245dfea0292c5032ebef9d66cc08a7d28c1b8050", size = 811905 } wheels = [ @@ -847,7 +910,8 @@ name = "docutils" version = "0.22.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.10'", + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", "python_full_version == '3.9.*'", ] sdist = { url = "https://files.pythonhosted.org/packages/4a/c0/89fe6215b443b919cb98a5002e107cb5026854ed1ccb6b5833e0768419d1/docutils-0.22.2.tar.gz", hash = "sha256:9fdb771707c8784c8f2728b67cb2c691305933d68137ef95a75db5f4dfbc213d", size = 2289092 } @@ -870,7 +934,7 @@ wheels = [ [[package]] name = "feelpp-mo2fmu" -version = "0.6.0" +version = "0.7.0" source = { editable = "." } dependencies = [ { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, @@ -897,6 +961,8 @@ all = [ { name = "isort", version = "7.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "mypy", version = "1.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "mypy", version = "1.18.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "ompython", version = "3.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "ompython", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pipx", version = "1.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "pipx", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, { name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, @@ -929,6 +995,10 @@ lint = [ { name = "mypy", version = "1.18.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, { name = "ruff" }, ] +openmodelica = [ + { name = "ompython", version = "3.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "ompython", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] test = [ { name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, @@ -941,12 +1011,13 @@ requires-dist = [ { name = "black", marker = "extra == 'lint'", specifier = ">=23.0" }, { name = "build", marker = "extra == 'dev'" }, { name = "click" }, - { name = "feelpp-mo2fmu", extras = ["test", "dev", "lint"], marker = "extra == 'all'" }, + { name = "feelpp-mo2fmu", extras = ["openmodelica", "test", "dev", "lint"], marker = "extra == 'all'" }, { name = "flake8", marker = "extra == 'lint'", specifier = ">=6.0" }, { name = "flake8-bugbear", marker = "extra == 'lint'", specifier = ">=23.0" }, { name = "flake8-docstrings", marker = "extra == 'lint'", specifier = ">=1.7" }, { name = "isort", marker = "extra == 'lint'", specifier = ">=5.12" }, { name = "mypy", marker = "extra == 'lint'", specifier = ">=1.0" }, + { name = "ompython", marker = "extra == 'openmodelica'", specifier = ">=3.4.0" }, { name = "pathlib" }, { name = "pipx", marker = "extra == 'dev'" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=7.0" }, @@ -956,7 +1027,7 @@ requires-dist = [ { name = "twine", marker = "extra == 'dev'" }, { name = "xvfbwrapper" }, ] -provides-extras = ["test", "dev", "lint", "all"] +provides-extras = ["openmodelica", "test", "dev", "lint", "all"] [[package]] name = "flake8" @@ -980,7 +1051,8 @@ name = "flake8" version = "7.3.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.10'", + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", "python_full_version == '3.9.*'", ] dependencies = [ @@ -1022,6 +1094,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3f/7d/76a278fa43250441ed9300c344f889c7fb1817080c8fb8996b840bf421c2/flake8_docstrings-1.7.0-py2.py3-none-any.whl", hash = "sha256:51f2344026da083fc084166a9353f5082b01f72901df422f74b4d953ae88ac75", size = 4994 }, ] +[[package]] +name = "future" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/b2/4140c69c6a66432916b26158687e821ba631a4c9273c474343badf84d3ba/future-1.0.0.tar.gz", hash = "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05", size = 1228490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/71/ae30dadffc90b9006d77af76b393cb9dfbfc9629f339fc1574a1c52e6806/future-1.0.0-py3-none-any.whl", hash = "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216", size = 491326 }, +] + [[package]] name = "id" version = "1.5.0" @@ -1064,7 +1145,8 @@ name = "importlib-metadata" version = "8.7.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.10'", + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", "python_full_version == '3.9.*'", ] dependencies = [ @@ -1105,7 +1187,8 @@ name = "iniconfig" version = "2.3.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.10'", + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", ] sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503 } wheels = [ @@ -1144,7 +1227,8 @@ name = "isort" version = "7.0.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.10'", + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", ] sdist = { url = "https://files.pythonhosted.org/packages/63/53/4f3c058e3bace40282876f9b553343376ee687f3c35a525dc79dbd450f88/isort-7.0.0.tar.gz", hash = "sha256:5513527951aadb3ac4292a41a16cbc50dd1642432f5e8c20057d414bdafb4187", size = 805049 } wheels = [ @@ -1196,7 +1280,8 @@ name = "jaraco-functools" version = "4.3.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.10'", + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", "python_full_version == '3.9.*'", ] dependencies = [ @@ -1243,7 +1328,8 @@ name = "keyring" version = "25.6.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.10'", + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", "python_full_version == '3.9.*'", ] dependencies = [ @@ -1282,7 +1368,8 @@ name = "markdown-it-py" version = "4.0.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.10'", + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", ] dependencies = [ { name = "mdurl", marker = "python_full_version >= '3.10'" }, @@ -1327,7 +1414,8 @@ name = "more-itertools" version = "10.8.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.10'", + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", "python_full_version == '3.9.*'", ] sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431 } @@ -1393,7 +1481,8 @@ name = "mypy" version = "1.18.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.10'", + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", "python_full_version == '3.9.*'", ] dependencies = [ @@ -1485,6 +1574,285 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/34/67/d5e07efd38194f52b59b8af25a029b46c0643e9af68204ee263022924c27/nh3-0.3.1-cp38-abi3-win_arm64.whl", hash = "sha256:a3e810a92fb192373204456cac2834694440af73d749565b4348e30235da7f0b", size = 586369 }, ] +[[package]] +name = "numpy" +version = "1.24.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/a4/9b/027bec52c633f6556dba6b722d9a0befb40498b9ceddd29cbe67a45a127c/numpy-1.24.4.tar.gz", hash = "sha256:80f5e3a4e498641401868df4208b74581206afbee7cf7b8329daae82676d9463", size = 10911229 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/80/6cdfb3e275d95155a34659163b83c09e3a3ff9f1456880bec6cc63d71083/numpy-1.24.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0bfb52d2169d58c1cdb8cc1f16989101639b34c7d3ce60ed70b19c63eba0b64", size = 19789140 }, + { url = "https://files.pythonhosted.org/packages/64/5f/3f01d753e2175cfade1013eea08db99ba1ee4bdb147ebcf3623b75d12aa7/numpy-1.24.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ed094d4f0c177b1b8e7aa9cba7d6ceed51c0e569a5318ac0ca9a090680a6a1b1", size = 13854297 }, + { url = "https://files.pythonhosted.org/packages/5a/b3/2f9c21d799fa07053ffa151faccdceeb69beec5a010576b8991f614021f7/numpy-1.24.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79fc682a374c4a8ed08b331bef9c5f582585d1048fa6d80bc6c35bc384eee9b4", size = 13995611 }, + { url = "https://files.pythonhosted.org/packages/10/be/ae5bf4737cb79ba437879915791f6f26d92583c738d7d960ad94e5c36adf/numpy-1.24.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ffe43c74893dbf38c2b0a1f5428760a1a9c98285553c89e12d70a96a7f3a4d6", size = 17282357 }, + { url = "https://files.pythonhosted.org/packages/c0/64/908c1087be6285f40e4b3e79454552a701664a079321cff519d8c7051d06/numpy-1.24.4-cp310-cp310-win32.whl", hash = "sha256:4c21decb6ea94057331e111a5bed9a79d335658c27ce2adb580fb4d54f2ad9bc", size = 12429222 }, + { url = "https://files.pythonhosted.org/packages/22/55/3d5a7c1142e0d9329ad27cece17933b0e2ab4e54ddc5c1861fbfeb3f7693/numpy-1.24.4-cp310-cp310-win_amd64.whl", hash = "sha256:b4bea75e47d9586d31e892a7401f76e909712a0fd510f58f5337bea9572c571e", size = 14841514 }, + { url = "https://files.pythonhosted.org/packages/a9/cc/5ed2280a27e5dab12994c884f1f4d8c3bd4d885d02ae9e52a9d213a6a5e2/numpy-1.24.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f136bab9c2cfd8da131132c2cf6cc27331dd6fae65f95f69dcd4ae3c3639c810", size = 19775508 }, + { url = "https://files.pythonhosted.org/packages/c0/bc/77635c657a3668cf652806210b8662e1aff84b818a55ba88257abf6637a8/numpy-1.24.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2926dac25b313635e4d6cf4dc4e51c8c0ebfed60b801c799ffc4c32bf3d1254", size = 13840033 }, + { url = "https://files.pythonhosted.org/packages/a7/4c/96cdaa34f54c05e97c1c50f39f98d608f96f0677a6589e64e53104e22904/numpy-1.24.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:222e40d0e2548690405b0b3c7b21d1169117391c2e82c378467ef9ab4c8f0da7", size = 13991951 }, + { url = "https://files.pythonhosted.org/packages/22/97/dfb1a31bb46686f09e68ea6ac5c63fdee0d22d7b23b8f3f7ea07712869ef/numpy-1.24.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7215847ce88a85ce39baf9e89070cb860c98fdddacbaa6c0da3ffb31b3350bd5", size = 17278923 }, + { url = "https://files.pythonhosted.org/packages/35/e2/76a11e54139654a324d107da1d98f99e7aa2a7ef97cfd7c631fba7dbde71/numpy-1.24.4-cp311-cp311-win32.whl", hash = "sha256:4979217d7de511a8d57f4b4b5b2b965f707768440c17cb70fbf254c4b225238d", size = 12422446 }, + { url = "https://files.pythonhosted.org/packages/d8/ec/ebef2f7d7c28503f958f0f8b992e7ce606fb74f9e891199329d5f5f87404/numpy-1.24.4-cp311-cp311-win_amd64.whl", hash = "sha256:b7b1fc9864d7d39e28f41d089bfd6353cb5f27ecd9905348c24187a768c79694", size = 14834466 }, + { url = "https://files.pythonhosted.org/packages/11/10/943cfb579f1a02909ff96464c69893b1d25be3731b5d3652c2e0cf1281ea/numpy-1.24.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1452241c290f3e2a312c137a9999cdbf63f78864d63c79039bda65ee86943f61", size = 19780722 }, + { url = "https://files.pythonhosted.org/packages/a7/ae/f53b7b265fdc701e663fbb322a8e9d4b14d9cb7b2385f45ddfabfc4327e4/numpy-1.24.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:04640dab83f7c6c85abf9cd729c5b65f1ebd0ccf9de90b270cd61935eef0197f", size = 13843102 }, + { url = "https://files.pythonhosted.org/packages/25/6f/2586a50ad72e8dbb1d8381f837008a0321a3516dfd7cb57fc8cf7e4bb06b/numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5425b114831d1e77e4b5d812b69d11d962e104095a5b9c3b641a218abcc050e", size = 14039616 }, + { url = "https://files.pythonhosted.org/packages/98/5d/5738903efe0ecb73e51eb44feafba32bdba2081263d40c5043568ff60faf/numpy-1.24.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd80e219fd4c71fc3699fc1dadac5dcf4fd882bfc6f7ec53d30fa197b8ee22dc", size = 17316263 }, + { url = "https://files.pythonhosted.org/packages/d1/57/8d328f0b91c733aa9aa7ee540dbc49b58796c862b4fbcb1146c701e888da/numpy-1.24.4-cp38-cp38-win32.whl", hash = "sha256:4602244f345453db537be5314d3983dbf5834a9701b7723ec28923e2889e0bb2", size = 12455660 }, + { url = "https://files.pythonhosted.org/packages/69/65/0d47953afa0ad569d12de5f65d964321c208492064c38fe3b0b9744f8d44/numpy-1.24.4-cp38-cp38-win_amd64.whl", hash = "sha256:692f2e0f55794943c5bfff12b3f56f99af76f902fc47487bdfe97856de51a706", size = 14868112 }, + { url = "https://files.pythonhosted.org/packages/9a/cd/d5b0402b801c8a8b56b04c1e85c6165efab298d2f0ab741c2406516ede3a/numpy-1.24.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2541312fbf09977f3b3ad449c4e5f4bb55d0dbf79226d7724211acc905049400", size = 19816549 }, + { url = "https://files.pythonhosted.org/packages/14/27/638aaa446f39113a3ed38b37a66243e21b38110d021bfcb940c383e120f2/numpy-1.24.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9667575fb6d13c95f1b36aca12c5ee3356bf001b714fc354eb5465ce1609e62f", size = 13879950 }, + { url = "https://files.pythonhosted.org/packages/8f/27/91894916e50627476cff1a4e4363ab6179d01077d71b9afed41d9e1f18bf/numpy-1.24.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a86ed21e4f87050382c7bc96571755193c4c1392490744ac73d660e8f564a9", size = 14030228 }, + { url = "https://files.pythonhosted.org/packages/7a/7c/d7b2a0417af6428440c0ad7cb9799073e507b1a465f827d058b826236964/numpy-1.24.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d11efb4dbecbdf22508d55e48d9c8384db795e1b7b51ea735289ff96613ff74d", size = 17311170 }, + { url = "https://files.pythonhosted.org/packages/18/9d/e02ace5d7dfccee796c37b995c63322674daf88ae2f4a4724c5dd0afcc91/numpy-1.24.4-cp39-cp39-win32.whl", hash = "sha256:6620c0acd41dbcb368610bb2f4d83145674040025e5536954782467100aa8835", size = 12454918 }, + { url = "https://files.pythonhosted.org/packages/63/38/6cc19d6b8bfa1d1a459daf2b3fe325453153ca7019976274b6f33d8b5663/numpy-1.24.4-cp39-cp39-win_amd64.whl", hash = "sha256:befe2bf740fd8373cf56149a5c23a0f601e82869598d41f8e188a0e9869926f8", size = 14867441 }, + { url = "https://files.pythonhosted.org/packages/a4/fd/8dff40e25e937c94257455c237b9b6bf5a30d42dd1cc11555533be099492/numpy-1.24.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:31f13e25b4e304632a4619d0e0777662c2ffea99fcae2029556b17d8ff958aef", size = 19156590 }, + { url = "https://files.pythonhosted.org/packages/42/e7/4bf953c6e05df90c6d351af69966384fed8e988d0e8c54dad7103b59f3ba/numpy-1.24.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95f7ac6540e95bc440ad77f56e520da5bf877f87dca58bd095288dce8940532a", size = 16705744 }, + { url = "https://files.pythonhosted.org/packages/fc/dd/9106005eb477d022b60b3817ed5937a43dad8fd1f20b0610ea8a32fcb407/numpy-1.24.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e98f220aa76ca2a977fe435f5b04d7b3470c0a2e6312907b37ba6068f26787f2", size = 14734290 }, +] + +[[package]] +name = "numpy" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/75/10dd1f8116a8b796cb2c737b674e02d02e80454bda953fa7e65d8c12b016/numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78", size = 18902015 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/91/3495b3237510f79f5d81f2508f9f13fea78ebfdf07538fc7444badda173d/numpy-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece", size = 21165245 }, + { url = "https://files.pythonhosted.org/packages/05/33/26178c7d437a87082d11019292dce6d3fe6f0e9026b7b2309cbf3e489b1d/numpy-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f15975dfec0cf2239224d80e32c3170b1d168335eaedee69da84fbe9f1f9cd04", size = 13738540 }, + { url = "https://files.pythonhosted.org/packages/ec/31/cc46e13bf07644efc7a4bf68df2df5fb2a1a88d0cd0da9ddc84dc0033e51/numpy-2.0.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8c5713284ce4e282544c68d1c3b2c7161d38c256d2eefc93c1d683cf47683e66", size = 5300623 }, + { url = "https://files.pythonhosted.org/packages/6e/16/7bfcebf27bb4f9d7ec67332ffebee4d1bf085c84246552d52dbb548600e7/numpy-2.0.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:becfae3ddd30736fe1889a37f1f580e245ba79a5855bff5f2a29cb3ccc22dd7b", size = 6901774 }, + { url = "https://files.pythonhosted.org/packages/f9/a3/561c531c0e8bf082c5bef509d00d56f82e0ea7e1e3e3a7fc8fa78742a6e5/numpy-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2da5960c3cf0df7eafefd806d4e612c5e19358de82cb3c343631188991566ccd", size = 13907081 }, + { url = "https://files.pythonhosted.org/packages/fa/66/f7177ab331876200ac7563a580140643d1179c8b4b6a6b0fc9838de2a9b8/numpy-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:496f71341824ed9f3d2fd36cf3ac57ae2e0165c143b55c3a035ee219413f3318", size = 19523451 }, + { url = "https://files.pythonhosted.org/packages/25/7f/0b209498009ad6453e4efc2c65bcdf0ae08a182b2b7877d7ab38a92dc542/numpy-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a61ec659f68ae254e4d237816e33171497e978140353c0c2038d46e63282d0c8", size = 19927572 }, + { url = "https://files.pythonhosted.org/packages/3e/df/2619393b1e1b565cd2d4c4403bdd979621e2c4dea1f8532754b2598ed63b/numpy-2.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d731a1c6116ba289c1e9ee714b08a8ff882944d4ad631fd411106a30f083c326", size = 14400722 }, + { url = "https://files.pythonhosted.org/packages/22/ad/77e921b9f256d5da36424ffb711ae79ca3f451ff8489eeca544d0701d74a/numpy-2.0.2-cp310-cp310-win32.whl", hash = "sha256:984d96121c9f9616cd33fbd0618b7f08e0cfc9600a7ee1d6fd9b239186d19d97", size = 6472170 }, + { url = "https://files.pythonhosted.org/packages/10/05/3442317535028bc29cf0c0dd4c191a4481e8376e9f0db6bcf29703cadae6/numpy-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:c7b0be4ef08607dd04da4092faee0b86607f111d5ae68036f16cc787e250a131", size = 15905558 }, + { url = "https://files.pythonhosted.org/packages/8b/cf/034500fb83041aa0286e0fb16e7c76e5c8b67c0711bb6e9e9737a717d5fe/numpy-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:49ca4decb342d66018b01932139c0961a8f9ddc7589611158cb3c27cbcf76448", size = 21169137 }, + { url = "https://files.pythonhosted.org/packages/4a/d9/32de45561811a4b87fbdee23b5797394e3d1504b4a7cf40c10199848893e/numpy-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11a76c372d1d37437857280aa142086476136a8c0f373b2e648ab2c8f18fb195", size = 13703552 }, + { url = "https://files.pythonhosted.org/packages/c1/ca/2f384720020c7b244d22508cb7ab23d95f179fcfff33c31a6eeba8d6c512/numpy-2.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:807ec44583fd708a21d4a11d94aedf2f4f3c3719035c76a2bbe1fe8e217bdc57", size = 5298957 }, + { url = "https://files.pythonhosted.org/packages/0e/78/a3e4f9fb6aa4e6fdca0c5428e8ba039408514388cf62d89651aade838269/numpy-2.0.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8cafab480740e22f8d833acefed5cc87ce276f4ece12fdaa2e8903db2f82897a", size = 6905573 }, + { url = "https://files.pythonhosted.org/packages/a0/72/cfc3a1beb2caf4efc9d0b38a15fe34025230da27e1c08cc2eb9bfb1c7231/numpy-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a15f476a45e6e5a3a79d8a14e62161d27ad897381fecfa4a09ed5322f2085669", size = 13914330 }, + { url = "https://files.pythonhosted.org/packages/ba/a8/c17acf65a931ce551fee11b72e8de63bf7e8a6f0e21add4c937c83563538/numpy-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13e689d772146140a252c3a28501da66dfecd77490b498b168b501835041f951", size = 19534895 }, + { url = "https://files.pythonhosted.org/packages/ba/86/8767f3d54f6ae0165749f84648da9dcc8cd78ab65d415494962c86fac80f/numpy-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9ea91dfb7c3d1c56a0e55657c0afb38cf1eeae4544c208dc465c3c9f3a7c09f9", size = 19937253 }, + { url = "https://files.pythonhosted.org/packages/df/87/f76450e6e1c14e5bb1eae6836478b1028e096fd02e85c1c37674606ab752/numpy-2.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c1c9307701fec8f3f7a1e6711f9089c06e6284b3afbbcd259f7791282d660a15", size = 14414074 }, + { url = "https://files.pythonhosted.org/packages/5c/ca/0f0f328e1e59f73754f06e1adfb909de43726d4f24c6a3f8805f34f2b0fa/numpy-2.0.2-cp311-cp311-win32.whl", hash = "sha256:a392a68bd329eafac5817e5aefeb39038c48b671afd242710b451e76090e81f4", size = 6470640 }, + { url = "https://files.pythonhosted.org/packages/eb/57/3a3f14d3a759dcf9bf6e9eda905794726b758819df4663f217d658a58695/numpy-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:286cd40ce2b7d652a6f22efdfc6d1edf879440e53e76a75955bc0c826c7e64dc", size = 15910230 }, + { url = "https://files.pythonhosted.org/packages/45/40/2e117be60ec50d98fa08c2f8c48e09b3edea93cfcabd5a9ff6925d54b1c2/numpy-2.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:df55d490dea7934f330006d0f81e8551ba6010a5bf035a249ef61a94f21c500b", size = 20895803 }, + { url = "https://files.pythonhosted.org/packages/46/92/1b8b8dee833f53cef3e0a3f69b2374467789e0bb7399689582314df02651/numpy-2.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8df823f570d9adf0978347d1f926b2a867d5608f434a7cff7f7908c6570dcf5e", size = 13471835 }, + { url = "https://files.pythonhosted.org/packages/7f/19/e2793bde475f1edaea6945be141aef6c8b4c669b90c90a300a8954d08f0a/numpy-2.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9a92ae5c14811e390f3767053ff54eaee3bf84576d99a2456391401323f4ec2c", size = 5038499 }, + { url = "https://files.pythonhosted.org/packages/e3/ff/ddf6dac2ff0dd50a7327bcdba45cb0264d0e96bb44d33324853f781a8f3c/numpy-2.0.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a842d573724391493a97a62ebbb8e731f8a5dcc5d285dfc99141ca15a3302d0c", size = 6633497 }, + { url = "https://files.pythonhosted.org/packages/72/21/67f36eac8e2d2cd652a2e69595a54128297cdcb1ff3931cfc87838874bd4/numpy-2.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05e238064fc0610c840d1cf6a13bf63d7e391717d247f1bf0318172e759e692", size = 13621158 }, + { url = "https://files.pythonhosted.org/packages/39/68/e9f1126d757653496dbc096cb429014347a36b228f5a991dae2c6b6cfd40/numpy-2.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0123ffdaa88fa4ab64835dcbde75dcdf89c453c922f18dced6e27c90d1d0ec5a", size = 19236173 }, + { url = "https://files.pythonhosted.org/packages/d1/e9/1f5333281e4ebf483ba1c888b1d61ba7e78d7e910fdd8e6499667041cc35/numpy-2.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:96a55f64139912d61de9137f11bf39a55ec8faec288c75a54f93dfd39f7eb40c", size = 19634174 }, + { url = "https://files.pythonhosted.org/packages/71/af/a469674070c8d8408384e3012e064299f7a2de540738a8e414dcfd639996/numpy-2.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec9852fb39354b5a45a80bdab5ac02dd02b15f44b3804e9f00c556bf24b4bded", size = 14099701 }, + { url = "https://files.pythonhosted.org/packages/d0/3d/08ea9f239d0e0e939b6ca52ad403c84a2bce1bde301a8eb4888c1c1543f1/numpy-2.0.2-cp312-cp312-win32.whl", hash = "sha256:671bec6496f83202ed2d3c8fdc486a8fc86942f2e69ff0e986140339a63bcbe5", size = 6174313 }, + { url = "https://files.pythonhosted.org/packages/b2/b5/4ac39baebf1fdb2e72585c8352c56d063b6126be9fc95bd2bb5ef5770c20/numpy-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:cfd41e13fdc257aa5778496b8caa5e856dc4896d4ccf01841daee1d96465467a", size = 15606179 }, + { url = "https://files.pythonhosted.org/packages/43/c1/41c8f6df3162b0c6ffd4437d729115704bd43363de0090c7f913cfbc2d89/numpy-2.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9059e10581ce4093f735ed23f3b9d283b9d517ff46009ddd485f1747eb22653c", size = 21169942 }, + { url = "https://files.pythonhosted.org/packages/39/bc/fd298f308dcd232b56a4031fd6ddf11c43f9917fbc937e53762f7b5a3bb1/numpy-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:423e89b23490805d2a5a96fe40ec507407b8ee786d66f7328be214f9679df6dd", size = 13711512 }, + { url = "https://files.pythonhosted.org/packages/96/ff/06d1aa3eeb1c614eda245c1ba4fb88c483bee6520d361641331872ac4b82/numpy-2.0.2-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:2b2955fa6f11907cf7a70dab0d0755159bca87755e831e47932367fc8f2f2d0b", size = 5306976 }, + { url = "https://files.pythonhosted.org/packages/2d/98/121996dcfb10a6087a05e54453e28e58694a7db62c5a5a29cee14c6e047b/numpy-2.0.2-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:97032a27bd9d8988b9a97a8c4d2c9f2c15a81f61e2f21404d7e8ef00cb5be729", size = 6906494 }, + { url = "https://files.pythonhosted.org/packages/15/31/9dffc70da6b9bbf7968f6551967fc21156207366272c2a40b4ed6008dc9b/numpy-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e795a8be3ddbac43274f18588329c72939870a16cae810c2b73461c40718ab1", size = 13912596 }, + { url = "https://files.pythonhosted.org/packages/b9/14/78635daab4b07c0930c919d451b8bf8c164774e6a3413aed04a6d95758ce/numpy-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b258c385842546006213344c50655ff1555a9338e2e5e02a0756dc3e803dd", size = 19526099 }, + { url = "https://files.pythonhosted.org/packages/26/4c/0eeca4614003077f68bfe7aac8b7496f04221865b3a5e7cb230c9d055afd/numpy-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fec9451a7789926bcf7c2b8d187292c9f93ea30284802a0ab3f5be8ab36865d", size = 19932823 }, + { url = "https://files.pythonhosted.org/packages/f1/46/ea25b98b13dccaebddf1a803f8c748680d972e00507cd9bc6dcdb5aa2ac1/numpy-2.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9189427407d88ff25ecf8f12469d4d39d35bee1db5d39fc5c168c6f088a6956d", size = 14404424 }, + { url = "https://files.pythonhosted.org/packages/c8/a6/177dd88d95ecf07e722d21008b1b40e681a929eb9e329684d449c36586b2/numpy-2.0.2-cp39-cp39-win32.whl", hash = "sha256:905d16e0c60200656500c95b6b8dca5d109e23cb24abc701d41c02d74c6b3afa", size = 6476809 }, + { url = "https://files.pythonhosted.org/packages/ea/2b/7fc9f4e7ae5b507c1a3a21f0f15ed03e794c1242ea8a242ac158beb56034/numpy-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:a3f4ab0caa7f053f6797fcd4e1e25caee367db3112ef2b6ef82d749530768c73", size = 15911314 }, + { url = "https://files.pythonhosted.org/packages/8f/3b/df5a870ac6a3be3a86856ce195ef42eec7ae50d2a202be1f5a4b3b340e14/numpy-2.0.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7f0a0c6f12e07fa94133c8a67404322845220c06a9e80e85999afe727f7438b8", size = 21025288 }, + { url = "https://files.pythonhosted.org/packages/2c/97/51af92f18d6f6f2d9ad8b482a99fb74e142d71372da5d834b3a2747a446e/numpy-2.0.2-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:312950fdd060354350ed123c0e25a71327d3711584beaef30cdaa93320c392d4", size = 6762793 }, + { url = "https://files.pythonhosted.org/packages/12/46/de1fbd0c1b5ccaa7f9a005b66761533e2f6a3e560096682683a223631fe9/numpy-2.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26df23238872200f63518dd2aa984cfca675d82469535dc7162dc2ee52d9dd5c", size = 19334885 }, + { url = "https://files.pythonhosted.org/packages/cc/dc/d330a6faefd92b446ec0f0dfea4c3207bb1fef3c4771d19cf4543efd2c78/numpy-2.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a46288ec55ebbd58947d31d72be2c63cbf839f0a63b49cb755022310792a3385", size = 15828784 }, +] + +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245 }, + { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048 }, + { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542 }, + { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301 }, + { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320 }, + { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050 }, + { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034 }, + { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185 }, + { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149 }, + { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620 }, + { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963 }, + { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743 }, + { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616 }, + { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579 }, + { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005 }, + { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570 }, + { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548 }, + { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521 }, + { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866 }, + { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455 }, + { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348 }, + { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362 }, + { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103 }, + { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382 }, + { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462 }, + { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618 }, + { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511 }, + { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783 }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506 }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190 }, + { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828 }, + { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006 }, + { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765 }, + { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736 }, + { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719 }, + { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072 }, + { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213 }, + { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632 }, + { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532 }, + { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885 }, + { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467 }, + { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144 }, + { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217 }, + { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014 }, + { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935 }, + { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122 }, + { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143 }, + { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260 }, + { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225 }, + { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374 }, + { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391 }, + { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754 }, + { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476 }, + { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666 }, +] + +[[package]] +name = "numpy" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/a4/7a/6a3d14e205d292b738db449d0de649b373a59edb0d0b4493821d0a3e8718/numpy-2.4.0.tar.gz", hash = "sha256:6e504f7b16118198f138ef31ba24d985b124c2c469fe8467007cf30fd992f934", size = 20685720 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/7e/7bae7cbcc2f8132271967aa03e03954fc1e48aa1f3bf32b29ca95fbef352/numpy-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:316b2f2584682318539f0bcaca5a496ce9ca78c88066579ebd11fd06f8e4741e", size = 16940166 }, + { url = "https://files.pythonhosted.org/packages/0f/27/6c13f5b46776d6246ec884ac5817452672156a506d08a1f2abb39961930a/numpy-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2718c1de8504121714234b6f8241d0019450353276c88b9453c9c3d92e101db", size = 12641781 }, + { url = "https://files.pythonhosted.org/packages/14/1c/83b4998d4860d15283241d9e5215f28b40ac31f497c04b12fa7f428ff370/numpy-2.4.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:21555da4ec4a0c942520ead42c3b0dc9477441e085c42b0fbdd6a084869a6f6b", size = 5470247 }, + { url = "https://files.pythonhosted.org/packages/54/08/cbce72c835d937795571b0464b52069f869c9e78b0c076d416c5269d2718/numpy-2.4.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:413aa561266a4be2d06cd2b9665e89d9f54c543f418773076a76adcf2af08bc7", size = 6799807 }, + { url = "https://files.pythonhosted.org/packages/ff/be/2e647961cd8c980591d75cdcd9e8f647d69fbe05e2a25613dc0a2ea5fb1a/numpy-2.4.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0feafc9e03128074689183031181fac0897ff169692d8492066e949041096548", size = 14701992 }, + { url = "https://files.pythonhosted.org/packages/a2/fb/e1652fb8b6fd91ce6ed429143fe2e01ce714711e03e5b762615e7b36172c/numpy-2.4.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8fdfed3deaf1928fb7667d96e0567cdf58c2b370ea2ee7e586aa383ec2cb346", size = 16646871 }, + { url = "https://files.pythonhosted.org/packages/62/23/d841207e63c4322842f7cd042ae981cffe715c73376dcad8235fb31debf1/numpy-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e06a922a469cae9a57100864caf4f8a97a1026513793969f8ba5b63137a35d25", size = 16487190 }, + { url = "https://files.pythonhosted.org/packages/bc/a0/6a842c8421ebfdec0a230e65f61e0dabda6edbef443d999d79b87c273965/numpy-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:927ccf5cd17c48f801f4ed43a7e5673a2724bd2171460be3e3894e6e332ef83a", size = 18580762 }, + { url = "https://files.pythonhosted.org/packages/0a/d1/c79e0046641186f2134dde05e6181825b911f8bdcef31b19ddd16e232847/numpy-2.4.0-cp311-cp311-win32.whl", hash = "sha256:882567b7ae57c1b1a0250208cc21a7976d8cbcc49d5a322e607e6f09c9e0bd53", size = 6233359 }, + { url = "https://files.pythonhosted.org/packages/fc/f0/74965001d231f28184d6305b8cdc1b6fcd4bf23033f6cb039cfe76c9fca7/numpy-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:8b986403023c8f3bf8f487c2e6186afda156174d31c175f747d8934dfddf3479", size = 12601132 }, + { url = "https://files.pythonhosted.org/packages/65/32/55408d0f46dfebce38017f5bd931affa7256ad6beac1a92a012e1fbc67a7/numpy-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:3f3096405acc48887458bbf9f6814d43785ac7ba2a57ea6442b581dedbc60ce6", size = 10573977 }, + { url = "https://files.pythonhosted.org/packages/8b/ff/f6400ffec95de41c74b8e73df32e3fff1830633193a7b1e409be7fb1bb8c/numpy-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2a8b6bb8369abefb8bd1801b054ad50e02b3275c8614dc6e5b0373c305291037", size = 16653117 }, + { url = "https://files.pythonhosted.org/packages/fd/28/6c23e97450035072e8d830a3c411bf1abd1f42c611ff9d29e3d8f55c6252/numpy-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2e284ca13d5a8367e43734148622caf0b261b275673823593e3e3634a6490f83", size = 12369711 }, + { url = "https://files.pythonhosted.org/packages/bc/af/acbef97b630ab1bb45e6a7d01d1452e4251aa88ce680ac36e56c272120ec/numpy-2.4.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:49ff32b09f5aa0cd30a20c2b39db3e669c845589f2b7fc910365210887e39344", size = 5198355 }, + { url = "https://files.pythonhosted.org/packages/c1/c8/4e0d436b66b826f2e53330adaa6311f5cac9871a5b5c31ad773b27f25a74/numpy-2.4.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:36cbfb13c152b1c7c184ddac43765db8ad672567e7bafff2cc755a09917ed2e6", size = 6545298 }, + { url = "https://files.pythonhosted.org/packages/ef/27/e1f5d144ab54eac34875e79037011d511ac57b21b220063310cb96c80fbc/numpy-2.4.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35ddc8f4914466e6fc954c76527aa91aa763682a4f6d73249ef20b418fe6effb", size = 14398387 }, + { url = "https://files.pythonhosted.org/packages/67/64/4cb909dd5ab09a9a5d086eff9586e69e827b88a5585517386879474f4cf7/numpy-2.4.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc578891de1db95b2a35001b695451767b580bb45753717498213c5ff3c41d63", size = 16363091 }, + { url = "https://files.pythonhosted.org/packages/9d/9c/8efe24577523ec6809261859737cf117b0eb6fdb655abdfdc81b2e468ce4/numpy-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98e81648e0b36e325ab67e46b5400a7a6d4a22b8a7c8e8bbfe20e7db7906bf95", size = 16176394 }, + { url = "https://files.pythonhosted.org/packages/61/f0/1687441ece7b47a62e45a1f82015352c240765c707928edd8aef875d5951/numpy-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d57b5046c120561ba8fa8e4030fbb8b822f3063910fa901ffadf16e2b7128ad6", size = 18287378 }, + { url = "https://files.pythonhosted.org/packages/d3/6f/f868765d44e6fc466467ed810ba9d8d6db1add7d4a748abfa2a4c99a3194/numpy-2.4.0-cp312-cp312-win32.whl", hash = "sha256:92190db305a6f48734d3982f2c60fa30d6b5ee9bff10f2887b930d7b40119f4c", size = 5955432 }, + { url = "https://files.pythonhosted.org/packages/d4/b5/94c1e79fcbab38d1ca15e13777477b2914dd2d559b410f96949d6637b085/numpy-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:680060061adb2d74ce352628cb798cfdec399068aa7f07ba9fb818b2b3305f98", size = 12306201 }, + { url = "https://files.pythonhosted.org/packages/70/09/c39dadf0b13bb0768cd29d6a3aaff1fb7c6905ac40e9aaeca26b1c086e06/numpy-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:39699233bc72dd482da1415dcb06076e32f60eddc796a796c5fb6c5efce94667", size = 10308234 }, + { url = "https://files.pythonhosted.org/packages/a7/0d/853fd96372eda07c824d24adf02e8bc92bb3731b43a9b2a39161c3667cc4/numpy-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a152d86a3ae00ba5f47b3acf3b827509fd0b6cb7d3259665e63dafbad22a75ea", size = 16649088 }, + { url = "https://files.pythonhosted.org/packages/e3/37/cc636f1f2a9f585434e20a3e6e63422f70bfe4f7f6698e941db52ea1ac9a/numpy-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:39b19251dec4de8ff8496cd0806cbe27bf0684f765abb1f4809554de93785f2d", size = 12364065 }, + { url = "https://files.pythonhosted.org/packages/ed/69/0b78f37ca3690969beee54103ce5f6021709134e8020767e93ba691a72f1/numpy-2.4.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:009bd0ea12d3c784b6639a8457537016ce5172109e585338e11334f6a7bb88ee", size = 5192640 }, + { url = "https://files.pythonhosted.org/packages/1d/2a/08569f8252abf590294dbb09a430543ec8f8cc710383abfb3e75cc73aeda/numpy-2.4.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5fe44e277225fd3dff6882d86d3d447205d43532c3627313d17e754fb3905a0e", size = 6541556 }, + { url = "https://files.pythonhosted.org/packages/93/e9/a949885a4e177493d61519377952186b6cbfdf1d6002764c664ba28349b5/numpy-2.4.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f935c4493eda9069851058fa0d9e39dbf6286be690066509305e52912714dbb2", size = 14396562 }, + { url = "https://files.pythonhosted.org/packages/99/98/9d4ad53b0e9ef901c2ef1d550d2136f5ac42d3fd2988390a6def32e23e48/numpy-2.4.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8cfa5f29a695cb7438965e6c3e8d06e0416060cf0d709c1b1c1653a939bf5c2a", size = 16351719 }, + { url = "https://files.pythonhosted.org/packages/28/de/5f3711a38341d6e8dd619f6353251a0cdd07f3d6d101a8fd46f4ef87f895/numpy-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba0cb30acd3ef11c94dc27fbfba68940652492bc107075e7ffe23057f9425681", size = 16176053 }, + { url = "https://files.pythonhosted.org/packages/2a/5b/2a3753dc43916501b4183532e7ace862e13211042bceafa253afb5c71272/numpy-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:60e8c196cd82cbbd4f130b5290007e13e6de3eca79f0d4d38014769d96a7c475", size = 18277859 }, + { url = "https://files.pythonhosted.org/packages/2c/c5/a18bcdd07a941db3076ef489d036ab16d2bfc2eae0cf27e5a26e29189434/numpy-2.4.0-cp313-cp313-win32.whl", hash = "sha256:5f48cb3e88fbc294dc90e215d86fbaf1c852c63dbdb6c3a3e63f45c4b57f7344", size = 5953849 }, + { url = "https://files.pythonhosted.org/packages/4f/f1/719010ff8061da6e8a26e1980cf090412d4f5f8060b31f0c45d77dd67a01/numpy-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:a899699294f28f7be8992853c0c60741f16ff199205e2e6cdca155762cbaa59d", size = 12302840 }, + { url = "https://files.pythonhosted.org/packages/f5/5a/b3d259083ed8b4d335270c76966cb6cf14a5d1b69e1a608994ac57a659e6/numpy-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:9198f447e1dc5647d07c9a6bbe2063cc0132728cc7175b39dbc796da5b54920d", size = 10308509 }, + { url = "https://files.pythonhosted.org/packages/31/01/95edcffd1bb6c0633df4e808130545c4f07383ab629ac7e316fb44fff677/numpy-2.4.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74623f2ab5cc3f7c886add4f735d1031a1d2be4a4ae63c0546cfd74e7a31ddf6", size = 12491815 }, + { url = "https://files.pythonhosted.org/packages/59/ea/5644b8baa92cc1c7163b4b4458c8679852733fa74ca49c942cfa82ded4e0/numpy-2.4.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:0804a8e4ab070d1d35496e65ffd3cf8114c136a2b81f61dfab0de4b218aacfd5", size = 5320321 }, + { url = "https://files.pythonhosted.org/packages/26/4e/e10938106d70bc21319bd6a86ae726da37edc802ce35a3a71ecdf1fdfe7f/numpy-2.4.0-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:02a2038eb27f9443a8b266a66911e926566b5a6ffd1a689b588f7f35b81e7dc3", size = 6641635 }, + { url = "https://files.pythonhosted.org/packages/b3/8d/a8828e3eaf5c0b4ab116924df82f24ce3416fa38d0674d8f708ddc6c8aac/numpy-2.4.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1889b3a3f47a7b5bee16bc25a2145bd7cb91897f815ce3499db64c7458b6d91d", size = 14456053 }, + { url = "https://files.pythonhosted.org/packages/68/a1/17d97609d87d4520aa5ae2dcfb32305654550ac6a35effb946d303e594ce/numpy-2.4.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85eef4cb5625c47ee6425c58a3502555e10f45ee973da878ac8248ad58c136f3", size = 16401702 }, + { url = "https://files.pythonhosted.org/packages/18/32/0f13c1b2d22bea1118356b8b963195446f3af124ed7a5adfa8fdecb1b6ca/numpy-2.4.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6dc8b7e2f4eb184b37655195f421836cfae6f58197b67e3ffc501f1333d993fa", size = 16242493 }, + { url = "https://files.pythonhosted.org/packages/ae/23/48f21e3d309fbc137c068a1475358cbd3a901b3987dcfc97a029ab3068e2/numpy-2.4.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:44aba2f0cafd287871a495fb3163408b0bd25bbce135c6f621534a07f4f7875c", size = 18324222 }, + { url = "https://files.pythonhosted.org/packages/ac/52/41f3d71296a3dcaa4f456aaa3c6fc8e745b43d0552b6bde56571bb4b4a0f/numpy-2.4.0-cp313-cp313t-win32.whl", hash = "sha256:20c115517513831860c573996e395707aa9fb691eb179200125c250e895fcd93", size = 6076216 }, + { url = "https://files.pythonhosted.org/packages/35/ff/46fbfe60ab0710d2a2b16995f708750307d30eccbb4c38371ea9e986866e/numpy-2.4.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b48e35f4ab6f6a7597c46e301126ceba4c44cd3280e3750f85db48b082624fa4", size = 12444263 }, + { url = "https://files.pythonhosted.org/packages/a3/e3/9189ab319c01d2ed556c932ccf55064c5d75bb5850d1df7a482ce0badead/numpy-2.4.0-cp313-cp313t-win_arm64.whl", hash = "sha256:4d1cfce39e511069b11e67cd0bd78ceff31443b7c9e5c04db73c7a19f572967c", size = 10378265 }, + { url = "https://files.pythonhosted.org/packages/ab/ed/52eac27de39d5e5a6c9aadabe672bc06f55e24a3d9010cd1183948055d76/numpy-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c95eb6db2884917d86cde0b4d4cf31adf485c8ec36bf8696dd66fa70de96f36b", size = 16647476 }, + { url = "https://files.pythonhosted.org/packages/77/c0/990ce1b7fcd4e09aeaa574e2a0a839589e4b08b2ca68070f1acb1fea6736/numpy-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:65167da969cd1ec3a1df31cb221ca3a19a8aaa25370ecb17d428415e93c1935e", size = 12374563 }, + { url = "https://files.pythonhosted.org/packages/37/7c/8c5e389c6ae8f5fd2277a988600d79e9625db3fff011a2d87ac80b881a4c/numpy-2.4.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3de19cfecd1465d0dcf8a5b5ea8b3155b42ed0b639dba4b71e323d74f2a3be5e", size = 5203107 }, + { url = "https://files.pythonhosted.org/packages/e6/94/ca5b3bd6a8a70a5eec9a0b8dd7f980c1eff4b8a54970a9a7fef248ef564f/numpy-2.4.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6c05483c3136ac4c91b4e81903cb53a8707d316f488124d0398499a4f8e8ef51", size = 6538067 }, + { url = "https://files.pythonhosted.org/packages/79/43/993eb7bb5be6761dde2b3a3a594d689cec83398e3f58f4758010f3b85727/numpy-2.4.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36667db4d6c1cea79c8930ab72fadfb4060feb4bfe724141cd4bd064d2e5f8ce", size = 14411926 }, + { url = "https://files.pythonhosted.org/packages/03/75/d4c43b61de473912496317a854dac54f1efec3eeb158438da6884b70bb90/numpy-2.4.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9a818668b674047fd88c4cddada7ab8f1c298812783e8328e956b78dc4807f9f", size = 16354295 }, + { url = "https://files.pythonhosted.org/packages/b8/0a/b54615b47ee8736a6461a4bb6749128dd3435c5a759d5663f11f0e9af4ac/numpy-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1ee32359fb7543b7b7bd0b2f46294db27e29e7bbdf70541e81b190836cd83ded", size = 16190242 }, + { url = "https://files.pythonhosted.org/packages/98/ce/ea207769aacad6246525ec6c6bbd66a2bf56c72443dc10e2f90feed29290/numpy-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e493962256a38f58283de033d8af176c5c91c084ea30f15834f7545451c42059", size = 18280875 }, + { url = "https://files.pythonhosted.org/packages/17/ef/ec409437aa962ea372ed601c519a2b141701683ff028f894b7466f0ab42b/numpy-2.4.0-cp314-cp314-win32.whl", hash = "sha256:6bbaebf0d11567fa8926215ae731e1d58e6ec28a8a25235b8a47405d301332db", size = 6002530 }, + { url = "https://files.pythonhosted.org/packages/5f/4a/5cb94c787a3ed1ac65e1271b968686521169a7b3ec0b6544bb3ca32960b0/numpy-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:3d857f55e7fdf7c38ab96c4558c95b97d1c685be6b05c249f5fdafcbd6f9899e", size = 12435890 }, + { url = "https://files.pythonhosted.org/packages/48/a0/04b89db963af9de1104975e2544f30de89adbf75b9e75f7dd2599be12c79/numpy-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:bb50ce5fb202a26fd5404620e7ef820ad1ab3558b444cb0b55beb7ef66cd2d63", size = 10591892 }, + { url = "https://files.pythonhosted.org/packages/53/e5/d74b5ccf6712c06c7a545025a6a71bfa03bdc7e0568b405b0d655232fd92/numpy-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:355354388cba60f2132df297e2d53053d4063f79077b67b481d21276d61fc4df", size = 12494312 }, + { url = "https://files.pythonhosted.org/packages/c2/08/3ca9cc2ddf54dfee7ae9a6479c071092a228c68aef08252aa08dac2af002/numpy-2.4.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:1d8f9fde5f6dc1b6fc34df8162f3b3079365468703fee7f31d4e0cc8c63baed9", size = 5322862 }, + { url = "https://files.pythonhosted.org/packages/87/74/0bb63a68394c0c1e52670cfff2e309afa41edbe11b3327d9af29e4383f34/numpy-2.4.0-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:e0434aa22c821f44eeb4c650b81c7fbdd8c0122c6c4b5a576a76d5a35625ecd9", size = 6644986 }, + { url = "https://files.pythonhosted.org/packages/06/8f/9264d9bdbcf8236af2823623fe2f3981d740fc3461e2787e231d97c38c28/numpy-2.4.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40483b2f2d3ba7aad426443767ff5632ec3156ef09742b96913787d13c336471", size = 14457958 }, + { url = "https://files.pythonhosted.org/packages/8c/d9/f9a69ae564bbc7236a35aa883319364ef5fd41f72aa320cc1cbe66148fe2/numpy-2.4.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9e6a7664ddd9746e20b7325351fe1a8408d0a2bf9c63b5e898290ddc8f09544", size = 16398394 }, + { url = "https://files.pythonhosted.org/packages/34/c7/39241501408dde7f885d241a98caba5421061a2c6d2b2197ac5e3aa842d8/numpy-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ecb0019d44f4cdb50b676c5d0cb4b1eae8e15d1ed3d3e6639f986fc92b2ec52c", size = 16241044 }, + { url = "https://files.pythonhosted.org/packages/7c/95/cae7effd90e065a95e59fe710eeee05d7328ed169776dfdd9f789e032125/numpy-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d0ffd9e2e4441c96a9c91ec1783285d80bf835b677853fc2770a89d50c1e48ac", size = 18321772 }, + { url = "https://files.pythonhosted.org/packages/96/df/3c6c279accd2bfb968a76298e5b276310bd55d243df4fa8ac5816d79347d/numpy-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:77f0d13fa87036d7553bf81f0e1fe3ce68d14c9976c9851744e4d3e91127e95f", size = 6148320 }, + { url = "https://files.pythonhosted.org/packages/92/8d/f23033cce252e7a75cae853d17f582e86534c46404dea1c8ee094a9d6d84/numpy-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b1f5b45829ac1848893f0ddf5cb326110604d6df96cdc255b0bf9edd154104d4", size = 12623460 }, + { url = "https://files.pythonhosted.org/packages/a4/4f/1f8475907d1a7c4ef9020edf7f39ea2422ec896849245f00688e4b268a71/numpy-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:23a3e9d1a6f360267e8fbb38ba5db355a6a7e9be71d7fce7ab3125e88bb646c8", size = 10661799 }, + { url = "https://files.pythonhosted.org/packages/4b/ef/088e7c7342f300aaf3ee5f2c821c4b9996a1bef2aaf6a49cc8ab4883758e/numpy-2.4.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b54c83f1c0c0f1d748dca0af516062b8829d53d1f0c402be24b4257a9c48ada6", size = 16819003 }, + { url = "https://files.pythonhosted.org/packages/ff/ce/a53017b5443b4b84517182d463fc7bcc2adb4faa8b20813f8e5f5aeb5faa/numpy-2.4.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:aabb081ca0ec5d39591fc33018cd4b3f96e1a2dd6756282029986d00a785fba4", size = 12567105 }, + { url = "https://files.pythonhosted.org/packages/77/58/5ff91b161f2ec650c88a626c3905d938c89aaadabd0431e6d9c1330c83e2/numpy-2.4.0-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:8eafe7c36c8430b7794edeab3087dec7bf31d634d92f2af9949434b9d1964cba", size = 5395590 }, + { url = "https://files.pythonhosted.org/packages/1d/4e/f1a084106df8c2df8132fc437e56987308e0524836aa7733721c8429d4fe/numpy-2.4.0-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2f585f52b2baf07ff3356158d9268ea095e221371f1074fadea2f42544d58b4d", size = 6709947 }, + { url = "https://files.pythonhosted.org/packages/63/09/3d8aeb809c0332c3f642da812ac2e3d74fc9252b3021f8c30c82e99e3f3d/numpy-2.4.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32ed06d0fe9cae27d8fb5f400c63ccee72370599c75e683a6358dd3a4fb50aaf", size = 14535119 }, + { url = "https://files.pythonhosted.org/packages/fd/7f/68f0fc43a2cbdc6bb239160c754d87c922f60fbaa0fa3cd3d312b8a7f5ee/numpy-2.4.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:57c540ed8fb1f05cb997c6761cd56db72395b0d6985e90571ff660452ade4f98", size = 16475815 }, + { url = "https://files.pythonhosted.org/packages/11/73/edeacba3167b1ca66d51b1a5a14697c2c40098b5ffa01811c67b1785a5ab/numpy-2.4.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a39fb973a726e63223287adc6dafe444ce75af952d711e400f3bf2b36ef55a7b", size = 12489376 }, +] + +[[package]] +name = "ompython" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", + "python_full_version < '3.9'", +] +dependencies = [ + { name = "future", marker = "python_full_version < '3.10'" }, + { name = "numpy", version = "1.24.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "psutil", marker = "python_full_version < '3.10'" }, + { name = "pyparsing", version = "3.1.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "pyparsing", version = "3.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "pyzmq", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/fe/9d54052f772ce6292687107d3bd5c4cb39f35f6b1b36e694cdf0bf54430b/OMPython-3.6.0.tar.gz", hash = "sha256:3b423bd9bab64a8224e029994fe98f833773d761998c878d5e69aa6e66b171ef", size = 27669 } + +[[package]] +name = "ompython" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "numpy", version = "2.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "psutil", marker = "python_full_version >= '3.10'" }, + { name = "pyparsing", version = "3.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pyzmq", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/10/13c2f0f5c3f8062092b700fe5cb21face8176ed4829181d35152fe7e32d2/ompython-4.0.0.tar.gz", hash = "sha256:3c048af806084181ede649de58fbad3464876d1aa0af8b5bc07eb9803fed49d4", size = 44982 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/a8/1f1f811d1a2f08d4215ff3938cb43ce397256af12ed407eb4034f7d56767/ompython-4.0.0-py3-none-any.whl", hash = "sha256:3625bd8684b77c29eb888e17467889d87eff6ff34024b4b73d0bfead840a8b2e", size = 42297 }, +] + [[package]] name = "packaging" version = "25.0" @@ -1537,7 +1905,8 @@ name = "pipx" version = "1.8.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.10'", + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", "python_full_version == '3.9.*'", ] dependencies = [ @@ -1583,7 +1952,8 @@ name = "platformdirs" version = "4.5.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.10'", + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", ] sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632 } wheels = [ @@ -1607,7 +1977,8 @@ name = "pluggy" version = "1.6.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.10'", + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", "python_full_version == '3.9.*'", ] sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 } @@ -1615,6 +1986,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, ] +[[package]] +name = "psutil" +version = "7.1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/88/bdd0a41e5857d5d703287598cbf08dad90aed56774ea52ae071bae9071b6/psutil-7.1.3.tar.gz", hash = "sha256:6c86281738d77335af7aec228328e944b30930899ea760ecf33a4dba66be5e74", size = 489059 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/93/0c49e776b8734fef56ec9c5c57f923922f2cf0497d62e0f419465f28f3d0/psutil-7.1.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0005da714eee687b4b8decd3d6cc7c6db36215c9e74e5ad2264b90c3df7d92dc", size = 239751 }, + { url = "https://files.pythonhosted.org/packages/6f/8d/b31e39c769e70780f007969815195a55c81a63efebdd4dbe9e7a113adb2f/psutil-7.1.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:19644c85dcb987e35eeeaefdc3915d059dac7bd1167cdcdbf27e0ce2df0c08c0", size = 240368 }, + { url = "https://files.pythonhosted.org/packages/62/61/23fd4acc3c9eebbf6b6c78bcd89e5d020cfde4acf0a9233e9d4e3fa698b4/psutil-7.1.3-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95ef04cf2e5ba0ab9eaafc4a11eaae91b44f4ef5541acd2ee91d9108d00d59a7", size = 287134 }, + { url = "https://files.pythonhosted.org/packages/30/1c/f921a009ea9ceb51aa355cb0cc118f68d354db36eae18174bab63affb3e6/psutil-7.1.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1068c303be3a72f8e18e412c5b2a8f6d31750fb152f9cb106b54090296c9d251", size = 289904 }, + { url = "https://files.pythonhosted.org/packages/a6/82/62d68066e13e46a5116df187d319d1724b3f437ddd0f958756fc052677f4/psutil-7.1.3-cp313-cp313t-win_amd64.whl", hash = "sha256:18349c5c24b06ac5612c0428ec2a0331c26443d259e2a0144a9b24b4395b58fa", size = 249642 }, + { url = "https://files.pythonhosted.org/packages/df/ad/c1cd5fe965c14a0392112f68362cfceb5230819dbb5b1888950d18a11d9f/psutil-7.1.3-cp313-cp313t-win_arm64.whl", hash = "sha256:c525ffa774fe4496282fb0b1187725793de3e7c6b29e41562733cae9ada151ee", size = 245518 }, + { url = "https://files.pythonhosted.org/packages/2e/bb/6670bded3e3236eb4287c7bcdc167e9fae6e1e9286e437f7111caed2f909/psutil-7.1.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b403da1df4d6d43973dc004d19cee3b848e998ae3154cc8097d139b77156c353", size = 239843 }, + { url = "https://files.pythonhosted.org/packages/b8/66/853d50e75a38c9a7370ddbeefabdd3d3116b9c31ef94dc92c6729bc36bec/psutil-7.1.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ad81425efc5e75da3f39b3e636293360ad8d0b49bed7df824c79764fb4ba9b8b", size = 240369 }, + { url = "https://files.pythonhosted.org/packages/41/bd/313aba97cb5bfb26916dc29cf0646cbe4dd6a89ca69e8c6edce654876d39/psutil-7.1.3-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f33a3702e167783a9213db10ad29650ebf383946e91bc77f28a5eb083496bc9", size = 288210 }, + { url = "https://files.pythonhosted.org/packages/c2/fa/76e3c06e760927a0cfb5705eb38164254de34e9bd86db656d4dbaa228b04/psutil-7.1.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fac9cd332c67f4422504297889da5ab7e05fd11e3c4392140f7370f4208ded1f", size = 291182 }, + { url = "https://files.pythonhosted.org/packages/0f/1d/5774a91607035ee5078b8fd747686ebec28a962f178712de100d00b78a32/psutil-7.1.3-cp314-cp314t-win_amd64.whl", hash = "sha256:3792983e23b69843aea49c8f5b8f115572c5ab64c153bada5270086a2123c7e7", size = 250466 }, + { url = "https://files.pythonhosted.org/packages/00/ca/e426584bacb43a5cb1ac91fae1937f478cd8fbe5e4ff96574e698a2c77cd/psutil-7.1.3-cp314-cp314t-win_arm64.whl", hash = "sha256:31d77fcedb7529f27bb3a0472bea9334349f9a04160e8e6e5020f22c59893264", size = 245756 }, + { url = "https://files.pythonhosted.org/packages/ef/94/46b9154a800253e7ecff5aaacdf8ebf43db99de4a2dfa18575b02548654e/psutil-7.1.3-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2bdbcd0e58ca14996a42adf3621a6244f1bb2e2e528886959c72cf1e326677ab", size = 238359 }, + { url = "https://files.pythonhosted.org/packages/68/3a/9f93cff5c025029a36d9a92fef47220ab4692ee7f2be0fba9f92813d0cb8/psutil-7.1.3-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:bc31fa00f1fbc3c3802141eede66f3a2d51d89716a194bf2cd6fc68310a19880", size = 239171 }, + { url = "https://files.pythonhosted.org/packages/ce/b1/5f49af514f76431ba4eea935b8ad3725cdeb397e9245ab919dbc1d1dc20f/psutil-7.1.3-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bb428f9f05c1225a558f53e30ccbad9930b11c3fc206836242de1091d3e7dd3", size = 263261 }, + { url = "https://files.pythonhosted.org/packages/e0/95/992c8816a74016eb095e73585d747e0a8ea21a061ed3689474fabb29a395/psutil-7.1.3-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56d974e02ca2c8eb4812c3f76c30e28836fffc311d55d979f1465c1feeb2b68b", size = 264635 }, + { url = "https://files.pythonhosted.org/packages/55/4c/c3ed1a622b6ae2fd3c945a366e64eb35247a31e4db16cf5095e269e8eb3c/psutil-7.1.3-cp37-abi3-win_amd64.whl", hash = "sha256:f39c2c19fe824b47484b96f9692932248a54c43799a84282cfe58d05a6449efd", size = 247633 }, + { url = "https://files.pythonhosted.org/packages/c9/ad/33b2ccec09bf96c2b2ef3f9a6f66baac8253d7565d8839e024a6b905d45d/psutil-7.1.3-cp37-abi3-win_arm64.whl", hash = "sha256:bd0d69cee829226a761e92f28140bec9a5ee9d5b4fb4b0cc589068dbfff559b1", size = 244608 }, +] + [[package]] name = "pybind11" version = "3.0.1" @@ -1641,7 +2038,8 @@ name = "pycodestyle" version = "2.14.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.10'", + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", "python_full_version == '3.9.*'", ] sdist = { url = "https://files.pythonhosted.org/packages/11/e0/abfd2a0d2efe47670df87f3e3a0e2edda42f055053c85361f19c0e2c1ca8/pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783", size = 39472 } @@ -1687,7 +2085,8 @@ name = "pyflakes" version = "3.4.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.10'", + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", "python_full_version == '3.9.*'", ] sdist = { url = "https://files.pythonhosted.org/packages/45/dc/fd034dc20b4b264b3d015808458391acbf9df40b1e54750ef175d39180b1/pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58", size = 64669 } @@ -1704,6 +2103,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217 }, ] +[[package]] +name = "pyparsing" +version = "3.1.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/83/08/13f3bce01b2061f2bbd582c9df82723de943784cf719a35ac886c652043a/pyparsing-3.1.4.tar.gz", hash = "sha256:f86ec8d1a83f11977c9a6ea7598e8c27fc5cddfa5b07ea2241edbbde1d7bc032", size = 900231 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/0c/0e3c05b1c87bb6a1c76d281b0f35e78d2d80ac91b5f8f524cebf77f51049/pyparsing-3.1.4-py3-none-any.whl", hash = "sha256:a6a7ee4235a3f944aa1fa2249307708f893fe5717dc603503c6c7969c070fb7c", size = 104100 }, +] + +[[package]] +name = "pyparsing" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/33/c1/1d9de9aeaa1b89b0186e5fe23294ff6517fce1bc69149185577cd31016b2/pyparsing-3.3.1.tar.gz", hash = "sha256:47fad0f17ac1e2cad3de3b458570fbc9b03560aa029ed5e16ee5554da9a2251c", size = 1550512 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/40/2614036cdd416452f5bf98ec037f38a1afb17f327cb8e6b652d4729e0af8/pyparsing-3.3.1-py3-none-any.whl", hash = "sha256:023b5e7e5520ad96642e2c6db4cb683d3970bd640cdf7115049a6e9c3682df82", size = 121793 }, +] + [[package]] name = "pyproject-hooks" version = "1.2.0" @@ -1738,7 +2163,8 @@ name = "pytest" version = "8.4.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.10'", + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", "python_full_version == '3.9.*'", ] dependencies = [ @@ -1777,7 +2203,8 @@ name = "pytest-cov" version = "7.0.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.10'", + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", "python_full_version == '3.9.*'", ] dependencies = [ @@ -1809,6 +2236,109 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756 }, ] +[[package]] +name = "pyzmq" +version = "27.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9' and implementation_name == 'pypy'" }, + { name = "cffi", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and implementation_name == 'pypy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/b9/52aa9ec2867528b54f1e60846728d8b4d84726630874fee3a91e66c7df81/pyzmq-27.1.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:508e23ec9bc44c0005c4946ea013d9317ae00ac67778bd47519fdf5a0e930ff4", size = 1329850 }, + { url = "https://files.pythonhosted.org/packages/99/64/5653e7b7425b169f994835a2b2abf9486264401fdef18df91ddae47ce2cc/pyzmq-27.1.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:507b6f430bdcf0ee48c0d30e734ea89ce5567fd7b8a0f0044a369c176aa44556", size = 906380 }, + { url = "https://files.pythonhosted.org/packages/73/78/7d713284dbe022f6440e391bd1f3c48d9185673878034cfb3939cdf333b2/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf7b38f9fd7b81cb6d9391b2946382c8237fd814075c6aa9c3b746d53076023b", size = 666421 }, + { url = "https://files.pythonhosted.org/packages/30/76/8f099f9d6482450428b17c4d6b241281af7ce6a9de8149ca8c1c649f6792/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03ff0b279b40d687691a6217c12242ee71f0fba28bf8626ff50e3ef0f4410e1e", size = 854149 }, + { url = "https://files.pythonhosted.org/packages/59/f0/37fbfff06c68016019043897e4c969ceab18bde46cd2aca89821fcf4fb2e/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:677e744fee605753eac48198b15a2124016c009a11056f93807000ab11ce6526", size = 1655070 }, + { url = "https://files.pythonhosted.org/packages/47/14/7254be73f7a8edc3587609554fcaa7bfd30649bf89cd260e4487ca70fdaa/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd2fec2b13137416a1c5648b7009499bcc8fea78154cd888855fa32514f3dad1", size = 2033441 }, + { url = "https://files.pythonhosted.org/packages/22/dc/49f2be26c6f86f347e796a4d99b19167fc94503f0af3fd010ad262158822/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:08e90bb4b57603b84eab1d0ca05b3bbb10f60c1839dc471fc1c9e1507bef3386", size = 1891529 }, + { url = "https://files.pythonhosted.org/packages/a3/3e/154fb963ae25be70c0064ce97776c937ecc7d8b0259f22858154a9999769/pyzmq-27.1.0-cp310-cp310-win32.whl", hash = "sha256:a5b42d7a0658b515319148875fcb782bbf118dd41c671b62dae33666c2213bda", size = 567276 }, + { url = "https://files.pythonhosted.org/packages/62/b2/f4ab56c8c595abcb26b2be5fd9fa9e6899c1e5ad54964e93ae8bb35482be/pyzmq-27.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:c0bb87227430ee3aefcc0ade2088100e528d5d3298a0a715a64f3d04c60ba02f", size = 632208 }, + { url = "https://files.pythonhosted.org/packages/3b/e3/be2cc7ab8332bdac0522fdb64c17b1b6241a795bee02e0196636ec5beb79/pyzmq-27.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:9a916f76c2ab8d045b19f2286851a38e9ac94ea91faf65bd64735924522a8b32", size = 559766 }, + { url = "https://files.pythonhosted.org/packages/06/5d/305323ba86b284e6fcb0d842d6adaa2999035f70f8c38a9b6d21ad28c3d4/pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86", size = 1333328 }, + { url = "https://files.pythonhosted.org/packages/bd/a0/fc7e78a23748ad5443ac3275943457e8452da67fda347e05260261108cbc/pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581", size = 908803 }, + { url = "https://files.pythonhosted.org/packages/7e/22/37d15eb05f3bdfa4abea6f6d96eb3bb58585fbd3e4e0ded4e743bc650c97/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f", size = 668836 }, + { url = "https://files.pythonhosted.org/packages/b1/c4/2a6fe5111a01005fc7af3878259ce17684fabb8852815eda6225620f3c59/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bbf8d3630bf96550b3be8e1fc0fea5cbdc8d5466c1192887bd94869da17a63e", size = 857038 }, + { url = "https://files.pythonhosted.org/packages/cb/eb/bfdcb41d0db9cd233d6fb22dc131583774135505ada800ebf14dfb0a7c40/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e", size = 1657531 }, + { url = "https://files.pythonhosted.org/packages/ab/21/e3180ca269ed4a0de5c34417dfe71a8ae80421198be83ee619a8a485b0c7/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2", size = 2034786 }, + { url = "https://files.pythonhosted.org/packages/3b/b1/5e21d0b517434b7f33588ff76c177c5a167858cc38ef740608898cd329f2/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394", size = 1894220 }, + { url = "https://files.pythonhosted.org/packages/03/f2/44913a6ff6941905efc24a1acf3d3cb6146b636c546c7406c38c49c403d4/pyzmq-27.1.0-cp311-cp311-win32.whl", hash = "sha256:6df079c47d5902af6db298ec92151db82ecb557af663098b92f2508c398bb54f", size = 567155 }, + { url = "https://files.pythonhosted.org/packages/23/6d/d8d92a0eb270a925c9b4dd039c0b4dc10abc2fcbc48331788824ef113935/pyzmq-27.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97", size = 633428 }, + { url = "https://files.pythonhosted.org/packages/ae/14/01afebc96c5abbbd713ecfc7469cfb1bc801c819a74ed5c9fad9a48801cb/pyzmq-27.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:eca6b47df11a132d1745eb3b5b5e557a7dae2c303277aa0e69c6ba91b8736e07", size = 559497 }, + { url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279 }, + { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645 }, + { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574 }, + { url = "https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31", size = 840995 }, + { url = "https://files.pythonhosted.org/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070 }, + { url = "https://files.pythonhosted.org/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121 }, + { url = "https://files.pythonhosted.org/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550 }, + { url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184 }, + { url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480 }, + { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993 }, + { url = "https://files.pythonhosted.org/packages/60/cb/84a13459c51da6cec1b7b1dc1a47e6db6da50b77ad7fd9c145842750a011/pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5", size = 1122436 }, + { url = "https://files.pythonhosted.org/packages/dc/b6/94414759a69a26c3dd674570a81813c46a078767d931a6c70ad29fc585cb/pyzmq-27.1.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6", size = 1156301 }, + { url = "https://files.pythonhosted.org/packages/a5/ad/15906493fd40c316377fd8a8f6b1f93104f97a752667763c9b9c1b71d42d/pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7", size = 1341197 }, + { url = "https://files.pythonhosted.org/packages/14/1d/d343f3ce13db53a54cb8946594e567410b2125394dafcc0268d8dda027e0/pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05", size = 897275 }, + { url = "https://files.pythonhosted.org/packages/69/2d/d83dd6d7ca929a2fc67d2c3005415cdf322af7751d773524809f9e585129/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9", size = 660469 }, + { url = "https://files.pythonhosted.org/packages/3e/cd/9822a7af117f4bc0f1952dbe9ef8358eb50a24928efd5edf54210b850259/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128", size = 847961 }, + { url = "https://files.pythonhosted.org/packages/9a/12/f003e824a19ed73be15542f172fd0ec4ad0b60cf37436652c93b9df7c585/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39", size = 1650282 }, + { url = "https://files.pythonhosted.org/packages/d5/4a/e82d788ed58e9a23995cee70dbc20c9aded3d13a92d30d57ec2291f1e8a3/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97", size = 2024468 }, + { url = "https://files.pythonhosted.org/packages/d9/94/2da0a60841f757481e402b34bf4c8bf57fa54a5466b965de791b1e6f747d/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db", size = 1885394 }, + { url = "https://files.pythonhosted.org/packages/4f/6f/55c10e2e49ad52d080dc24e37adb215e5b0d64990b57598abc2e3f01725b/pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c", size = 574964 }, + { url = "https://files.pythonhosted.org/packages/87/4d/2534970ba63dd7c522d8ca80fb92777f362c0f321900667c615e2067cb29/pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2", size = 641029 }, + { url = "https://files.pythonhosted.org/packages/f6/fa/f8aea7a28b0641f31d40dea42d7ef003fded31e184ef47db696bc74cd610/pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e", size = 561541 }, + { url = "https://files.pythonhosted.org/packages/87/45/19efbb3000956e82d0331bafca5d9ac19ea2857722fa2caacefb6042f39d/pyzmq-27.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a", size = 1341197 }, + { url = "https://files.pythonhosted.org/packages/48/43/d72ccdbf0d73d1343936296665826350cb1e825f92f2db9db3e61c2162a2/pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea", size = 897175 }, + { url = "https://files.pythonhosted.org/packages/2f/2e/a483f73a10b65a9ef0161e817321d39a770b2acf8bcf3004a28d90d14a94/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96", size = 660427 }, + { url = "https://files.pythonhosted.org/packages/f5/d2/5f36552c2d3e5685abe60dfa56f91169f7a2d99bbaf67c5271022ab40863/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d", size = 847929 }, + { url = "https://files.pythonhosted.org/packages/c4/2a/404b331f2b7bf3198e9945f75c4c521f0c6a3a23b51f7a4a401b94a13833/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146", size = 1650193 }, + { url = "https://files.pythonhosted.org/packages/1c/0b/f4107e33f62a5acf60e3ded67ed33d79b4ce18de432625ce2fc5093d6388/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd", size = 2024388 }, + { url = "https://files.pythonhosted.org/packages/0d/01/add31fe76512642fd6e40e3a3bd21f4b47e242c8ba33efb6809e37076d9b/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a", size = 1885316 }, + { url = "https://files.pythonhosted.org/packages/c4/59/a5f38970f9bf07cee96128de79590bb354917914a9be11272cfc7ff26af0/pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92", size = 587472 }, + { url = "https://files.pythonhosted.org/packages/70/d8/78b1bad170f93fcf5e3536e70e8fadac55030002275c9a29e8f5719185de/pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0", size = 661401 }, + { url = "https://files.pythonhosted.org/packages/81/d6/4bfbb40c9a0b42fc53c7cf442f6385db70b40f74a783130c5d0a5aa62228/pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7", size = 575170 }, + { url = "https://files.pythonhosted.org/packages/38/f8/946ecde123eaffe933ecf287186495d5f22a8bf444bcb774d9c83dcb2fa5/pyzmq-27.1.0-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:18339186c0ed0ce5835f2656cdfb32203125917711af64da64dbaa3d949e5a1b", size = 1332188 }, + { url = "https://files.pythonhosted.org/packages/56/08/5960fd162bf1e0e22f251c2f7744101241bc419fbc52abab4108260eb3e0/pyzmq-27.1.0-cp38-cp38-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:753d56fba8f70962cd8295fb3edb40b9b16deaa882dd2b5a3a2039f9ff7625aa", size = 907319 }, + { url = "https://files.pythonhosted.org/packages/7f/62/2d8712aafbd7fcf0e303d67c1d923f64a41aa872f1348e3d5dcec147c909/pyzmq-27.1.0-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b721c05d932e5ad9ff9344f708c96b9e1a485418c6618d765fca95d4daacfbef", size = 864213 }, + { url = "https://files.pythonhosted.org/packages/e1/04/e9a1550d2dcb29cd662d88c89e9fe975393dd577e2c8b2c528d0a0bacfac/pyzmq-27.1.0-cp38-cp38-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be883ff3d722e6085ee3f4afc057a50f7f2e0c72d289fd54df5706b4e3d3a50", size = 668520 }, + { url = "https://files.pythonhosted.org/packages/48/ad/1638518b7554686d17b5fdd0c0381c13656fe4899dc13af0ba10850d56f0/pyzmq-27.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:b2e592db3a93128daf567de9650a2f3859017b3f7a66bc4ed6e4779d6034976f", size = 1657582 }, + { url = "https://files.pythonhosted.org/packages/cc/b7/6cb8123ee217c1efa8e917feabe86425185a7b55504af32bffa057dcd91d/pyzmq-27.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ad68808a61cbfbbae7ba26d6233f2a4aa3b221de379ce9ee468aa7a83b9c36b0", size = 2035054 }, + { url = "https://files.pythonhosted.org/packages/cb/95/8d6ec87b43e1d8608be461165180fec4744da9edceea4ce48c7bd8c60402/pyzmq-27.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:e2687c2d230e8d8584fbea433c24382edfeda0c60627aca3446aa5e58d5d1831", size = 1894186 }, + { url = "https://files.pythonhosted.org/packages/a7/2a/7806479dd1f1b964d0aa07f1d961fcaa8673ed543c911847fc45e91f103a/pyzmq-27.1.0-cp38-cp38-win32.whl", hash = "sha256:a1aa0ee920fb3825d6c825ae3f6c508403b905b698b6460408ebd5bb04bbb312", size = 567508 }, + { url = "https://files.pythonhosted.org/packages/9f/24/70e83d3ff64ef7e3d6666bd30a241be695dad0ef30d5519bf9c5ff174786/pyzmq-27.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:df7cd397ece96cf20a76fae705d40efbab217d217897a5053267cd88a700c266", size = 632740 }, + { url = "https://files.pythonhosted.org/packages/ac/4e/782eb6df91b6a9d9afa96c2dcfc5cac62562a68eb62a02210101f886014d/pyzmq-27.1.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:96c71c32fff75957db6ae33cd961439f386505c6e6b377370af9b24a1ef9eafb", size = 1330426 }, + { url = "https://files.pythonhosted.org/packages/8d/ca/2b8693d06b1db4e0c084871e4c9d7842b561d0a6ff9d780640f5e3e9eb55/pyzmq-27.1.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:49d3980544447f6bd2968b6ac913ab963a49dcaa2d4a2990041f16057b04c429", size = 906559 }, + { url = "https://files.pythonhosted.org/packages/6a/b3/b99b39e2cfdcebd512959780e4d299447fd7f46010b1d88d63324e2481ec/pyzmq-27.1.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:849ca054d81aa1c175c49484afaaa5db0622092b5eccb2055f9f3bb8f703782d", size = 863816 }, + { url = "https://files.pythonhosted.org/packages/61/b2/018fa8e8eefb34a625b1a45e2effcbc9885645b22cdd0a68283f758351e7/pyzmq-27.1.0-cp39-cp39-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3970778e74cb7f85934d2b926b9900e92bfe597e62267d7499acc39c9c28e345", size = 666735 }, + { url = "https://files.pythonhosted.org/packages/01/05/8ae778f7cd7c94030731ae2305e6a38f3a333b6825f56c0c03f2134ccf1b/pyzmq-27.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:da96ecdcf7d3919c3be2de91a8c513c186f6762aa6cf7c01087ed74fad7f0968", size = 1655425 }, + { url = "https://files.pythonhosted.org/packages/ad/ad/d69478a97a3f3142f9dbbbd9daa4fcf42541913a85567c36d4cfc19b2218/pyzmq-27.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9541c444cfe1b1c0156c5c86ece2bb926c7079a18e7b47b0b1b3b1b875e5d098", size = 2033729 }, + { url = "https://files.pythonhosted.org/packages/9a/6d/e3c6ad05bc1cddd25094e66cc15ae8924e15c67e231e93ed2955c401007e/pyzmq-27.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e30a74a39b93e2e1591b58eb1acef4902be27c957a8720b0e368f579b82dc22f", size = 1891803 }, + { url = "https://files.pythonhosted.org/packages/7f/a7/97e8be0daaca157511563160b67a13d4fe76b195e3fa6873cb554ad46be3/pyzmq-27.1.0-cp39-cp39-win32.whl", hash = "sha256:b1267823d72d1e40701dcba7edc45fd17f71be1285557b7fe668887150a14b78", size = 567627 }, + { url = "https://files.pythonhosted.org/packages/5c/91/70bbf3a7c5b04c904261ef5ba224d8a76315f6c23454251bf5f55573a8a1/pyzmq-27.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0c996ded912812a2fcd7ab6574f4ad3edc27cb6510349431e4930d4196ade7db", size = 632315 }, + { url = "https://files.pythonhosted.org/packages/cc/b5/a4173a83c7fd37f6bdb5a800ea338bc25603284e9ef8681377cec006ede4/pyzmq-27.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:346e9ba4198177a07e7706050f35d733e08c1c1f8ceacd5eb6389d653579ffbc", size = 559833 }, + { url = "https://files.pythonhosted.org/packages/f3/81/a65e71c1552f74dec9dff91d95bafb6e0d33338a8dfefbc88aa562a20c92/pyzmq-27.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c17e03cbc9312bee223864f1a2b13a99522e0dc9f7c5df0177cd45210ac286e6", size = 836266 }, + { url = "https://files.pythonhosted.org/packages/58/ed/0202ca350f4f2b69faa95c6d931e3c05c3a397c184cacb84cb4f8f42f287/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f328d01128373cb6763823b2b4e7f73bdf767834268c565151eacb3b7a392f90", size = 800206 }, + { url = "https://files.pythonhosted.org/packages/47/42/1ff831fa87fe8f0a840ddb399054ca0009605d820e2b44ea43114f5459f4/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1790386614232e1b3a40a958454bdd42c6d1811837b15ddbb052a032a43f62", size = 567747 }, + { url = "https://files.pythonhosted.org/packages/d1/db/5c4d6807434751e3f21231bee98109aa57b9b9b55e058e450d0aef59b70f/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:448f9cb54eb0cee4732b46584f2710c8bc178b0e5371d9e4fc8125201e413a74", size = 747371 }, + { url = "https://files.pythonhosted.org/packages/26/af/78ce193dbf03567eb8c0dc30e3df2b9e56f12a670bf7eb20f9fb532c7e8a/pyzmq-27.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:05b12f2d32112bf8c95ef2e74ec4f1d4beb01f8b5e703b38537f8849f92cb9ba", size = 544862 }, + { url = "https://files.pythonhosted.org/packages/4c/c6/c4dcdecdbaa70969ee1fdced6d7b8f60cfabe64d25361f27ac4665a70620/pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066", size = 836265 }, + { url = "https://files.pythonhosted.org/packages/3e/79/f38c92eeaeb03a2ccc2ba9866f0439593bb08c5e3b714ac1d553e5c96e25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604", size = 800208 }, + { url = "https://files.pythonhosted.org/packages/49/0e/3f0d0d335c6b3abb9b7b723776d0b21fa7f3a6c819a0db6097059aada160/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c", size = 567747 }, + { url = "https://files.pythonhosted.org/packages/a1/cf/f2b3784d536250ffd4be70e049f3b60981235d70c6e8ce7e3ef21e1adb25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271", size = 747371 }, + { url = "https://files.pythonhosted.org/packages/01/1b/5dbe84eefc86f48473947e2f41711aded97eecef1231f4558f1f02713c12/pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355", size = 544862 }, + { url = "https://files.pythonhosted.org/packages/eb/d8/2cf36ee6d037b52640997bde488d046db55bdea05e34229cf9cd3154fd7d/pyzmq-27.1.0-pp38-pypy38_pp73-macosx_10_15_x86_64.whl", hash = "sha256:50081a4e98472ba9f5a02850014b4c9b629da6710f8f14f3b15897c666a28f1b", size = 836250 }, + { url = "https://files.pythonhosted.org/packages/e5/40/5ff9acff898558fb54731d4b897d5bf16b3725e0c1778166ac9a234b5297/pyzmq-27.1.0-pp38-pypy38_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:510869f9df36ab97f89f4cff9d002a89ac554c7ac9cadd87d444aa4cf66abd27", size = 800201 }, + { url = "https://files.pythonhosted.org/packages/2f/58/f941950f64c5e7919c64d36e52991ade7ac8ea4805e9d2cdba47337d9edc/pyzmq-27.1.0-pp38-pypy38_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1f8426a01b1c4098a750973c37131cf585f61c7911d735f729935a0c701b68d3", size = 758755 }, + { url = "https://files.pythonhosted.org/packages/7b/26/ddd3502658bf85d41ab6d75dcab78a7af5bb32fb5f7ac38bd7cf1bce321d/pyzmq-27.1.0-pp38-pypy38_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:726b6a502f2e34c6d2ada5e702929586d3ac948a4dbbb7fed9854ec8c0466027", size = 567742 }, + { url = "https://files.pythonhosted.org/packages/36/ad/50515db14fb3c19d48a2a05716c7f4d658da51ea2b145c67f003b3f443d2/pyzmq-27.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:bd67e7c8f4654bef471c0b1ca6614af0b5202a790723a58b79d9584dc8022a78", size = 544859 }, + { url = "https://files.pythonhosted.org/packages/57/f4/c2e978cf6b833708bad7d6396c3a20c19750585a1775af3ff13c435e1912/pyzmq-27.1.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:722ea791aa233ac0a819fc2c475e1292c76930b31f1d828cb61073e2fe5e208f", size = 836257 }, + { url = "https://files.pythonhosted.org/packages/5f/5f/4e10c7f57a4c92ab0fbb2396297aa8d618e6f5b9b8f8e9756d56f3e6fc52/pyzmq-27.1.0-pp39-pypy39_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:01f9437501886d3a1dd4b02ef59fb8cc384fa718ce066d52f175ee49dd5b7ed8", size = 800203 }, + { url = "https://files.pythonhosted.org/packages/19/72/a74a007cd636f903448c6ab66628104b1fc5f2ba018733d5eabb94a0a6fb/pyzmq-27.1.0-pp39-pypy39_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4a19387a3dddcc762bfd2f570d14e2395b2c9701329b266f83dd87a2b3cbd381", size = 758756 }, + { url = "https://files.pythonhosted.org/packages/a9/d4/30c25b91f2b4786026372f5ef454134d7f576fcf4ac58539ad7dd5de4762/pyzmq-27.1.0-pp39-pypy39_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c618fbcd069e3a29dcd221739cacde52edcc681f041907867e0f5cc7e85f172", size = 567742 }, + { url = "https://files.pythonhosted.org/packages/92/aa/ee86edad943438cd0316964020c4b6d09854414f9f945f8e289ea6fcc019/pyzmq-27.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ff8d114d14ac671d88c89b9224c63d6c4e5a613fe8acd5594ce53d752a3aafe9", size = 544857 }, +] + [[package]] name = "readme-renderer" version = "43.0" @@ -1831,7 +2361,8 @@ name = "readme-renderer" version = "44.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.10'", + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", "python_full_version == '3.9.*'", ] dependencies = [ @@ -1867,7 +2398,8 @@ name = "requests" version = "2.32.5" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.10'", + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", "python_full_version == '3.9.*'", ] dependencies = [ @@ -1965,7 +2497,8 @@ name = "secretstorage" version = "3.4.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.10'", + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", ] dependencies = [ { name = "cryptography", marker = "python_full_version >= '3.10'" }, @@ -2075,7 +2608,8 @@ name = "twine" version = "6.2.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.10'", + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", "python_full_version == '3.9.*'", ] dependencies = [ @@ -2112,7 +2646,8 @@ name = "typing-extensions" version = "4.15.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.10'", + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", "python_full_version == '3.9.*'", ] sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391 } @@ -2137,7 +2672,8 @@ name = "urllib3" version = "2.5.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.10'", + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", "python_full_version == '3.9.*'", ] sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185 } @@ -2187,7 +2723,8 @@ name = "xvfbwrapper" version = "0.2.15" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.10'", + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", ] sdist = { url = "https://files.pythonhosted.org/packages/f7/4f/830b1e7324facdb8d3e05008174c6b487f79b22b6869a4c58e351a4c55e0/xvfbwrapper-0.2.15.tar.gz", hash = "sha256:695a4580ab4c9f69b653629ec243a873107536845879356f98d27569c43dac7e", size = 9027 } wheels = [ @@ -2211,7 +2748,8 @@ name = "zipp" version = "3.23.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.10'", + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", "python_full_version == '3.9.*'", ] sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547 } From 12335cf5df7cd8e2721ee9c9a406bc98be177dd5 Mon Sep 17 00:00:00 2001 From: Christophe Prud'homme Date: Sun, 25 Jan 2026 13:51:07 +0100 Subject: [PATCH 2/8] Refactor CI configuration to standardize runner specification --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6f2c3d0..c922c30 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ on: jobs: build_wheel: - runs-on: [self-hosted, ubuntu-24.04, gaya] + runs-on: self-ubuntu-24.04 continue-on-error: true strategy: fail-fast: false @@ -73,7 +73,7 @@ jobs: build_docs: needs: build_wheel - runs-on: [self-hosted, ubuntu-24.04, gaya] + runs-on: self-ubuntu-24.04 strategy: matrix: os: [ubuntu-24.04] From 40fc582c5c9abaca4a54ab47ed59480c4c54a170 Mon Sep 17 00:00:00 2001 From: Christophe Prud'homme Date: Sun, 25 Jan 2026 16:13:51 +0100 Subject: [PATCH 3/8] Add FMI 3.0 support and refactor API to camelCase with CLI subcommands, fix quality checks failures in CI Fixes #14 --- pyproject.toml | 2 + src/python/feelpp/mo2fmu/__init__.py | 32 +++-- .../feelpp/mo2fmu/compilers/__init__.py | 4 +- src/python/feelpp/mo2fmu/compilers/base.py | 16 +-- src/python/feelpp/mo2fmu/compilers/dymola.py | 16 +-- .../feelpp/mo2fmu/compilers/openmodelica.py | 50 ++++---- src/python/feelpp/mo2fmu/mo2fmu.py | 114 +++++++++--------- tests/conftest.py | 1 - tests/test_compilers.py | 6 +- tests/test_fmu_simulation.py | 40 ++---- 10 files changed, 132 insertions(+), 149 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 58ab82c..410f99d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -193,6 +193,8 @@ module = [ "dymola.*", "xvfbwrapper.*", "spdlog.*", + "OMPython.*", + "fmpy.*", ] ignore_missing_imports = true diff --git a/src/python/feelpp/mo2fmu/__init__.py b/src/python/feelpp/mo2fmu/__init__.py index d2c8db0..f6a5111 100644 --- a/src/python/feelpp/mo2fmu/__init__.py +++ b/src/python/feelpp/mo2fmu/__init__.py @@ -36,26 +36,21 @@ # Single source of truth: version comes from pyproject.toml __version__ = _get_version("feelpp-mo2fmu") __all__ = [ - # Primary API (camelCase) - "compileFmu", - "checkCompilers", - "getCompiler", - # CLI - "mo2fmuCLI", - # Compiler classes + "CompilationConfig", + "CompilationResult", "DymolaCompiler", "DymolaConfig", + "FMUCompiler", + "ModelicaModel", "OpenModelicaCompiler", "OpenModelicaConfig", - # Data classes - "CompilationConfig", - "CompilationResult", - "ModelicaModel", - "FMUCompiler", - # Legacy API (deprecated, for backward compatibility) + "checkCompilers", + "compileFmu", + "getCompiler", + "get_compiler", "mo2fmu", + "mo2fmuCLI", "mo2fmu_new", - "get_compiler", ] from feelpp.mo2fmu.compilers.base import ( @@ -65,15 +60,18 @@ ModelicaModel, ) from feelpp.mo2fmu.compilers.dymola import DymolaCompiler, DymolaConfig -from feelpp.mo2fmu.compilers.openmodelica import OpenModelicaCompiler, OpenModelicaConfig +from feelpp.mo2fmu.compilers.openmodelica import ( + OpenModelicaCompiler, + OpenModelicaConfig, +) from feelpp.mo2fmu.mo2fmu import ( # Primary API checkCompilers, compileFmu, - getCompiler, - mo2fmuCLI, # Legacy API (deprecated) get_compiler, + getCompiler, mo2fmu, mo2fmu_new, + mo2fmuCLI, ) diff --git a/src/python/feelpp/mo2fmu/compilers/__init__.py b/src/python/feelpp/mo2fmu/compilers/__init__.py index 8fc9b49..e22c59e 100644 --- a/src/python/feelpp/mo2fmu/compilers/__init__.py +++ b/src/python/feelpp/mo2fmu/compilers/__init__.py @@ -20,10 +20,10 @@ from feelpp.mo2fmu.compilers.openmodelica import OpenModelicaCompiler __all__ = [ - "FMUCompiler", "CompilationConfig", "CompilationResult", - "ModelicaModel", "DymolaCompiler", + "FMUCompiler", + "ModelicaModel", "OpenModelicaCompiler", ] diff --git a/src/python/feelpp/mo2fmu/compilers/base.py b/src/python/feelpp/mo2fmu/compilers/base.py index b4d01f0..8d615f4 100644 --- a/src/python/feelpp/mo2fmu/compilers/base.py +++ b/src/python/feelpp/mo2fmu/compilers/base.py @@ -23,7 +23,7 @@ class FMIType(Enum): CO_SIMULATION_SOLVER = "csSolver" @classmethod - def from_string(cls, value: str) -> "FMIType": + def from_string(cls, value: str) -> FMIType: """Convert string to FMIType.""" mapping = { "me": cls.MODEL_EXCHANGE, @@ -32,7 +32,8 @@ def from_string(cls, value: str) -> "FMIType": "csSolver": cls.CO_SIMULATION_SOLVER, } if value not in mapping: - raise ValueError(f"Invalid FMI type: {value}. Valid options: {list(mapping.keys())}") + msg = f"Invalid FMI type: {value}. Valid options: {list(mapping.keys())}" + raise ValueError(msg) return mapping[value] @@ -44,13 +45,12 @@ class FMIVersion(Enum): FMI_3_0 = "3" @classmethod - def from_string(cls, value: str) -> "FMIVersion": + def from_string(cls, value: str) -> FMIVersion: """Convert string to FMIVersion.""" mapping = {"1": cls.FMI_1_0, "2": cls.FMI_2_0, "3": cls.FMI_3_0} if value not in mapping: - raise ValueError( - f"Invalid FMI version: {value}. Valid options: {list(mapping.keys())}" - ) + msg = f"Invalid FMI version: {value}. Valid options: {list(mapping.keys())}" + raise ValueError(msg) return mapping[value] @@ -96,7 +96,7 @@ def _extract_package_name(self) -> Optional[str]: match = re.search(r"within\s+([\w.]+)\s*;", content) if match: return match.group(1) - except (OSError, IOError): + except OSError: pass return None @@ -137,7 +137,7 @@ def from_legacy( flags: Optional[tuple[str, ...]] = None, force: bool = False, verbose: bool = False, - ) -> "CompilationConfig": + ) -> CompilationConfig: """Create config from legacy mo2fmu parameters.""" return cls( fmi_type=FMIType.from_string(type), diff --git a/src/python/feelpp/mo2fmu/compilers/dymola.py b/src/python/feelpp/mo2fmu/compilers/dymola.py index b22ea19..4eed5ed 100644 --- a/src/python/feelpp/mo2fmu/compilers/dymola.py +++ b/src/python/feelpp/mo2fmu/compilers/dymola.py @@ -52,7 +52,7 @@ class DymolaConfig: additional_commands: list[str] = field(default_factory=list) @classmethod - def from_env(cls) -> "DymolaConfig": + def from_env(cls) -> DymolaConfig: """Create configuration from environment variables.""" return cls( root=os.getenv("DYMOLA_ROOT", cls.root), @@ -247,8 +247,8 @@ def compile( dymola = None try: - # Initialize Dymola - dymola = self._dymola_interface( + # Initialize Dymola (interface is guaranteed non-None after is_available check) + dymola = self._dymola_interface( # type: ignore[misc] dymolapath=self._config.executable, showwindow=False ) @@ -351,7 +351,8 @@ def check_model(self, model: ModelicaModel, packages: Optional[list[str]] = None dymola = None try: - dymola = self._dymola_interface( + # Interface is guaranteed non-None after is_available check + dymola = self._dymola_interface( # type: ignore[misc] dymolapath=self._config.executable, showwindow=False ) @@ -364,7 +365,7 @@ def check_model(self, model: ModelicaModel, packages: Optional[list[str]] = None dymola.openModel(str(model.path), changeDirectory=False) # Check the model - return dymola.checkModel(model.fully_qualified_name) + return bool(dymola.checkModel(model.fully_qualified_name)) except Exception: return False @@ -392,7 +393,8 @@ def validate_fmu(self, fmu_path: Path, simulate: bool = True) -> bool: dymola = None try: - dymola = self._dymola_interface( + # Interface is guaranteed non-None after is_available check + dymola = self._dymola_interface( # type: ignore[misc] dymolapath=self._config.executable, showwindow=False ) @@ -404,7 +406,7 @@ def validate_fmu(self, fmu_path: Path, simulate: bool = True) -> bool: if simulate: # Get model name from FMU fmu_model = f"{fmu_path.stem}_fmu" - return dymola.checkModel(problem=fmu_model, simulate=True) + return bool(dymola.checkModel(problem=fmu_model, simulate=True)) return True diff --git a/src/python/feelpp/mo2fmu/compilers/openmodelica.py b/src/python/feelpp/mo2fmu/compilers/openmodelica.py index 9d67307..d348aeb 100644 --- a/src/python/feelpp/mo2fmu/compilers/openmodelica.py +++ b/src/python/feelpp/mo2fmu/compilers/openmodelica.py @@ -13,6 +13,7 @@ from __future__ import annotations +import contextlib import os import shutil import subprocess @@ -59,7 +60,7 @@ class OpenModelicaConfig: command_line_options: list[str] = field(default_factory=list) @classmethod - def from_env(cls) -> "OpenModelicaConfig": + def from_env(cls) -> OpenModelicaConfig: """Create configuration from environment variables.""" return cls( omc_path=os.getenv("OPENMODELICA_HOME"), @@ -109,23 +110,22 @@ def _check_availability(self) -> None: try: result = subprocess.run( [omc_cmd, "--version"], + check=False, capture_output=True, text=True, timeout=10, ) self._omc_cli_available = result.returncode == 0 - except (subprocess.TimeoutExpired, FileNotFoundError, OSError): + except (subprocess.TimeoutExpired, OSError): self._omc_cli_available = False # Check OMPython (requires omc to be available) # OMPython is a Python interface to communicate with omc, so omc must be installed - try: - from OMPython import OMCSessionZMQ + import importlib.util - # OMPython package is installed, but it needs omc to work - self._ompython_available = self._omc_cli_available - except ImportError: - self._ompython_available = False + ompython_spec = importlib.util.find_spec("OMPython") + # OMPython package is installed, but it needs omc to work + self._ompython_available = ompython_spec is not None and self._omc_cli_available def _get_omc_command(self) -> str: """Get the omc command path.""" @@ -156,6 +156,7 @@ def get_version(self) -> Optional[str]: try: result = subprocess.run( [omc_cmd, "--version"], + check=False, capture_output=True, text=True, timeout=10, @@ -169,7 +170,7 @@ def get_version(self) -> Optional[str]: if part.startswith("v") or part[0].isdigit(): return part.lstrip("v") return version_line - except (subprocess.TimeoutExpired, FileNotFoundError, OSError): + except (subprocess.TimeoutExpired, OSError): pass return None @@ -248,7 +249,7 @@ def _compile_with_ompython( if pkg_path.suffix == ".mo": result = omc.sendExpression(f'loadFile("{pkg_path.as_posix()}")') else: - result = omc.sendExpression(f'loadModel({package})') + result = omc.sendExpression(f"loadModel({package})") if not result: error = omc.sendExpression("getErrorString()") @@ -357,10 +358,8 @@ def _compile_with_ompython( finally: if omc is not None: - try: + with contextlib.suppress(Exception): omc.sendExpression("quit()") - except Exception: - pass os.chdir(original_cwd) def _compile_with_cli( @@ -419,6 +418,7 @@ def _compile_with_cli( try: result = subprocess.run( [omc_cmd, str(script_path)], + check=False, capture_output=True, text=True, cwd=str(temp_path), @@ -511,13 +511,12 @@ def compile( # Choose compilation method if self._config.ompython_session and self._ompython_available: return self._compile_with_ompython(model, output_dir, config, logger) - elif self._omc_cli_available: + if self._omc_cli_available: return self._compile_with_cli(model, output_dir, config, logger) - else: - return CompilationResult( - success=False, - error_message="Neither OMPython nor omc CLI is available", - ) + return CompilationResult( + success=False, + error_message="Neither OMPython nor omc CLI is available", + ) def check_model(self, model: ModelicaModel, packages: Optional[list[str]] = None) -> bool: """Validate a Modelica model using OpenModelica. @@ -534,7 +533,7 @@ def check_model(self, model: ModelicaModel, packages: Optional[list[str]] = None if self._ompython_available: return self._check_model_ompython(model, packages) - elif self._omc_cli_available: + if self._omc_cli_available: return self._check_model_cli(model, packages) return False @@ -569,14 +568,10 @@ def _check_model_ompython( finally: if omc is not None: - try: + with contextlib.suppress(Exception): omc.sendExpression("quit()") - except Exception: - pass - def _check_model_cli( - self, model: ModelicaModel, packages: Optional[list[str]] = None - ) -> bool: + def _check_model_cli(self, model: ModelicaModel, packages: Optional[list[str]] = None) -> bool: """Check model using omc CLI.""" omc_cmd = self._get_omc_command() @@ -606,11 +601,12 @@ def _check_model_cli( try: result = subprocess.run( [omc_cmd, str(script_path)], + check=False, capture_output=True, text=True, cwd=str(temp_path), timeout=60, ) return result.returncode == 0 and "Error" not in result.stdout - except (subprocess.TimeoutExpired, FileNotFoundError, OSError): + except (subprocess.TimeoutExpired, OSError): return False diff --git a/src/python/feelpp/mo2fmu/mo2fmu.py b/src/python/feelpp/mo2fmu/mo2fmu.py index ffb072c..ac0e7ce 100644 --- a/src/python/feelpp/mo2fmu/mo2fmu.py +++ b/src/python/feelpp/mo2fmu/mo2fmu.py @@ -9,7 +9,7 @@ import warnings from importlib.metadata import version as get_version from pathlib import Path -from typing import Literal, Optional, Union +from typing import Literal, Optional import click @@ -23,7 +23,10 @@ ModelicaModel, ) from feelpp.mo2fmu.compilers.dymola import DymolaCompiler, DymolaConfig -from feelpp.mo2fmu.compilers.openmodelica import OpenModelicaCompiler, OpenModelicaConfig +from feelpp.mo2fmu.compilers.openmodelica import ( + OpenModelicaCompiler, + OpenModelicaConfig, +) # Type alias for backend selection Backend = Literal["dymola", "openmodelica", "auto"] @@ -37,7 +40,7 @@ def checkCompilers( dymolaConfig: Optional[DymolaConfig] = None, openModelicaConfig: Optional[OpenModelicaConfig] = None, -) -> dict: +) -> dict[str, dict[str, object]]: """Check availability and FMI version support for all compilers. Args: @@ -47,7 +50,7 @@ def checkCompilers( Returns: Dictionary with compiler availability and FMI support information """ - results = { + results: dict[str, dict[str, object]] = { "dymola": { "available": False, "version": None, @@ -64,43 +67,39 @@ def checkCompilers( dymola = DymolaCompiler(dymolaConfig) results["dymola"]["available"] = dymola.is_available if dymola.is_available: - results["dymola"]["version"] = dymola.get_version() + dymolaVersion = dymola.get_version() + results["dymola"]["version"] = dymolaVersion # Dymola 2024+ supports FMI 3.0, earlier versions support FMI 1.0 and 2.0 - versionStr = results["dymola"]["version"] - if versionStr: + fmiSupport: list[str] = ["1", "2"] + if dymolaVersion: try: # Version format: "2025.1" or "2024.1" - majorVersion = int(versionStr.split(".")[0]) - results["dymola"]["fmiSupport"] = ["1", "2"] + majorVersion = int(dymolaVersion.split(".")[0]) if majorVersion >= 2024: - results["dymola"]["fmiSupport"].append("3") + fmiSupport.append("3") except (ValueError, IndexError): - # Can't parse version, assume FMI 2.0 support - results["dymola"]["fmiSupport"] = ["1", "2"] - else: - results["dymola"]["fmiSupport"] = ["1", "2"] + pass # Keep default FMI 1.0 and 2.0 support + results["dymola"]["fmiSupport"] = fmiSupport # Check OpenModelica omc = OpenModelicaCompiler(openModelicaConfig) results["openmodelica"]["available"] = omc.is_available if omc.is_available: - results["openmodelica"]["version"] = omc.get_version() + omcVersion = omc.get_version() + results["openmodelica"]["version"] = omcVersion # OpenModelica 1.21+ supports FMI 3.0, earlier versions support FMI 1.0 and 2.0 - versionStr = results["openmodelica"]["version"] - if versionStr: + fmiSupport = ["1", "2"] + if omcVersion: try: # Version format: "1.22.0" or "1.21.0" - parts = versionStr.split(".") + parts = omcVersion.split(".") major = int(parts[0]) minor = int(parts[1]) if len(parts) > 1 else 0 - results["openmodelica"]["fmiSupport"] = ["1", "2"] if major > 1 or (major == 1 and minor >= 21): - results["openmodelica"]["fmiSupport"].append("3") + fmiSupport.append("3") except (ValueError, IndexError): - # Can't parse version, assume FMI 2.0 support - results["openmodelica"]["fmiSupport"] = ["1", "2"] - else: - results["openmodelica"]["fmiSupport"] = ["1", "2"] + pass # Keep default FMI 1.0 and 2.0 support + results["openmodelica"]["fmiSupport"] = fmiSupport return results @@ -124,18 +123,18 @@ def getCompiler( RuntimeError: If no suitable compiler is available """ if backend == "dymola": - compiler = DymolaCompiler(dymolaConfig) - if not compiler.is_available: - raise RuntimeError("Dymola is not available. Check installation and configuration.") - return compiler + dymolaCompiler = DymolaCompiler(dymolaConfig) + if not dymolaCompiler.is_available: + msg = "Dymola is not available. Check installation and configuration." + raise RuntimeError(msg) + return dymolaCompiler if backend == "openmodelica": - compiler = OpenModelicaCompiler(openModelicaConfig) - if not compiler.is_available: - raise RuntimeError( - "OpenModelica is not available. Install omc and/or OMPython." - ) - return compiler + omcCompiler = OpenModelicaCompiler(openModelicaConfig) + if not omcCompiler.is_available: + msg = "OpenModelica is not available. Install omc and/or OMPython." + raise RuntimeError(msg) + return omcCompiler # Auto-detection: prefer Dymola if available, fall back to OpenModelica dymola = DymolaCompiler(dymolaConfig) @@ -146,14 +145,13 @@ def getCompiler( if omc.is_available: return omc - raise RuntimeError( - "No Modelica compiler available. Install Dymola or OpenModelica." - ) + msg = "No Modelica compiler available. Install Dymola or OpenModelica." + raise RuntimeError(msg) def compileFmu( - mo: Union[str, Path], - outdir: Union[str, Path], + mo: str | Path, + outdir: str | Path, backend: Backend = "auto", fmuModelName: Optional[str] = None, load: Optional[list[str]] = None, @@ -238,8 +236,8 @@ def get_compiler( def mo2fmu_new( - mo: Union[str, Path], - outdir: Union[str, Path], + mo: str | Path, + outdir: str | Path, backend: Backend = "auto", fmumodelname: Optional[str] = None, load: Optional[list[str]] = None, @@ -333,7 +331,7 @@ def mo2fmu( result = compileFmu( mo=mo, outdir=outdir, - backend=backend, # type: ignore + backend=backend, # type: ignore[arg-type] fmuModelName=fmumodelname, load=list(load) if load else None, flags=list(flags) if flags else None, @@ -361,7 +359,6 @@ def mo2fmuCLI(ctx: click.Context, version: bool) -> None: Use 'mo2fmu compile' to generate FMUs or 'mo2fmu check' to verify compilers. Examples: - mo2fmu compile model.mo ./output mo2fmu compile -v --force model.mo ./output @@ -466,7 +463,6 @@ def compileCmd( OUTDIR: Output directory for the generated FMU Examples: - mo2fmu compile model.mo ./output mo2fmu compile -v --force --fmi-version 3 model.mo ./output @@ -486,7 +482,7 @@ def compileCmd( result = compileFmu( mo=mo, outdir=outdir, - backend=backend, # type: ignore + backend=backend, # type: ignore[arg-type] fmuModelName=name, load=list(load) if load else None, flags=list(flags) if flags else None, @@ -503,11 +499,11 @@ def compileCmd( click.echo(f"Error: {result.error_message}", err=True) if result.log: click.echo(f"Log:\n{result.log}", err=True) - raise SystemExit(1) + raise SystemExit(1) from None except RuntimeError as e: click.echo(f"Error: {e}", err=True) - raise SystemExit(1) + raise SystemExit(1) from e @mo2fmuCLI.command("check") @@ -545,7 +541,6 @@ def checkCmd( and reports their versions and supported FMI versions. Examples: - mo2fmu check mo2fmu check --json @@ -562,6 +557,7 @@ def checkCmd( if asJson: import json + click.echo(json.dumps(results, indent=2)) return @@ -574,8 +570,10 @@ def checkCmd( if results["dymola"]["available"]: click.echo(" Status: Available") click.echo(f" Version: {results['dymola']['version'] or 'Unknown'}") - fmiVersions = ", ".join(results["dymola"]["fmiSupport"]) - click.echo(f" FMI Support: {fmiVersions}") + dymolaFmiSupport = results["dymola"]["fmiSupport"] + if isinstance(dymolaFmiSupport, list): + fmiVersions = ", ".join(dymolaFmiSupport) + click.echo(f" FMI Support: {fmiVersions}") else: click.echo(" Status: Not available") click.echo(" Hint: Set DYMOLA_ROOT environment variable or use --dymola option") @@ -585,8 +583,10 @@ def checkCmd( if results["openmodelica"]["available"]: click.echo(" Status: Available") click.echo(f" Version: {results['openmodelica']['version'] or 'Unknown'}") - fmiVersions = ", ".join(results["openmodelica"]["fmiSupport"]) - click.echo(f" FMI Support: {fmiVersions}") + omcFmiSupport = results["openmodelica"]["fmiSupport"] + if isinstance(omcFmiSupport, list): + fmiVersions = ", ".join(omcFmiSupport) + click.echo(f" FMI Support: {fmiVersions}") else: click.echo(" Status: Not available") click.echo(" Hint: Install OpenModelica and OMPython (pip install OMPython)") @@ -598,7 +598,7 @@ def checkCmd( if availableCount == 0: click.echo("Warning: No compilers available!") raise SystemExit(1) - elif availableCount == 1: + if availableCount == 1: compilerName = "Dymola" if results["dymola"]["available"] else "OpenModelica" click.echo(f"Summary: {compilerName} is available for FMU generation.") else: @@ -622,7 +622,9 @@ def checkCmd( @click.option("--backend", default="auto", type=click.Choice(["dymola", "openmodelica", "auto"])) @click.option("--dymola", default="/opt/dymola-2025xRefresh1-x86_64/", type=click.Path()) @click.option("--dymolapath", default="/usr/local/bin/dymola", type=click.Path()) -@click.option("--dymolawhl", default="Modelica/Library/python_interface/dymola-2025.1-py3-none-any.whl") +@click.option( + "--dymolawhl", default="Modelica/Library/python_interface/dymola-2025.1-py3-none-any.whl" +) @click.option("-v", "--verbose", is_flag=True) @click.option("-f", "--force", is_flag=True) def mo2fmuLegacyCLI( @@ -655,7 +657,9 @@ def mo2fmuLegacyCLI( if check: # Invoke check command ctx = click.Context(checkCmd) - ctx.invoke(checkCmd, dymola=dymola, dymola_exec=dymolapath, dymola_whl=dymolawhl, asJson=False) + ctx.invoke( + checkCmd, dymola=dymola, dymola_exec=dymolapath, dymola_whl=dymolawhl, asJson=False + ) return if not mo or not outdir: diff --git a/tests/conftest.py b/tests/conftest.py index d9d6d5f..ccfcb37 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,7 +6,6 @@ import pytest - # ============================================================================= # Model Directory Fixtures # ============================================================================= diff --git a/tests/test_compilers.py b/tests/test_compilers.py index 78d9e8a..46ab46d 100644 --- a/tests/test_compilers.py +++ b/tests/test_compilers.py @@ -17,8 +17,10 @@ ModelicaModel, ) from feelpp.mo2fmu.compilers.dymola import DymolaCompiler, DymolaConfig -from feelpp.mo2fmu.compilers.openmodelica import OpenModelicaCompiler, OpenModelicaConfig - +from feelpp.mo2fmu.compilers.openmodelica import ( + OpenModelicaCompiler, + OpenModelicaConfig, +) # ============================================================================= # Compiler Availability Checks diff --git a/tests/test_fmu_simulation.py b/tests/test_fmu_simulation.py index ff88ac2..d1e429c 100644 --- a/tests/test_fmu_simulation.py +++ b/tests/test_fmu_simulation.py @@ -13,7 +13,6 @@ import pytest from feelpp.mo2fmu import compileFmu -from feelpp.mo2fmu.compilers.base import CompilationResult from feelpp.mo2fmu.compilers.dymola import DymolaConfig @@ -59,9 +58,7 @@ def _get_dymola_config() -> DymolaConfig: class TestFmpySimulation: """Tests for FMU simulation using FMPy with CVODE solver.""" - def test_simulate_cosimulation_fmu( - self, simpleOdeModel: Path, tmp_path: Path - ) -> None: + def test_simulate_cosimulation_fmu(self, simpleOdeModel: Path, tmp_path: Path) -> None: """Test simulating a Co-Simulation FMU with FMPy.""" from fmpy import simulate_fmu @@ -101,9 +98,7 @@ def test_simulate_cosimulation_fmu( assert y_values[0] > 0.9 # Initial value close to 1 assert y_values[-1] < y_values[0] # Decayed - def test_simulate_model_exchange_fmu( - self, simpleOdeModel: Path, tmp_path: Path - ) -> None: + def test_simulate_model_exchange_fmu(self, simpleOdeModel: Path, tmp_path: Path) -> None: """Test simulating a Model Exchange FMU with FMPy using CVODE solver.""" from fmpy import simulate_fmu @@ -141,7 +136,6 @@ def test_simulate_model_exchange_fmu( # Check that the solution decays correctly y_values = sim_result["y"] - time_values = sim_result["time"] # Initial value should be close to 1 assert abs(y_values[0] - 1.0) < 0.01 @@ -192,12 +186,10 @@ def test_simulate_model_exchange_sinusoidal( y_final = sim_result["y"][-1] assert abs(y_final) < 0.1 # Should be close to 0 - def test_compare_cs_and_me_results( - self, simpleOdeModel: Path, tmp_path: Path - ) -> None: + def test_compare_cs_and_me_results(self, simpleOdeModel: Path, tmp_path: Path) -> None: """Test that Co-Simulation and Model Exchange produce similar results.""" - from fmpy import simulate_fmu import numpy as np + from fmpy import simulate_fmu # Compile Co-Simulation FMU outdir_cs = tmp_path / "output_cs" @@ -257,9 +249,7 @@ def test_compare_cs_and_me_results( class TestFmpyFmi3Simulation: """Tests for FMI 3.0 FMU simulation using FMPy.""" - def test_simulate_fmi3_model_exchange( - self, simpleOdeModel: Path, tmp_path: Path - ) -> None: + def test_simulate_fmi3_model_exchange(self, simpleOdeModel: Path, tmp_path: Path) -> None: """Test simulating an FMI 3.0 Model Exchange FMU.""" from fmpy import simulate_fmu @@ -299,9 +289,7 @@ def test_simulate_fmi3_model_exchange( # FMPy might not fully support FMI 3.0 yet pytest.skip(f"FMPy FMI 3.0 simulation failed: {e}") - def test_simulate_fmi3_cosimulation( - self, simpleOdeModel: Path, tmp_path: Path - ) -> None: + def test_simulate_fmi3_cosimulation(self, simpleOdeModel: Path, tmp_path: Path) -> None: """Test simulating an FMI 3.0 Co-Simulation FMU.""" from fmpy import simulate_fmu @@ -422,9 +410,7 @@ class TestBouncingBallSimulation: - Input variables (wind_force) """ - def test_compile_bouncing_ball_fmi2( - self, bouncingBallModel: Path, tmp_path: Path - ) -> None: + def test_compile_bouncing_ball_fmi2(self, bouncingBallModel: Path, tmp_path: Path) -> None: """Test compiling bouncing ball to FMI 2.0.""" outdir = tmp_path / "output_bb_fmi2" result = compileFmu( @@ -536,9 +522,7 @@ def test_simulate_bouncing_ball_model_exchange( # Should detect at least some bounces assert max(bounce_counts) >= 1, "No bounces detected in ME simulation" - def test_bouncing_ball_bounce_count( - self, bouncingBallModel: Path, tmp_path: Path - ) -> None: + def test_bouncing_ball_bounce_count(self, bouncingBallModel: Path, tmp_path: Path) -> None: """Test that bounce counter increments correctly.""" from fmpy import simulate_fmu @@ -619,9 +603,7 @@ def test_bouncing_ball_fmi3_model_exchange( except Exception as e: pytest.skip(f"FMPy FMI 3.0 event simulation failed: {e}") - def test_bouncing_ball_fmi3_cosimulation( - self, bouncingBallModel: Path, tmp_path: Path - ) -> None: + def test_bouncing_ball_fmi3_cosimulation(self, bouncingBallModel: Path, tmp_path: Path) -> None: """Test bouncing ball with FMI 3.0 Co-Simulation.""" from fmpy import simulate_fmu @@ -658,9 +640,7 @@ def test_bouncing_ball_fmi3_cosimulation( except Exception as e: pytest.skip(f"FMPy FMI 3.0 Co-Simulation failed: {e}") - def test_bouncing_ball_with_wind_input( - self, bouncingBallModel: Path, tmp_path: Path - ) -> None: + def test_bouncing_ball_with_wind_input(self, bouncingBallModel: Path, tmp_path: Path) -> None: """Test bouncing ball with wind_force input variable.""" from fmpy import read_model_description From 8db86b850e051ec69f1275539cdca289cc147046 Mon Sep 17 00:00:00 2001 From: Christophe Prud'homme Date: Sun, 25 Jan 2026 16:19:30 +0100 Subject: [PATCH 4/8] Update black dependency version and refactor test model creation for improved readability --- pyproject.toml | 2 +- tests/test_compilers.py | 12 ++++-------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 410f99d..fd26d0d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,7 +65,7 @@ dev = [ "twine", ] lint = [ - "black>=23.0", + "black>=26.0", "flake8>=6.0", "flake8-docstrings>=1.7", "flake8-bugbear>=23.0", diff --git a/tests/test_compilers.py b/tests/test_compilers.py index 46ab46d..83613f9 100644 --- a/tests/test_compilers.py +++ b/tests/test_compilers.py @@ -78,14 +78,12 @@ class TestModelicaModel: def test_simple_model(self, tmp_path: Path) -> None: """Test creating a model from a simple .mo file.""" mo_file = tmp_path / "simple.mo" - mo_file.write_text( - """model simple + mo_file.write_text("""model simple Real x; equation der(x) = -x; end simple; -""" - ) +""") model = ModelicaModel(mo_file) @@ -97,15 +95,13 @@ def test_simple_model(self, tmp_path: Path) -> None: def test_model_with_package(self, tmp_path: Path) -> None: """Test creating a model with a 'within' statement.""" mo_file = tmp_path / "test_model.mo" - mo_file.write_text( - """within MyPackage.SubPackage; + mo_file.write_text("""within MyPackage.SubPackage; model test_model Real x; equation der(x) = -x; end test_model; -""" - ) +""") model = ModelicaModel(mo_file) From 51631149eeed24ce4ee84ab96c5987e140a68da2 Mon Sep 17 00:00:00 2001 From: Christophe Prud'homme Date: Sun, 25 Jan 2026 16:34:43 +0100 Subject: [PATCH 5/8] Add runtime license check for Dymola in FMU simulation tests --- tests/test_fmu_simulation.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_fmu_simulation.py b/tests/test_fmu_simulation.py index d1e429c..e3372b0 100644 --- a/tests/test_fmu_simulation.py +++ b/tests/test_fmu_simulation.py @@ -38,6 +38,10 @@ def _check_fmpy_available() -> bool: ) HAS_DYMOLA = (Path(DYMOLA_PATH) / DYMOLA_WHL).is_file() +# Check if Dymola runtime license is available (needed to run FMUs) +DYMOLA_RUNTIME_LICENSE = os.getenv("DYMOLA_RUNTIME_LICENSE", "") +HAS_RUNTIME_LICENSE = bool(DYMOLA_RUNTIME_LICENSE) and Path(DYMOLA_RUNTIME_LICENSE).is_file() + def _get_dymola_config() -> DymolaConfig: """Get Dymola configuration.""" @@ -55,6 +59,7 @@ def _get_dymola_config() -> DymolaConfig: @pytest.mark.skipif(not HAS_FMPY, reason="FMPy not available") @pytest.mark.skipif(not HAS_DYMOLA, reason="Dymola not available") +@pytest.mark.skipif(not HAS_RUNTIME_LICENSE, reason="Dymola runtime license not available") class TestFmpySimulation: """Tests for FMU simulation using FMPy with CVODE solver.""" @@ -246,6 +251,7 @@ def test_compare_cs_and_me_results(self, simpleOdeModel: Path, tmp_path: Path) - @pytest.mark.skipif(not HAS_FMPY, reason="FMPy not available") @pytest.mark.skipif(not HAS_DYMOLA, reason="Dymola not available") +@pytest.mark.skipif(not HAS_RUNTIME_LICENSE, reason="Dymola runtime license not available") class TestFmpyFmi3Simulation: """Tests for FMI 3.0 FMU simulation using FMPy.""" @@ -400,6 +406,7 @@ def test_read_model_variables(self, simpleOdeModel: Path, tmp_path: Path) -> Non @pytest.mark.skipif(not HAS_FMPY, reason="FMPy not available") @pytest.mark.skipif(not HAS_DYMOLA, reason="Dymola not available") +@pytest.mark.skipif(not HAS_RUNTIME_LICENSE, reason="Dymola runtime license not available") class TestBouncingBallSimulation: """Tests for bouncing ball model with events using FMPy. From c13c58bc5dda0967d0226b31030cd4702f1b21a4 Mon Sep 17 00:00:00 2001 From: Christophe Prud'homme Date: Sun, 25 Jan 2026 18:14:29 +0100 Subject: [PATCH 6/8] fix(ci): ignore pyparsing deprecation warnings from OMPython OMPython uses deprecated pyparsing functions (infixNotation) that trigger PyparsingDeprecationWarning. Since this is a third-party issue, add a filter to ignore these warnings in pytest. Co-Authored-By: Claude Opus 4.5 --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index fd26d0d..73467c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -97,6 +97,8 @@ xfail_strict = true filterwarnings = [ "error", "ignore:(ast.Str|Attribute s|ast.NameConstant|ast.Num) is deprecated:DeprecationWarning:_pytest", + # OMPython uses deprecated pyparsing functions (infixNotation -> infix_notation) + "ignore:.*deprecated.*:pyparsing.warnings.PyparsingDeprecationWarning", ] [tool.ruff] From e79044dd0f8d265bb55d5000e9531b695e695197 Mon Sep 17 00:00:00 2001 From: Christophe Prud'homme Date: Sun, 25 Jan 2026 18:34:16 +0100 Subject: [PATCH 7/8] Revert "fix(ci): ignore pyparsing deprecation warnings from OMPython" This reverts commit c13c58bc5dda0967d0226b31030cd4702f1b21a4. --- pyproject.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 73467c0..fd26d0d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -97,8 +97,6 @@ xfail_strict = true filterwarnings = [ "error", "ignore:(ast.Str|Attribute s|ast.NameConstant|ast.Num) is deprecated:DeprecationWarning:_pytest", - # OMPython uses deprecated pyparsing functions (infixNotation -> infix_notation) - "ignore:.*deprecated.*:pyparsing.warnings.PyparsingDeprecationWarning", ] [tool.ruff] From bf864b936a9ecd305968032d362839404896f683 Mon Sep 17 00:00:00 2001 From: Christophe Prud'homme Date: Sun, 25 Jan 2026 19:25:51 +0100 Subject: [PATCH 8/8] fix: ignore deprecated pyparsing warnings in pytest filterwarnings --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index fd26d0d..73467c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -97,6 +97,8 @@ xfail_strict = true filterwarnings = [ "error", "ignore:(ast.Str|Attribute s|ast.NameConstant|ast.Num) is deprecated:DeprecationWarning:_pytest", + # OMPython uses deprecated pyparsing functions (infixNotation -> infix_notation) + "ignore:.*deprecated.*:pyparsing.warnings.PyparsingDeprecationWarning", ] [tool.ruff]