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
50 changes: 50 additions & 0 deletions .github/workflows/sdk-extras-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: SDK install extras

# Asserts that every documented `pip install "tracebloc[...]"` line names an
# extra that actually exists in the release its version floor resolves to.
#
# Catches the failure mode from backend#1858: tools-help/tracebloc.mdx
# documented `tracebloc[boosting]` and `tracebloc[survival]` for eight
# published releases after both were removed in 0.10.0.
#
# This is the one class of docs error no other check here can see:
# - Mintlify validates links and MDX; a fenced code block is opaque to it.
# - `pip` does not fail on an unknown extra. It warns, installs the core
# package, and exits 0 — so the reader's install "succeeds" and then dies
# later as an ImportError, far from the command that caused it.
#
# Runs on a schedule as well as on push, because the docs can rot without the
# docs changing: a floating `>=` floor silently re-points at each new release,
# so an extra removed upstream breaks a page nobody edited.
#
# No step interpolates event data into a shell command; the only expression
# used is github.ref, in the concurrency key.

on:
push:
branches: [develop, main]
pull_request:
paths:
- '**.mdx'
- '**.md'
- 'scripts/check-sdk-extras.py'
- '.github/workflows/sdk-extras-check.yml'
schedule:
- cron: '30 6 * * 1'
workflow_dispatch:

permissions:
contents: read

concurrency:
group: sdk-extras-${{ github.ref }}
cancel-in-progress: true

jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0

- name: Validate documented extras against PyPI
run: python3 scripts/check-sdk-extras.py
35 changes: 21 additions & 14 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ help:
@echo "tracebloc/docs — make targets"
@echo
@echo " check mint broken-links (~2 s) — run this before every push"
@echo " check-all the same; see below for why"
@echo " check-all check + the SDK install-extras gate (needs network)"
@echo " setup npm i -g mint"
@echo
@echo " dev mint dev — local preview on http://localhost:3000"
Expand All @@ -31,25 +31,32 @@ help:
#
# MEASURED at 1.8 s on the current tree, and green.
#
# check-all is the same set, and that is honest rather than lazy. This
# repo has no test suite and no content gate in CI: the only automated
# checks on a docs PR are the org-shared ones (gitleaks + house-rules,
# which need the shared checker from tracebloc/.github and are not
# reproducible from a working tree), and preview-page-coverage.yml —
# which probes the RENDERED site over HTTP after Mintlify has deployed,
# so by construction it cannot run against local files.
# This repo has no test suite. The automated checks on a docs PR are the
# org-shared ones (gitleaks + house-rules, which need the shared checker
# from tracebloc/.github and are not reproducible from a working tree),
# preview-page-coverage.yml — which probes the RENDERED site over HTTP
# after Mintlify has deployed, so by construction it cannot run against
# local files — and sdk-extras-check.yml, which CAN run locally and is
# wired into check-all below.
#
# `mint broken-links` is therefore the whole of what a person can
# usefully check here before pushing. It is also the check that would
# have caught the class of problem preview-page-coverage.yml exists to
# catch, one step earlier.
# `mint broken-links` is what catches a bad link before pushing. It is
# also the check that would have caught the class of problem
# preview-page-coverage.yml exists to catch, one step earlier.
.PHONY: check
check: guard-mint
$(MINT) broken-links

# check-all adds the one gate that needs network: every documented
# `pip install "tracebloc[...]"` line must name an extra that really
# exists in the release its floor resolves to (backend#1858). Kept out of
# `check` so the fast path stays offline and under the 60 s budget.
.PHONY: check-all
check-all: check
@echo "==> check-all: green (this repo has no separate slow tier — see the Makefile comment)"
check-all: check check-sdk-extras
@echo "==> check-all: green"

.PHONY: check-sdk-extras
check-sdk-extras:
python3 scripts/check-sdk-extras.py

