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
This commit is contained in:
@@ -23,6 +23,23 @@ def _parse_threshold_items(items: list[str]) -> dict[int, float]:
|
||||
return thresholds
|
||||
|
||||
|
||||
def _percentile(values: list[float], q: float) -> float:
|
||||
"""Return the q percentile using linear interpolation."""
|
||||
if not values:
|
||||
raise ValueError("Cannot compute percentile of empty sequence")
|
||||
if q <= 0:
|
||||
return min(values)
|
||||
if q >= 100:
|
||||
return max(values)
|
||||
|
||||
values = sorted(values)
|
||||
rank = (len(values) - 1) * (q / 100.0)
|
||||
lower = int(rank)
|
||||
upper = min(lower + 1, len(values) - 1)
|
||||
weight = rank - lower
|
||||
return values[lower] * (1.0 - weight) + values[upper] * weight
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Check TA-Lib benchmark JSON against regression thresholds."
|
||||
@@ -47,8 +64,20 @@ def main() -> int:
|
||||
parser.add_argument(
|
||||
"--min-speedup-floor",
|
||||
action="append",
|
||||
default=["10000=0.10", "100000=0.10"],
|
||||
help="Hard minimum per-row speedup floor per size, e.g. 100000=0.1 (repeatable)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--tail-percentile",
|
||||
type=float,
|
||||
default=10.0,
|
||||
help="Tail percentile used for distribution-based slowdown checks (default: 10)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--tail-speedup-floor",
|
||||
action="append",
|
||||
default=["10000=0.20", "100000=0.20"],
|
||||
help="Required minimum per-row speedup floor per size, e.g. 100000=0.2 (repeatable)",
|
||||
help="Required minimum tail percentile speedup per size, e.g. 100000=0.2 (repeatable)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -67,10 +96,19 @@ def main() -> int:
|
||||
for entry in data.get("summary", {}).get("by_size", [])
|
||||
if entry.get("size") is not None
|
||||
}
|
||||
results_by_size: dict[int, list[dict[str, object]]] = {}
|
||||
for row in data.get("results", []):
|
||||
if "speedup" not in row or row.get("size") is None:
|
||||
continue
|
||||
size = int(row["size"])
|
||||
results_by_size.setdefault(size, []).append(row)
|
||||
|
||||
median_floor = _parse_threshold_items(args.median_floor)
|
||||
min_speedup_floor = _parse_threshold_items(args.min_speedup_floor)
|
||||
required_sizes = sorted(set(median_floor) | set(min_speedup_floor))
|
||||
tail_speedup_floor = _parse_threshold_items(args.tail_speedup_floor)
|
||||
required_sizes = sorted(
|
||||
set(median_floor) | set(min_speedup_floor) | set(tail_speedup_floor)
|
||||
)
|
||||
|
||||
failures: list[str] = []
|
||||
for size in required_sizes:
|
||||
@@ -78,12 +116,20 @@ def main() -> int:
|
||||
if entry is None:
|
||||
failures.append(f"missing summary for size={size}")
|
||||
continue
|
||||
rows_for_size = results_by_size.get(size, [])
|
||||
if not rows_for_size:
|
||||
failures.append(f"missing detailed rows for size={size}")
|
||||
continue
|
||||
|
||||
rows = int(entry.get("rows", 0))
|
||||
med = float(entry.get("median_speedup", 0.0))
|
||||
min_s = float(entry.get("min_speedup", 0.0))
|
||||
speedups = [float(row["speedup"]) for row in rows_for_size]
|
||||
tail_s = _percentile(speedups, args.tail_percentile)
|
||||
print(
|
||||
f"size={size}: rows={rows}, median_speedup={med:.4f}, min_speedup={min_s:.4f}"
|
||||
"size="
|
||||
f"{size}: rows={rows}, median_speedup={med:.4f}, "
|
||||
f"p{args.tail_percentile:g}_speedup={tail_s:.4f}, min_speedup={min_s:.4f}"
|
||||
)
|
||||
|
||||
if rows < args.min_rows:
|
||||
@@ -94,6 +140,12 @@ def main() -> int:
|
||||
failures.append(
|
||||
f"size={size} median_speedup {med:.4f} < floor {median_floor[size]:.4f}"
|
||||
)
|
||||
if tail_s < tail_speedup_floor.get(size, float("-inf")):
|
||||
failures.append(
|
||||
"size="
|
||||
f"{size} p{args.tail_percentile:g}_speedup {tail_s:.4f} "
|
||||
f"< floor {tail_speedup_floor[size]:.4f}"
|
||||
)
|
||||
if min_s < min_speedup_floor.get(size, float("-inf")):
|
||||
failures.append(
|
||||
f"size={size} min_speedup {min_s:.4f} < floor {min_speedup_floor[size]:.4f}"
|
||||
|
||||
Reference in New Issue
Block a user