feat: broaden CPU and platform coverage across PyPI, nodes, and crate… (#26)

* feat: broaden CPU and platform coverage across PyPI, nodes, and crates.io

Replace static SIMD with runtime CPU-feature dispatch and expand the release
wheel matrix so one set of artifacts runs on any target CPU and platform
without illegal-instruction crashes.

Rust core:
- Add multiversion runtime dispatch (crates/ferro_ta_core/src/simd.rs); drop
  compile-time `wide`. `simd` feature is now default-on and forwarded through
  the pyo3 crate, and stays compatible with #![forbid(unsafe_code)].

Packaging:
- abi3-py310: one cp310-abi3 wheel per platform (covers CPython 3.10+).
- CI matrix adds Linux aarch64 + musllinux (x86_64/aarch64) and Windows arm64.

Node/Docker + docs:
- api/Dockerfile: document baseline+dispatch (no target-cpu pin) and add a
  fail-fast import check; aarch64 containers now install cleanly.
- Rewrite docs/guides/simd.md; fix stale `wide` mention in ADR 0003.
- Add ADR 0006 (CPU coverage strategy).

Also bundles in-flight release prep already staged in the tree (DTW exception
types, SBOM/provenance security, supporting docs).

* fix(ci): clear cargo-deny and pip-audit failures; apply dependency bumps

cargo-deny (advisories):
- Ignore pyo3 RUSTSEC-2026-0176 / RUSTSEC-2026-0177 in deny.toml with a
  documented rationale: ferro-ta uses neither affected code path
  (PyList/PyTuple nth iterators; PyCFunction::new_closure). Upstream fix
  needs pyo3 >=0.29 (large API migration), tracked as a follow-up.

pip-audit:
- Bump dev lockfile idna 3.18, pytest 9.1.1, urllib3 2.7.0 to clear
  PYSEC-2026-215, CVE-2025-71176, PYSEC-2026-141/142.

Dependency bumps (supersede open dependabot PRs; they auto-close on merge):
- cargo: log 0.4.32, serde_json 1.0.150, rayon 1.12.0
- api/requirements.txt: uvicorn>=0.49.0, pydantic>=2.13.4, ferro-ta>=1.1.4
- CI actions: deploy-pages v5, upload-pages-artifact v5, action-gh-release v3

The open `wide` 1.5.0 bump (PR #24) is obsolete — the crate is removed in
this branch.

* chore: address CodeRabbit review; remove docs/adr section

CodeRabbit findings:
- CI sbom job: add `attestations: write` so attest-build-provenance can run
  (it had only contents:write + id-token:write).
- simd.rs: vectorize `wma_seed` with lane-local accumulators — it was scalar
  behind the multiversion wrapper, adding dispatch overhead for no SIMD gain.
- CHANGELOG: consolidate the duplicate `### Changed` heading.
- python/ferro_ta/__init__.py: also re-export the `FerroTaError` alias.
- docs/guides/dtw.md: soften "byte-for-byte" parity to within-tolerance.

Remove docs/adr/ at maintainer request and clean up the ADR links in the
SIMD and DTW guides. The ADR files remain in commit 9506a30 if ever needed.
This commit is contained in:
Pratik Bhadane
2026-06-29 18:21:22 +05:30
committed by GitHub
parent fd1bb137d6
commit 288b1546b2
23 changed files with 839 additions and 169 deletions
+6
View File
@@ -110,8 +110,14 @@ __version__ = _detect_version()
# ---------------------------------------------------------------------------
from ferro_ta.core.exceptions import ( # noqa: F401
FerroTAError,
FerroTaError,
FerroTAInputError,
FerroTAValueError,
InsufficientDataError,
InvalidInputError,
InvalidPeriodError,
LengthMismatchError,
NumericConvergenceError,
)
# ---------------------------------------------------------------------------
+61 -4
View File
@@ -131,6 +131,63 @@ class FerroTAInputError(FerroTAError, ValueError):
code = "FTERR002"
# ---------------------------------------------------------------------------
# Finer-grained exception subclasses (added in 1.2.0).
#
# These are drop-in compatible with the base classes: every subclass still
# inherits from ``FerroTAError`` and ``ValueError``, so existing user code
# like ``except FerroTAValueError:`` or ``except ValueError:`` keeps working.
# The subclasses exist so users can catch *specific* failure modes without
# string-matching on the error message.
# ---------------------------------------------------------------------------
class InvalidPeriodError(FerroTAValueError):
"""Parameter like ``timeperiod``, ``fastperiod``, ``slowperiod`` is out of range.
Default error code: ``FTERR001``.
"""
class InsufficientDataError(FerroTAInputError):
"""Input array is shorter than the minimum required for the indicator.
Default error code: ``FTERR003``.
"""
code = "FTERR003"
class LengthMismatchError(FerroTAInputError):
"""Two or more input arrays (e.g. OHLC) have different lengths.
Default error code: ``FTERR004``.
"""
code = "FTERR004"
class NumericConvergenceError(FerroTAValueError):
"""An iterative calculation failed to converge within tolerance.
Raised by iterative pricing models (implied volatility root-finding,
Newton-Raphson, etc.) when the maximum iteration count is exhausted.
"""
class InvalidInputError(FerroTAInputError):
"""Input contains NaN/Inf in strict mode, wrong dtype, or wrong shape.
Default error code: ``FTERR005``.
"""
code = "FTERR005"
# Public aliases that match the names documented in the README and
# CHANGELOG [Unreleased] section.
FerroTaError = FerroTAError # type: ignore[misc]
# ---------------------------------------------------------------------------
# Validation helpers (called by Python wrappers)
# ---------------------------------------------------------------------------
@@ -154,7 +211,7 @@ def check_timeperiod(value: int, name: str = "timeperiod", minimum: int = 1) ->
If ``value < minimum``.
"""
if value < minimum:
raise FerroTAValueError(
raise InvalidPeriodError(
f"{name} must be >= {minimum}, got {value}",
suggestion=f"Set {name}={minimum} or higher.",
)
@@ -193,7 +250,7 @@ def check_equal_length(**arrays: object) -> None:
if len(set(lengths.values())) > 1:
detail = ", ".join(f"{k}={v}" for k, v in lengths.items())
raise FerroTAInputError(
raise LengthMismatchError(
f"All input arrays must have the same length. Got: {detail}",
code=_CODE_LENGTH_MISMATCH,
suggestion="Trim or align your arrays so that open, high, low, close, and volume all have the same number of rows.",
@@ -223,7 +280,7 @@ def check_finite(arr: object, name: str = "input") -> None:
a = np.asarray(arr, dtype=np.float64)
if not np.all(np.isfinite(a)):
raise FerroTAInputError(
raise InvalidInputError(
f"{name} contains NaN or Inf values. "
"ferro_ta propagates NaN by default; call check_finite() only "
"when you require all-finite inputs.",
@@ -255,7 +312,7 @@ def check_min_length(arr: object, min_len: int, name: str = "input") -> None:
elif hasattr(arr, "shape"):
length = arr.shape[0] # type: ignore[union-attr]
if length < min_len:
raise FerroTAInputError(
raise InsufficientDataError(
f"{name} must have at least {min_len} elements, got {length}",
code=_CODE_TOO_SHORT,
suggestion=f"Provide at least {min_len} data points. Current length: {length}.",