# setup: the Mintlify CLI, exactly as CONTRIBUTING.md prescribes. No
# pre-commit / pre-push hook is installed here — that is a later step of
Expand Down
14 changes: 9 additions & 5 deletions join-use-case/start-training.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,24 @@ Then, install the [Anaconda](https://www.anaconda.com/docs/getting-started/anaco
Create a new environment, name it for example "tracebloc":

```bash
conda create -n tracebloc python=3.9
conda create -n tracebloc python=3.12
conda activate tracebloc
```

The SDK requires Python 3.11 or 3.12. On an older Python, no release satisfies
the `>=0.18.1` requirement below, so `pip` stops with a `No matching distribution
found` error instead of installing anything.

Then, install requirements:

```bash
python -m pip install --upgrade pip

# Install with the extra that matches your framework:
pip install "tracebloc[pytorch]>=0.8.1"
# pip install "tracebloc[tensorflow]>=0.8.1"
# pip install "tracebloc[sklearn]>=0.8.1"
# pip install "tracebloc[all]>=0.8.1" # legacy behaviour — all frameworks
pip install "tracebloc[pytorch]>=0.18.1"
# pip install "tracebloc[tensorflow]>=0.18.1"
# pip install "tracebloc[sklearn]>=0.18.1"
# pip install "tracebloc[all]>=0.18.1" # all frameworks
```

## Install and Launch Jupyter Notebook
Expand Down
179 changes: 179 additions & 0 deletions scripts/check-sdk-extras.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
#!/usr/bin/env python3
"""Validate every documented `pip install "tracebloc[...]"` line against PyPI.

Why this exists (backend#1858)
------------------------------
`tools-help/tracebloc.mdx` documented `tracebloc[boosting]` and
`tracebloc[survival]` for eight published releases after both extras were
removed in 0.10.0. Nothing caught it, because this is the one class of docs
error that no docs tool can see:

* Mintlify validates links and MDX. A fenced code block is opaque to it.
* `pip` does NOT fail on an unknown extra. It prints
"WARNING: tracebloc X does not provide the extra 'boosting'", installs
the core package, and exits 0. The user's build appears to succeed and
then dies later as an ImportError, far from the command that caused it.

So a wrong extra in the docs silently mis-installs software. This script
closes that gap by resolving each documented extra against the published
`Provides-Extra` metadata for the version the documented floor selects.

It also checks the version floor itself, because the floor is what made the
original bug silent: `>=0.8.1` floats forward to a release that no longer has
the extras, while still being satisfiable by an ancient release on an old
Python.

Usage:
python3 scripts/check-sdk-extras.py # scan the repo
python3 scripts/check-sdk-extras.py FILE... # scan specific files

Exits 0 if every documented extra exists, 1 otherwise. Requires network
access to pypi.org.
"""

from __future__ import annotations

import json
import re
import sys
import urllib.error
import urllib.request
from pathlib import Path

PACKAGE = "tracebloc"
PYPI_URL = f"https://pypi.org/pypi/{PACKAGE}/json"

# Matches: tracebloc[a,b]>=1.2.3 / tracebloc[a] / tracebloc[a]==1.2.3
# Captures the extras list and, when present, the version specifier.
SPEC_RE = re.compile(
r"\b" + PACKAGE + r"\[([^\]]+)\]\s*(?:(==|>=|~=|>)\s*([0-9][0-9A-Za-z.*+!-]*))?"
)

DOC_SUFFIXES = {".mdx", ".md"}
SKIP_DIRS = {".git", "node_modules", ".venv", "images"}


def fetch_metadata() -> dict:
try:
with urllib.request.urlopen(PYPI_URL, timeout=30) as resp:
return json.load(resp)
except (urllib.error.URLError, TimeoutError, json.JSONDecodeError) as exc:
sys.exit(f"error: could not read {PYPI_URL}: {exc}")


def parse_version(value: str) -> tuple:
"""Coarse numeric version key. Good enough to order this package's tags."""
parts = []
for chunk in value.split("."):
digits = re.match(r"\d+", chunk)
parts.append(int(digits.group()) if digits else 0)
return tuple(parts)


def resolve_version(floor: str | None, operator: str | None, releases: list[str]) -> str:
"""Which published version does this documented specifier actually select?

`pip` picks the NEWEST version satisfying the specifier, so that — not the
floor itself — is the version whose extras the reader ends up with.
"""
if floor is None:
return max(releases, key=parse_version)
if operator == "==":
return floor
candidates = [r for r in releases if parse_version(r) >= parse_version(floor)]
if not candidates:
return floor
return max(candidates, key=parse_version)


def iter_doc_files(roots: list[str]) -> list[Path]:
if roots:
return [Path(r) for r in roots]
found: list[Path] = []
for path in Path(".").rglob("*"):
if any(part in SKIP_DIRS for part in path.parts):
continue
if path.is_file() and path.suffix in DOC_SUFFIXES:
found.append(path)
return sorted(found)


def main(argv: list[str]) -> int:
meta = fetch_metadata()
releases = sorted(meta["releases"].keys(), key=parse_version)
latest = meta["info"]["version"]

# Provides-Extra per version needs a per-version fetch; cache it.
extras_cache: dict[str, set[str]] = {
latest: set(meta["info"].get("provides_extra") or [])
}

def extras_for(version: str) -> set[str] | None:
if version in extras_cache:
return extras_cache[version]
url = f"https://pypi.org/pypi/{PACKAGE}/{version}/json"
try:
with urllib.request.urlopen(url, timeout=30) as resp:
data = json.load(resp)
except (urllib.error.URLError, TimeoutError, json.JSONDecodeError):
extras_cache[version] = None
return None
extras_cache[version] = set(data["info"].get("provides_extra") or [])
return extras_cache[version]

failures: list[str] = []
checked = 0

for path in iter_doc_files(argv):
try:
text = path.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError):
continue

