Files
Pratik Bhadane 307beeca02 release: cut v1.0.0
Prepare the first public 1.0.0 release and finish the remaining CI hardening work.

Highlights:
- align Python, Rust, WASM, Conda, API, MCP, and docs version metadata to 1.0.0
- promote package metadata to Production/Stable and update stability/versioning docs for the stable series
- move the accumulated Unreleased notes into a dated 1.0.0 changelog section and keep a fresh top-level Unreleased block
- strengthen the changelog checker so it validates a single top-level Unreleased section
- fix the CI/package support mismatch by declaring Python >=3.10 consistently and gating pandas-ta extras to Python 3.12+
- restore Sphinx autodoc compatibility for documented ferro_ta.<module> imports by registering module aliases
- make the TA-Lib benchmark guardrail less flaky by checking median and tail-percentile speedups instead of failing on a single mild outlier
- switch PyPI publishing to OIDC-only trusted publishing and wire the changelog check into the required CI gate
- apply the Ruff-driven cleanup across the Python and test tree and refresh uv/cargo lockfiles

Validated locally:
- python3 scripts/check_changelog.py
- uv run --with ruff ruff check python tests
- uv run --with ruff ruff format --check python tests
- uv lock --check
- sphinx-build -b html docs docs/_build -W --keep-going
- build/install the ferro_ta 1.0.0 wheel successfully
2026-03-23 23:57:30 +05:30

94 lines
3.3 KiB
Python

"""
Data-driven binding layer — generic wrapper for Rust indicator calls.
This module provides a single helper that performs validation, array conversion
(_to_f64), Rust call, and error normalization. Indicator modules can use it to
reduce repetitive wrapper code; a manifest (see _indicator_manifest.yaml) describes
each indicator so that wrappers or code generation can be driven from data.
Usage (manual wrapper):
from ferro_ta._binding import binding_call
def SMA(close, timeperiod=30):
return binding_call(
_sma,
array_params=["close"],
timeperiod_param="timeperiod",
close=close,
timeperiod=timeperiod,
)
Future: A code generator can read the manifest and emit either full wrapper
functions or binding_call(...) invocations so that ~6000 lines of repetitive
wrapper code are generated from the manifest.
"""
from __future__ import annotations
from collections.abc import Callable
from typing import Any, Optional
from ferro_ta._utils import _to_f64
from ferro_ta.core.exceptions import (
_normalize_rust_error,
check_equal_length,
check_timeperiod,
)
def binding_call(
rust_fn: Callable[..., Any],
*,
array_params: list[str],
timeperiod_param: Optional[str] = None,
timeperiod_min: int = 1,
equal_length_groups: Optional[list[list[str]]] = None,
**kwargs: Any,
) -> Any:
"""Call a Rust indicator with validation and array conversion.
Parameters
----------
rust_fn : callable
The Rust function from _ferro_ta (e.g. _sma).
array_params : list of str
Names of keyword arguments that are array-like; they are converted
with _to_f64 and passed in order as positional args to rust_fn.
timeperiod_param : str, optional
If set, the value of kwargs[timeperiod_param] is validated with
check_timeperiod(..., minimum=timeperiod_min).
timeperiod_min : int
Minimum allowed value for timeperiod_param (default 1).
equal_length_groups : list of list of str, optional
Each inner list is a group of param names that must have equal length;
check_equal_length is called with that group.
**kwargs
Keyword arguments to pass. Array params are converted and passed
positionally; non-array params are passed as keyword arguments to
rust_fn (caller must ensure rust_fn signature matches).
Returns
-------
Result of rust_fn(...). Typically numpy.ndarray or tuple of ndarray.
Raises
------
FerroTAValueError, FerroTAInputError
Via check_timeperiod / check_equal_length or _normalize_rust_error.
"""
if timeperiod_param is not None and timeperiod_param in kwargs:
check_timeperiod(
kwargs[timeperiod_param],
name=timeperiod_param,
minimum=timeperiod_min,
)
if equal_length_groups is not None:
for group in equal_length_groups:
check_equal_length(**{k: kwargs[k] for k in group if k in kwargs})
# Build positional args for rust_fn in array_params order, then remaining kwargs
pos_args = [_to_f64(kwargs[p]) for p in array_params if p in kwargs]
rest_kw = {k: v for k, v in kwargs.items() if k not in array_params}
try:
return rust_fn(*pos_args, **rest_kw)
except ValueError as e:
_normalize_rust_error(e)