Skip to content
Merged
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
157 changes: 157 additions & 0 deletions .github/workflows/build-pyxirr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# SPDX-FileCopyrightText: 2026 The RISE Project
# SPDX-License-Identifier: MIT
---
# This workflow is based on the `linux` job of
# https://github.com/Anexen/pyxirr/blob/v0.10.8/.github/workflows/ci.yaml
name: Build pyxirr wheels (riscv64)

on:
workflow_dispatch:
inputs:
version:
description: 'pyxirr version to build (git tag without leading v, e.g. 0.10.8)'
required: true
default: '0.10.8'
pull_request:
paths:
- '.github/workflows/build-pyxirr.yml'

concurrency:
group: ${{ github.workflow }}-${{ inputs.version || '0.10.8' }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

permissions:
contents: read # to fetch code (actions/checkout)

env:
# `inputs.version` is empty on pull_request events; default to 0.10.8 there.
PYXIRR_VERSION: ${{ inputs.version || '0.10.8' }}
MANYLINUX_RISCV64_IMAGE: quay.io/pypa/manylinux_2_39_riscv64

jobs:
setup:
uses: $/.github/workflows/_setup.yml

build_wheels:
needs: [setup]
name: Build pyxirr ${{ inputs.version || '0.10.8' }} ${{ matrix.tag }}-manylinux_riscv64
runs-on: ubuntu-24.04-riscv
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
include:
# pyxirr's [tool.maturin] features only lists "extension"
# (pyo3/extension-module); upstream's own release job additionally
# passes --features extension,abi (pyo3/abi3-py37) on the maturin
# command line to get the single cp37-abi3 wheel it publishes to
# PyPI, so we restate the same flags here (gotcha 155). Upstream
# ships no free-threaded wheel, so the matrix stays a single entry
# built once at our cp312 floor and re-tested on cp313/cp314.
- tag: cp37-abi3
build: >-
cp312-manylinux_riscv64 cp313-manylinux_riscv64
cp314-manylinux_riscv64
features: --features extension,abi

steps:
- name: Checkout pyxirr v${{ env.PYXIRR_VERSION }}
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: Anexen/pyxirr
ref: v${{ env.PYXIRR_VERSION }}
persist-credentials: false

- name: Stage the riscv64 smoke tests
# Upstream's tests/ directory holds only Rust integration tests
# (exercised by `cargo test` in ci.yaml), no Python-level suite ships
# in the checkout or the sdist. This mirrors the README's examples
# against the compiled extension.
run: |
cat > tests/test_riscv64_smoke.py <<'EOF'
from datetime import date

import numpy as np
import pytest
from pyxirr import DayCount, fv, irr, is_conventional_cash_flow, npv, xirr, xnpv


def test_extension_is_compiled():
from pyxirr import _pyxirr

assert _pyxirr.__file__.endswith(".so"), _pyxirr.__file__


def test_irr():
assert irr([-100, 39, 59, 55, 20]) == pytest.approx(0.28094842)


def test_xirr_dates_and_amounts():
dates = [date(2020, 1, 1), date(2021, 1, 1), date(2022, 1, 1)]
amounts = [-1000, 750, 500]
assert xirr(dates, amounts) == pytest.approx(0.17500926)


def test_xirr_string_dates_and_dict():
assert xirr(["2020-01-01", "2021-01-01"], [-1000, 1200]) == pytest.approx(0.19940237)
assert xirr(dict(zip(
["2020-01-01", "2021-01-01"], [-1000, 1200],
))) == pytest.approx(0.19940237)


def test_npv_and_xnpv():
assert npv(0.1, [-100, 60, 60, 60]) == pytest.approx(49.21111946)
assert xnpv(0.1, ["2020-01-01", "2021-01-01"], [-100, 110]) == pytest.approx(-0.02610897)


def test_fv_broadcasts_over_numpy_arrays():
rates = np.array([0.05, 0.1])
result = fv(rates, 10, -100, 0)
assert result == pytest.approx([1257.78925, 1593.7424601])


def test_day_count_convention():
assert xirr(
["2020-01-01", "2021-01-01"], [-1000, 1200],
day_count=DayCount.ACT_360,
) == pytest.approx(0.19641870)


def test_is_conventional_cash_flow():
assert is_conventional_cash_flow([-100, 50, 50, 50])
assert not is_conventional_cash_flow([-100, 50, -20, 80])
EOF

- name: Build and test wheels
uses: pypa/cibuildwheel@1828c10ab37f080699c7b81cea34097c684a7074 # v4.2.0
env:
CIBW_ARCHS: riscv64
CIBW_BUILD: ${{ matrix.build }}
CIBW_MANYLINUX_RISCV64_IMAGE: ${{ env.MANYLINUX_RISCV64_IMAGE }}
# pyxirr ships no [tool.cibuildwheel], so the Rust toolchain its
# maturin backend needs is installed in-container here.
CIBW_BEFORE_ALL_LINUX: >-
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
CIBW_ENVIRONMENT_LINUX: >-
PATH="$PATH:$HOME/.cargo/bin"
MATURIN_PEP517_ARGS="${{ matrix.features }}"
PIP_EXTRA_INDEX_URL=https://pypi.riseproject.dev/simple/
PIP_ONLY_BINARY=numpy
CIBW_TEST_REQUIRES: pytest numpy
CIBW_TEST_COMMAND: python -m pytest -vv {project}/tests/test_riscv64_smoke.py

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: pyxirr-${{ env.PYXIRR_VERSION }}-${{ matrix.tag }}-manylinux_riscv64
path: ./wheelhouse/*.whl
if-no-files-found: error

publish:
name: Publish pyxirr ${{ inputs.version || '0.10.8' }}
needs: [setup, build_wheels]
permissions:
contents: write
pull-requests: write
uses: $/.github/workflows/_publish-wheel.yml
with:
artifact-pattern: pyxirr-${{ inputs.version || '0.10.8' }}-*-manylinux_riscv64