diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c4acb20..df85bc6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,6 +1,6 @@ name: Test Windows on: - push + workflow_dispatch: jobs: build: diff --git a/.github/workflows/full_test.yml b/.github/workflows/full_test.yml new file mode 100644 index 0000000..6227d28 --- /dev/null +++ b/.github/workflows/full_test.yml @@ -0,0 +1,45 @@ +# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions + +name: Test Windows and Linux + +on: + push: + branches: [ master] + pull_request: + branches: [ master ] + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + python-version: [3.8, 3.13] + + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v6 + with: + version: "0.7.21" + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + # this is installed by default on Ubuntu 22.04 + - name: Install libnsl2 (Linux only) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y libnsl2 + + - name: Install the project + run: uv sync + + - name: Run tests + run: uv run pytest + diff --git a/.github/workflows/test_linux.yml b/.github/workflows/test_linux.yml index 9b53880..084de6d 100644 --- a/.github/workflows/test_linux.yml +++ b/.github/workflows/test_linux.yml @@ -1,11 +1,6 @@ name: Test Linux on: - push: - branches: [ master ] - pull_request: - branches: [ master ] - workflow_dispatch: jobs: diff --git a/.gitignore b/.gitignore index b937d44..f0f81e7 100644 --- a/.gitignore +++ b/.gitignore @@ -90,3 +90,4 @@ bin x64 /tmp/ /TestResults +uv.lock diff --git a/README.md b/README.md index fc88183..80026a5 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,33 @@ This library is the foundation for [MIKE IO](https://github.com/DHI/mikeio). ## Installation ```pip install mikecore``` -``` + +## Development + +All commands are run from the project root. + +1. **Sync & Build** + ```bash + uv sync + # Use 'uv sync --reinstall' to force-rebuild native components. + ``` + +2. **Update EUM Types** (for new release, or whenever EUM.xml changes) + ```bash + # Generate definitions from the new native build + uv run ./buildUtil/eumXMLProcess.py > eumItemUnit.txt + + # Use a diff tool to merge changes into mikecore/eum.py. + ``` + +3. **Run Tests** + ```bash + uv run pytest + ``` + +4. **Build Packages** (Optional) + ```bash + uv build + ``` diff --git a/buildUtil/build.py b/buildUtil/build.py new file mode 100644 index 0000000..2c122de --- /dev/null +++ b/buildUtil/build.py @@ -0,0 +1,107 @@ +from __future__ import annotations + +import shutil +import urllib.request +import xml.etree.ElementTree as ET +import zipfile +from pathlib import Path +from typing import Any +import subprocess +import platform + +from hatchling.builders.hooks.plugin.interface import BuildHookInterface + + +def download_nuget_package( + package_id: str, + version: str, + output_dir: str | Path +) -> None: + """ + Download a NuGet package (.nupkg) to output_dir. + """ + package_id_lower: str = package_id.lower() + version_lower: str = version.lower() + url: str = f"https://api.nuget.org/v3-flatcontainer/{package_id_lower}/{version_lower}/{package_id_lower}.{version_lower}.nupkg" + output_dir = Path(output_dir) + nupkg_path: Path = output_dir / f"{package_id}.{version}.nupkg" + extract_path: Path = output_dir / f"{package_id}" + + output_dir.mkdir(parents=True, exist_ok=True) + + print(f"Downloading {package_id} {version} from {url}...") + with urllib.request.urlopen(url) as response, nupkg_path.open("wb") as f: + f.write(response.read()) + + print(f"Extracting {nupkg_path} to {extract_path}...") + with zipfile.ZipFile(nupkg_path, "r") as zip_ref: + zip_ref.extractall(extract_path) + print("Done.") + +def copy_native_libs_to_bin(packages_dir: str | Path, bin_dir: str | Path) -> None: + """ + Copy native shared libraries from NuGet packages to bin directory. + """ + packages_dir = Path(packages_dir) + bin_windows = Path(bin_dir) / "windows" + bin_linux = Path(bin_dir) / "linux" + bin_windows.mkdir(parents=True, exist_ok=True) + bin_linux.mkdir(parents=True, exist_ok=True) + + for package in packages_dir.iterdir(): + win_native = package / "runtimes" / "win-x64" / "native" + linux_native = package / "runtimes" / "linux-x64" / "native" + + if win_native.exists(): + for lib in win_native.glob("*"): + dest = bin_windows / lib.name + print(f"Copying {lib} to {dest}") + shutil.copy2(lib, dest) + + if linux_native.exists(): + for lib in linux_native.glob("*"): + dest = bin_linux / lib.name + print(f"Copying {lib} to {dest}") + shutil.copy2(lib, dest) + +def read_packages_config(filepath: str | Path) -> list[tuple[str, str]]: + """ + Reads NuGet packages.config and returns a list of (id, version) tuples. + """ + tree = ET.parse(filepath) + root = tree.getroot() + return [ + (pkg.attrib["id"], pkg.attrib["version"]) + for pkg in root.findall("package") + ] + +def modify_linux_so_rpath(bin_folder: str | Path): + patchelf_path = shutil.which("patchelf") + for so in Path(bin_folder).glob("*.so*"): + print(f"Setting RUNPATH for {so} to '$ORIGIN'") + subprocess.run( + [patchelf_path, "--set-rpath", "$ORIGIN", str(so.absolute())], + check=True, + ) + +def setup(): + """Setup function to download NuGet packages and copy native libraries into bin folder. + """ + packages = read_packages_config("buildUtil/packages.config") + for name, version in packages: + download_nuget_package(name, version, output_dir="packages") + copy_native_libs_to_bin("packages", "mikecore/bin") + + if platform.system().lower() == "linux": + modify_linux_so_rpath("mikecore/bin/linux") + + +class BuildHook(BuildHookInterface): + """Custom build hook to run setup during the build process.""" + + def initialize(self, version: str, build_data: dict[str : Any]) -> None: + """Initialize the build hook.""" + setup() + +if __name__ == "__main__": + setup() \ No newline at end of file diff --git a/buildUtil/eumXMLProcess.py b/buildUtil/eumXMLProcess.py index 0e73f46..bc540bc 100644 --- a/buildUtil/eumXMLProcess.py +++ b/buildUtil/eumXMLProcess.py @@ -12,7 +12,7 @@ import re # Using readlines() -eumFile = open('build/lib/mikecore/bin/windows/EUM.xml', 'r') +eumFile = open('mikecore/bin/windows/EUM.xml', 'r') eumLines = eumFile.readlines() eumFile.close() diff --git a/buildUtil/packages.config b/buildUtil/packages.config new file mode 100644 index 0000000..c2a57c0 --- /dev/null +++ b/buildUtil/packages.config @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/mikecore/eum.py b/mikecore/eum.py index db8ca56..d5f9c56 100644 --- a/mikecore/eum.py +++ b/mikecore/eum.py @@ -605,6 +605,50 @@ class eumItem(IntEnum): eumIBladeThickness = 110318 eumIPlantDensity = 110319 eumIThickness = 110320 + eumICarbonEmmisionFactor = 110321 + eumIAluminiumAl = 110322 + eumIBismuthBi = 110323 + eumICalciumCa = 110324 + eumICadmiumCd = 110325 + eumICobaltCo = 110326 + eumIChromiumCr = 110327 + eumICopperCu = 110328 + eumIIronFe = 110329 + eumIPotassiumK = 110330 + eumILithiumLi = 110331 + eumIMagnesiumMg = 110332 + eumIManganeseMn = 110333 + eumISodiumNa = 110334 + eumINickelNi = 110335 + eumILeadPb = 110336 + eumIStrontiumSr = 110337 + eumIZincZn = 110338 + eumISilverAg = 110339 + eumIBariumBa = 110340 + eumIBerylliumBe = 110341 + eumIIndiumIn = 110342 + eumIVanadiumV = 110343 + eumIArsenicAs = 110344 + eumISulfurS = 110345 + eumIMolybdenumMo = 110346 + eumIPhosphorusP = 110347 + eumIMercuryHg = 110348 + eumIAntimonySb = 110349 + eumISeleniumSe = 110350 + eumIUraniumU = 110351 + eumIBoronB = 110352 + eumIOxidationReductionPotentialORP = 110353 + eumITotalSuspendedSolidsTSS = 110354 + eumITotalDissolvedSolidsTDS = 110355 + eumIChlorideCl = 110356 + eumITurbidityNTU = 110357 + eumISulphateSO4 = 110358 + eumIPhosphatePO4 = 110359 + eumINitrateNO3 = 110360 + eumINitriteNO2 = 110361 + eumIFluorideF = 110362 + eumITotalAlkanlinity = 110363 + eumIChemicalOxygenDemandCOD = 110364 # Predefined enums of EUM units. # @@ -886,6 +930,7 @@ class eumUnit(IntEnum): eumUTonPerYear = 4218 eumUTonPerDay = 4219 eumUTonPerSec = 4220 + eumUmilligramPerMin = 4221 eumUgramPerM2 = 4400 eumUkilogramPerM = 4401 eumUkilogramPerM2 = 4402 @@ -1024,6 +1069,8 @@ class eumUnit(IntEnum): eumUexaJoule = 5608 eumUmegaWattHour = 5609 eumUgigaWattHour = 5610 + eumUkilogramPerKiloWattHour = 5630 + eumUpoundPerKiloWattHour = 5631 eumUperJoule = 5650 eumUperKiloJoule = 5651 eumUperMegaJoule = 5652 @@ -1124,6 +1171,17 @@ class eumUnit(IntEnum): eumUMgalUKPerDayPerPsi = 7508 eumUacftPerDayPerPsi = 7509 eumUm3PerHourPerBar = 7510 + eumUliterPerSecPerMeter2OneHalf = 7530 + eumUliterPerMinPerMeter2OneHalf = 7531 + eumUMegaLiterPerDayPerMeter2OneHalf = 7532 + eumUm3PerHourPerMeter2OneHalf = 7533 + eumUm3PerDayPerMeter2OneHalf = 7534 + eumUft3PerSecPerPsi2OneHalf = 7535 + eumUgallonPerMinPerPsi2OneHalf = 7536 + eumUMgalPerDayPerPsi2OneHalf = 7537 + eumUMgalUKPerDayPerPsi2OneHalf = 7538 + eumUacftPerDayPerPsi2OneHalf = 7539 + eumUm3PerHourPerBar2OneHalf = 7540 eumUKilogramPerS2 = 8100 eumUm2Perkilogram = 9100 eumUPerMeterPerSecond = 9200 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..2077d57 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,38 @@ +[build-system] +requires = ["hatchling", "patchelf; sys_platform == 'linux'"] +build-backend = "hatchling.build" + +[tool.hatch.build.hooks.custom] +path = "buildUtil/build.py" + +[tool.hatch.build.targets.sdist.force-include] +"mikecore/bin" = "mikecore/bin" + +[tool.hatch.build.targets.wheel.force-include] +"mikecore/bin" = "mikecore/bin" + +[project] +name = "mikecore" +version = "0.3.0a0" +description = "MIKE Core contains core libraries, like DFS (Data File System), EUM and more." +readme = "README.md" +license = { file = "LICENSE.txt" } +authors = [ + { name = "DHI", email = "mike@dhigroup.com" } +] +requires-python = ">=3.5" +dependencies = [ + "numpy" +] +classifiers = [ + "License :: OSI Approved :: BSD License", + "Programming Language :: Python :: 3" +] + +[project.urls] +Homepage = "https://github.com/DHI/mikecore-python" + +[dependency-groups] +dev = [ + "pytest>=6.1.2", +] diff --git a/setup.py b/setup.py deleted file mode 100644 index fcafb70..0000000 --- a/setup.py +++ /dev/null @@ -1,24 +0,0 @@ -import setuptools - -with open("README.md", "r") as fh: - long_description = fh.read() - -setuptools.setup( - name="mikecore", - version="0.3.0a0", - install_requires=["numpy"], - author="DHI", - author_email="mike@dhigroup.com", - description="MIKE Core contains core libraries, like DFS (Data File System), EUM and more.", - long_description=long_description, - long_description_content_type="text/markdown", - url="https://github.com/DHI/mikecore-python", - packages=setuptools.find_packages(), - license="BSD-3", - classifiers=[ - "License :: OSI Approved :: BSD License", - "Programming Language :: Python :: 3", - ], - python_requires=">=3.5", - include_package_data=True, -)