for lineno, line in enumerate(text.splitlines(), start=1):
for match in SPEC_RE.finditer(line):
extras_raw, operator, floor = match.groups()
extras = [e.strip() for e in extras_raw.split(",") if e.strip()]
checked += 1

if floor is not None and floor not in releases:
failures.append(
f"{path}:{lineno}: version {floor} is not published on PyPI"
)
continue

version = resolve_version(floor, operator, releases)
available = extras_for(version)
if available is None:
failures.append(
f"{path}:{lineno}: could not read metadata for {PACKAGE} {version}"
)
continue

for extra in extras:
if extra not in available:
failures.append(
f"{path}:{lineno}: {PACKAGE}[{extra}] does not exist in "
f"{version} (the version '{operator or ''}{floor or 'latest'}' "
f"resolves to). Available: {', '.join(sorted(available))}"
)

print(f"Checked {checked} documented '{PACKAGE}[...]' spec(s); latest release is {latest}.")

if failures:
print(f"\n{len(failures)} problem(s) found:\n", file=sys.stderr)
for failure in failures:
print(f" {failure}", file=sys.stderr)
print(
"\nNote: pip warns and exits 0 on an unknown extra, so a wrong extra here "
"silently installs the core SDK only.",
file=sys.stderr,
)
return 1

print("All documented extras exist in the versions they resolve to.")
return 0


if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
18 changes: 12 additions & 6 deletions tools-help/tracebloc.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,23 @@ description: "Python library for uploading models, linking them with datasets, c

## Installation

Requires Python 3.11 or 3.12.

Pick the extra that matches your ML framework — the default install ships the core SDK only (~140 MB, ~30 sec) instead of every framework (~8 GB):

```bash
pip install "tracebloc[pytorch]>=0.8.1" # most users
# pip install "tracebloc[tensorflow]>=0.8.1" # TensorFlow
# pip install "tracebloc[sklearn]>=0.8.1" # scikit-learn only
# pip install "tracebloc[boosting]>=0.8.1" # XGBoost / CatBoost / LightGBM
# pip install "tracebloc[survival]>=0.8.1" # lifelines / scikit-survival
# pip install "tracebloc[all]>=0.8.1" # everything
pip install "tracebloc[pytorch]>=0.18.1" # most users
# pip install "tracebloc[tensorflow]>=0.18.1" # TensorFlow
# pip install "tracebloc[sklearn]>=0.18.1" # scikit-learn, incl. XGBoost / CatBoost / LightGBM
# pip install "tracebloc[xgboost]>=0.18.1" # one boosting backend only (or: catboost, lightgbm)
# pip install "tracebloc[lifelines,scikit-survival]>=0.18.1" # survival analysis
# pip install "tracebloc[all]>=0.18.1" # everything
```

<Note>
The umbrella `[boosting]` and `[survival]` extras were removed in 0.10.0, along with `[huggingface]` (the Hugging Face stack now ships inside `[pytorch]`). Use the per-library extras above. `pip` does not fail on an unknown extra — it prints a warning, installs the core SDK only, and exits 0, so the mistake surfaces later as an `ImportError`.
</Note>

## Key Features

- Upload model files and pretrained weights
Expand Down
Loading