Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions .github/workflows/pack-pip.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
name: pip packaging
on:
workflow_dispatch:
inputs:
python_repository:
description: python repository
type: choice
default: None # no publishing to any Package Index by default
options: [None, testpypi, pypi]
push:
tags: ['*']
pull_request:
paths:
- .github/workflows/pack-pip.yml
- pyproject.toml
- CMakeLists.txt

defaults:
run:
shell: bash

jobs:
build-wheel:
name: Build wheel
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-24.04, ubuntu-24.04-arm, windows-2025, windows-11-arm, macos-15-intel, macos-15]
steps:
- uses: actions/checkout@v6

- name: Load Visual C++ Environment Variables (Windows)
if: runner.os == 'Windows'
shell: cmd
run: |
call "C:\\Program Files\\Microsoft Visual Studio\\2022\\Enterprise\\VC\\Auxiliary\\Build\\vcvars64.bat"
set >> %GITHUB_ENV%

- name: Build wheels
uses: pypa/cibuildwheel@v3.3.1

- name: install Python
uses: actions/setup-python@v5

- name: Test built wheel
run: |
pip install --find-links=wheelhouse khisto
pip install pytest>=8.3 pytest-cov>=6 pytest-xdist>=3.6 pytest-sugar>=1.0

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd put these test dependencies into a test-requirements.txt file, then just do pip install -r test-requirements.txt.

pytest tests/

- uses: actions/upload-artifact@v6
with:
name: pkg-wheel-${{ matrix.os }}
path: wheelhouse/*.whl
if-no-files-found: error

release-testpypi:
# Publish only on tag pushes in the KhiopsML repository
if: github.ref_type == 'tag' && github.repository_owner == 'KhiopsML' && inputs.python_repository == 'testpypi'
name: Publish to Test.PyPI.org
needs: [build-wheel]
runs-on: ubuntu-latest
permissions:
id-token: write
environment:
name: testpypi
steps:
- uses: actions/download-artifact@v5
with:
pattern: pkg-*
path: dist
merge-multiple: true
- uses: pypa/gh-action-pypi-publish@release/v1
with:
verbose: true
repository-url: https://test.pypi.org/legacy/

release-pypi:
# Publish only on tag pushes in the KhiopsML repository
name: Publish to PyPI.org
if: github.ref_type == 'tag' && github.repository_owner == 'KhiopsML' && inputs.python_repository == 'pypi'
needs: [build-wheel]
runs-on: ubuntu-latest
permissions:
id-token: write
environment:
name: pypi
steps:
- uses: actions/download-artifact@v5
with:
pattern: pkg-*
path: dist
merge-multiple: true
- uses: pypa/gh-action-pypi-publish@release/v1
with:
verbose: true
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -177,3 +177,6 @@ uv.lock

# Excel
**/*.xlsx

# macOS
.DS_Store
3 changes: 0 additions & 3 deletions .gitmodules

This file was deleted.

21 changes: 15 additions & 6 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -112,12 +112,12 @@ repos:
pass_filenames: false
always_run: true
args: [--markdown-linebreak-ext=md]
- id: pyrefly
name: pyrefly
entry: pyrefly check
language: system
pass_filenames: false
always_run: true
# - id: pyrefly
# name: pyrefly
# entry: pyrefly check
# language: system
# pass_filenames: false
# always_run: true
- id: ruff-check
name: ruff-check
entry: ruff check
Expand All @@ -130,3 +130,12 @@ repos:
entry: ruff format
language: system
pass_filenames: false
- id: update-copyright
name: update-copyright
entry: python scripts/update-copyright.py
language: system
types_or: [c, c++, java, python]
- id: check-obsolete-copyright
name: check-obsolete-copyright
entry: python scripts/check-obsolete-copyright.py
language: system
21 changes: 21 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
cmake_minimum_required(VERSION 3.16.3)
project(khisto)
include(FetchContent)

# Extract the khiops version from pyproject.toml
# The version is specified in the "tool.khiops" section as "khiops-version = "x.y.z""
# It must correspond to a tag in the khiops repository
file(READ pyproject.toml PYPROJECT_FILE)
string(REGEX MATCH "khiops-version = \"([^\"]*)\"" _ ${PYPROJECT_FILE})
set(KHIOPS_VERSION ${CMAKE_MATCH_1})
if(NOT KHIOPS_VERSION)
message(FATAL_ERROR "Could not extract khiops version from pyproject.toml")
endif()
message(STATUS "Khiops version: ${KHIOPS_VERSION}")

# Download and build khiops from the specified tag
fetchcontent_Declare(
khiops
GIT_REPOSITORY https://github.com/KhiopsML/khiops.git
GIT_TAG ${KHIOPS_VERSION})
FetchContent_MakeAvailable(khiops)
32 changes: 32 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
The Clear BSD License

Copyright (c) 2023-2026 Orange S.A.
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted (subject to the limitations in the disclaimer
below) provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.

* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from this
software without specific prior written permission.

NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
47 changes: 39 additions & 8 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,18 @@ name = "khisto"
version = "0.1.0"
description = "Optimal histogram visualization using the Khiops algorithm"
readme = "README.md"
requires-python = ">=3.11"
license = "BSD-3-Clause-Clear"
license-files = ["LICENSE"]
requires-python = ">=3.10"
dependencies = ["numpy>=2.0"]

[tool.khiops]
khiops-version = "11.0.1-a.2"

[project.optional-dependencies]
matplotlib = ["matplotlib>=3.8"]
all = ["matplotlib>=3.8"]


[dependency-groups]
dev = [
"pytest>=8.3",
Expand All @@ -25,8 +29,8 @@ dev = [
]

[build-system]
requires = ["hatchling>=1.25"]
build-backend = "hatchling.build"
requires = ["scikit-build-core>=0.11.6", "ninja"]
build-backend = "scikit_build_core.build"

[tool.pytest.ini_options]
addopts = "-n 4 --cov --cov-report term-missing --durations=10"
Expand All @@ -42,7 +46,6 @@ ignore-regex = 'https://([\w/\.])+'
quiet-level = 3

[tool.ruff]

# Group violations by containing file.
output-format = "grouped"
line-length = 88
Expand All @@ -52,7 +55,7 @@ indent-width = 4
fix = true

# Exclude a variety of commonly ignored directories.
extend-exclude = ["src/khiops"]
extend-exclude = ["scripts", "docs", "sandbox", ".github"]

[tool.ruff.format]
# Like Black, use double quotes for strings.
Expand All @@ -70,5 +73,33 @@ ignore = [
"E402", # module level import not at top of file
]

[tool.pyrefly]
project-excludes = ["src/khiops"]
[tool.scikit-build]
wheel.py-api = "py3" # We specify full compatibility with all Python 3 releases
cmake.version = "CMakeLists.txt" # Read CMake minimal version from file
cmake.args = ["-G", "Ninja"] # Generator specification and build configuration

# CMake variables
# Build khisto binary in Release mode for better performance, as it is not intended for debugging
cmake.define.CMAKE_BUILD_TYPE = "Release"
# Options
cmake.define.MPI = false
cmake.define.TESTING = false
cmake.define.BUILD_LEX_YACC = false
cmake.define.BUILD_JARS = false
cmake.define.GENERATE_VIEWS = false
cmake.define.C11 = true
ninja.make-fallback = false
build.targets = ["khisto"] # Build only khisto
install.components = ["KHISTO"] # Only install KHISTO component in wheel

[tool.cibuildwheel]
build = "cp310-*"
skip = "*musllinux*" # Khiops does not compile when using musl as the C standard library
archs = "auto64" # Do not build 32-bit wheels

# Install build-time dependencies in containers created by cibuildwheel
# This is compatible with Debian-like and Fedora-like distributions
[tool.cibuildwheel.linux]
before-all = [
"apt-get install -y ninja-build || dnf install -y ninja-build"
]
67 changes: 67 additions & 0 deletions scripts/check-obsolete-copyright.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Copyright (c) 2025-2026 Orange. All rights reserved.
# This software is distributed under the BSD 3-Clause-clear License, the text of which is available
# at https://spdx.org/licenses/BSD-3-Clause-Clear.html or see the "LICENSE" file for more details.

"""Updates the copyright notice of the input files"""

import argparse
import os
import sys
from datetime import datetime

# Constants to check the presence of obsolete copyright
copyright_prefix = bytes("(c) 2025-", encoding="ascii")
valid_copyright = bytes(f"(c) 2025-{datetime.today().year} Orange", encoding="ascii")

# List of special files
special_files = ["LICENSE"]


def main(args):
"""Main method"""
# Check obsolete copyright
valid_copyright = True
for file_path in args.file_paths:
if test_obsolete_copyright(file_path):
print(f"Copyright notice must be updated in {file_path}")
valid_copyright = False

# Set the return code
return_code = 0
if not valid_copyright:
return_code = 1

return return_code


def test_obsolete_copyright(file_path):
"""Check if a special file contains an obsolete copyright notice"""

basename = os.path.basename(file_path)
if basename in special_files:
# Read the lines from the source file
with open(file_path, "rb") as file:
lines = file.readlines()

# Check copyright
for n, line in enumerate(lines):
line = line.rstrip()
if copyright_prefix in line and valid_copyright not in line:
return True
# Obsolete copyright not found
return False


if __name__ == "__main__":
parser = argparse.ArgumentParser(
prog="python check-obsolete-copyright.py",
formatter_class=argparse.RawTextHelpFormatter,
description="Checks the presence of an obsolete copyright notice of the input files",
)
parser.add_argument(
"file_paths",
metavar="FILE",
nargs="+",
help="One or more source files",
)
sys.exit(main(parser.parse_args()))
Loading