Ir para o conteúdo

tempestweb.tabular

Inferência sklearn→ONNX no browser, irmã do vision. O manifesto — quais features o modelo espera e em que ordem — é o que impede a predição silenciosamente errada. Modos A e B (o Modo C recusa o import no build). Treinar e exportar são passo de build em venv descartável, não dependência.

Guia com exemplos: Inferência tabular no browser.

tempestweb.tabular

Tabular inference in the browser — the sibling of tempestweb.vision.

Vision runs a model on pixels. This runs one on a row of numbers, which is the commonest kind of ML in a business app: a risk score, a demand forecast, a lead classification. Without it, those had to call an endpoint — which breaks offline-first, one of the framework's promises.

Modules

* :mod:`manifest` — which features the model expects, and in what order.
* :mod:`predictor` — `TabularPredictor`, loaded lazily, addressed by name.
* :mod:`errors` — one named error per way a row can fail to match.
Example
from tempestweb.tabular import TabularPredictor

PREDICTOR = TabularPredictor("/models/risk.onnx", manifest="/models/risk.json")

prediction = await PREDICTOR.predict({"age": 30, "income": 3200.0})
print(prediction.score, prediction.label, prediction.probabilities)

The manifest is what keeps this from being silently wrong

An ONNX model is a function from an unlabelled vector of floats to a number: the order carries all the meaning, and nothing in the runtime checks it. An app that sends {"idade": 30} to a model trained on age does not fail — it reads a zero where the age should be and answers a plausible, wrong score, and nothing downstream can tell.

With a manifest that raises MissingFeatureError, naming the feature that is missing and the one that was sent instead, because the two together are usually one typo.

Training and export are a build step, not a dependency

Exporting sklearn to ONNX runs in a throwaway environment (uvx --from skl2onnx …), documented in the recipe. Nothing here depends on sklearn, skl2onnx or numpy at runtime: those bounds would propagate to every tempestweb consumer for a step that happens once, on a developer's machine.

Modes A and B only

Mode C serves a fixed set of modules — tempest_core, tempestweb.components and tempestweb.native — and refuses this import at build time.

Import everything from this package level rather than from submodules.

CompactModel dataclass

A parsed compact model: its header, and its arrays.

Attributes:

Name Type Description
kind str

"linear" or "tree_ensemble" — which reader scores it.

task str

"classification" or "regression".

link str

How raw scores become probabilities ("softmax", "sigmoid", "normalize") or stay as they are ("identity").

classes tuple[str, ...]

Class labels in score-column order. Empty for a regressor.

n_features int

Values expected per row.

n_outputs int

Score columns per row.

n_trees int

Trees in the ensemble; 0 for a linear model.

estimator str

Class name of the exported estimator, which names the model in every :class:CompactFormatError this reader raises — the most useful thing in the header for whoever is debugging the file. The exporter also records class_type; this reader does not read it, because :attr:classes arrives already stringified and :attr:~tempestweb.tabular.Prediction.label is a str, so there is nothing for it to influence.

feature_names tuple[str, ...]

The column order the model was trained on, when the export recorded it.

offset tuple[float, ...]

Per-feature offset of a folded scaler, empty when there is none.

scale tuple[float, ...]

Per-feature scale of a folded scaler, empty when there is none.

sections dict[str, Sequence[float]]

The decoded arrays, keyed by the name the header gave them.

Source code in tempestweb/tabular/compact.py
@dataclass(frozen=True)
class CompactModel:
    """A parsed compact model: its header, and its arrays.

    Attributes:
        kind: ``"linear"`` or ``"tree_ensemble"`` — which reader scores it.
        task: ``"classification"`` or ``"regression"``.
        link: How raw scores become probabilities (``"softmax"``, ``"sigmoid"``,
            ``"normalize"``) or stay as they are (``"identity"``).
        classes: Class labels in score-column order. Empty for a regressor.
        n_features: Values expected per row.
        n_outputs: Score columns per row.
        n_trees: Trees in the ensemble; ``0`` for a linear model.
        estimator: Class name of the exported estimator, which names the model
            in every :class:`CompactFormatError` this reader raises — the most
            useful thing in the header for whoever is debugging the file. The
            exporter also records ``class_type``; this reader does not read it,
            because :attr:`classes` arrives already stringified and
            :attr:`~tempestweb.tabular.Prediction.label` is a ``str``, so there
            is nothing for it to influence.
        feature_names: The column order the model was trained on, when the
            export recorded it.
        offset: Per-feature offset of a folded scaler, empty when there is none.
        scale: Per-feature scale of a folded scaler, empty when there is none.
        sections: The decoded arrays, keyed by the name the header gave them.
    """

    kind: str
    task: str
    link: str
    classes: tuple[str, ...] = ()
    n_features: int = 0
    n_outputs: int = 0
    n_trees: int = 0
    estimator: str = ""
    feature_names: tuple[str, ...] = ()
    offset: tuple[float, ...] = ()
    scale: tuple[float, ...] = ()
    sections: dict[str, Sequence[float]] = field(default_factory=dict)

    def manifest(self) -> FeatureManifest:
        """Build the manifest the file itself declares.

        Returns:
            The :class:`~tempestweb.tabular.FeatureManifest` over
            :attr:`feature_names` and :attr:`classes`.

        Raises:
            ManifestError: If the export recorded no feature names — the file
                can still be scored positionally, but not addressed by name.
        """
        return FeatureManifest(features=self.feature_names, classes=self.classes)

    def section(self, name: str) -> Sequence[float]:
        """Read one decoded section.

        Args:
            name: The section name the header gave it (e.g. ``"coef"``).

        Returns:
            Its values.

        Raises:
            CompactFormatError: If the model carries no such section.
        """
        try:
            return self.sections[name]
        except KeyError:
            raise CompactFormatError(
                f"a {self.estimator or self.kind} compact model needs the "
                f"{name!r} section; this file carries: "
                f"{', '.join(sorted(self.sections)) or 'none'}"
            ) from None

manifest

manifest() -> FeatureManifest

Build the manifest the file itself declares.

Returns:

Name Type Description
The FeatureManifest

class:~tempestweb.tabular.FeatureManifest over

FeatureManifest

attr:feature_names and :attr:classes.

Raises:

Type Description
ManifestError

If the export recorded no feature names — the file can still be scored positionally, but not addressed by name.

Source code in tempestweb/tabular/compact.py
def manifest(self) -> FeatureManifest:
    """Build the manifest the file itself declares.

    Returns:
        The :class:`~tempestweb.tabular.FeatureManifest` over
        :attr:`feature_names` and :attr:`classes`.

    Raises:
        ManifestError: If the export recorded no feature names — the file
            can still be scored positionally, but not addressed by name.
    """
    return FeatureManifest(features=self.feature_names, classes=self.classes)

section

section(name: str) -> Sequence[float]

Read one decoded section.

Parameters:

Name Type Description Default
name str

The section name the header gave it (e.g. "coef").

required

Returns:

Type Description
Sequence[float]

Its values.

Raises:

Type Description
CompactFormatError

If the model carries no such section.

Source code in tempestweb/tabular/compact.py
def section(self, name: str) -> Sequence[float]:
    """Read one decoded section.

    Args:
        name: The section name the header gave it (e.g. ``"coef"``).

    Returns:
        Its values.

    Raises:
        CompactFormatError: If the model carries no such section.
    """
    try:
        return self.sections[name]
    except KeyError:
        raise CompactFormatError(
            f"a {self.estimator or self.kind} compact model needs the "
            f"{name!r} section; this file carries: "
            f"{', '.join(sorted(self.sections)) or 'none'}"
        ) from None

CompactPredictor

A compact model, loaded lazily and addressed by feature name.

The file is downloaded on the first prediction, not in __init__: building a predictor at module scope must not fetch anything, and an app that defines three and uses one should pay for one.

Attributes:

Name Type Description
model_url str

Where the .tmc file is served from.

Source code in tempestweb/tabular/compact.py
class CompactPredictor:
    """A compact model, loaded lazily and addressed by feature name.

    The file is downloaded on the first prediction, not in ``__init__``: building
    a predictor at module scope must not fetch anything, and an app that defines
    three and uses one should pay for one.

    Attributes:
        model_url: Where the ``.tmc`` file is served from.
    """

    def __init__(
        self,
        model_url: str,
        *,
        manifest: FeatureManifest | Mapping[str, object] | str | None = None,
    ) -> None:
        """Describe a model without downloading it.

        Args:
            model_url: Where the ``.tmc`` file is served from, same-origin in the
                artifact (``"/models/risk.tmc"``).
            manifest: Overrides the feature order the file itself records. A
                :class:`~tempestweb.tabular.FeatureManifest`, a decoded manifest,
                or a URL to fetch one from. Leave it out for any export that
                recorded ``feature_names`` — which is every export that was
                given them.
        """
        self.model_url: str = model_url
        self._manifest_source: FeatureManifest | Mapping[str, object] | str | None = (
            manifest
        )
        self._manifest: FeatureManifest | None = None
        self._model: CompactModel | None = None

    async def load(self) -> CompactModel:
        """Download and parse the model, or return the one already parsed.

        Returns:
            The :class:`CompactModel`.

        Raises:
            CompactFormatError: If the bytes are not a compact model this reader
                understands.
            NativeError: If the file cannot be downloaded (``model_load``).
        """
        if self._model is None:
            self._model = parse(await native_compact.load(self.model_url))
        return self._model

    async def manifest(self) -> FeatureManifest:
        """Resolve the feature order, from the file or from the override.

        Returns:
            The :class:`~tempestweb.tabular.FeatureManifest`, cached after the
            first resolution.

        Raises:
            ManifestError: If neither the file nor the override declares
                features.
            NativeError: If a manifest URL cannot be fetched.
        """
        if self._manifest is not None:
            return self._manifest
        source = self._manifest_source
        if source is None:
            self._manifest = (await self.load()).manifest()
        elif isinstance(source, FeatureManifest):
            self._manifest = source
        elif isinstance(source, str):
            response = await native_http.request("GET", source)
            self._manifest = manifest_from_json(response.text)
        else:
            self._manifest = manifest_from_dict(source)
        return self._manifest

    async def predict(
        self, row: Mapping[str, object], *, strict: bool = True
    ) -> Prediction:
        """Score one row.

        Args:
            row: The feature values, in any order — the manifest imposes the one
                the model needs.
            strict: Whether a feature the model does not declare is an error.

        Returns:
            The :class:`~tempestweb.tabular.Prediction`.

        Raises:
            ManifestError: If the manifest orders a different number of features
                than the model expects.
            MissingFeatureError: If the row lacks a declared feature.
            UnknownFeatureError: If ``strict`` and the row carries an undeclared
                one.
            CompactFormatError: If the file is not a compact model this reader
                understands.
            NativeError: If the download fails.
        """
        return (await self.predict_many([row], strict=strict))[0]

    async def predict_many(
        self,
        rows: Sequence[Mapping[str, object]],
        *,
        strict: bool = True,
    ) -> list[Prediction]:
        """Score several rows.

        Args:
            rows: The rows to score.
            strict: Whether a feature the model does not declare is an error.

        Returns:
            One :class:`~tempestweb.tabular.Prediction` per row, in order. An
            empty ``rows`` returns ``[]`` without downloading the model —
            scoring nothing is valid.

        Raises:
            ManifestError: If the manifest orders a different number of features
                than the model expects — the mismatch the manifest exists to
                catch, arriving from the manifest's own side.
            MissingFeatureError: If a row lacks a declared feature.
            UnknownFeatureError: If ``strict`` and a row carries an undeclared
                one.
            CompactFormatError: If the file is not a compact model this reader
                understands.
            NativeError: If the download fails.
        """
        if not rows:
            return []
        manifest = await self.manifest()
        model = await self.load()
        if len(manifest.features) != model.n_features:
            raise ManifestError(
                f"the manifest orders {len(manifest.features)} features and this "
                f"{model.estimator or model.kind} model expects "
                f"{model.n_features}; a row ordered by a manifest of the wrong "
                "size still scores, it just scores the wrong coefficients"
            )
        vectors = [manifest.vector(row, strict=strict) for row in rows]
        scores = [_score(model, _preprocess(model, vector)) for vector in vectors]
        return [_finish(model, manifest, row_scores) for row_scores in scores]

load async

load() -> CompactModel

Download and parse the model, or return the one already parsed.

Returns:

Name Type Description
The CompactModel

class:CompactModel.

Raises:

Type Description
CompactFormatError

If the bytes are not a compact model this reader understands.

NativeError

If the file cannot be downloaded (model_load).

Source code in tempestweb/tabular/compact.py
async def load(self) -> CompactModel:
    """Download and parse the model, or return the one already parsed.

    Returns:
        The :class:`CompactModel`.

    Raises:
        CompactFormatError: If the bytes are not a compact model this reader
            understands.
        NativeError: If the file cannot be downloaded (``model_load``).
    """
    if self._model is None:
        self._model = parse(await native_compact.load(self.model_url))
    return self._model

manifest async

manifest() -> FeatureManifest

Resolve the feature order, from the file or from the override.

Returns:

Name Type Description
The FeatureManifest

class:~tempestweb.tabular.FeatureManifest, cached after the

FeatureManifest

first resolution.

Raises:

Type Description
ManifestError

If neither the file nor the override declares features.

NativeError

If a manifest URL cannot be fetched.

Source code in tempestweb/tabular/compact.py
async def manifest(self) -> FeatureManifest:
    """Resolve the feature order, from the file or from the override.

    Returns:
        The :class:`~tempestweb.tabular.FeatureManifest`, cached after the
        first resolution.

    Raises:
        ManifestError: If neither the file nor the override declares
            features.
        NativeError: If a manifest URL cannot be fetched.
    """
    if self._manifest is not None:
        return self._manifest
    source = self._manifest_source
    if source is None:
        self._manifest = (await self.load()).manifest()
    elif isinstance(source, FeatureManifest):
        self._manifest = source
    elif isinstance(source, str):
        response = await native_http.request("GET", source)
        self._manifest = manifest_from_json(response.text)
    else:
        self._manifest = manifest_from_dict(source)
    return self._manifest

predict async

predict(row: Mapping[str, object], *, strict: bool = True) -> Prediction

Score one row.

Parameters:

Name Type Description Default
row Mapping[str, object]

The feature values, in any order — the manifest imposes the one the model needs.

required
strict bool

Whether a feature the model does not declare is an error.

True

Returns:

Name Type Description
The Prediction

class:~tempestweb.tabular.Prediction.

Raises:

Type Description
ManifestError

If the manifest orders a different number of features than the model expects.

MissingFeatureError

If the row lacks a declared feature.

UnknownFeatureError

If strict and the row carries an undeclared one.

CompactFormatError

If the file is not a compact model this reader understands.

NativeError

If the download fails.

Source code in tempestweb/tabular/compact.py
async def predict(
    self, row: Mapping[str, object], *, strict: bool = True
) -> Prediction:
    """Score one row.

    Args:
        row: The feature values, in any order — the manifest imposes the one
            the model needs.
        strict: Whether a feature the model does not declare is an error.

    Returns:
        The :class:`~tempestweb.tabular.Prediction`.

    Raises:
        ManifestError: If the manifest orders a different number of features
            than the model expects.
        MissingFeatureError: If the row lacks a declared feature.
        UnknownFeatureError: If ``strict`` and the row carries an undeclared
            one.
        CompactFormatError: If the file is not a compact model this reader
            understands.
        NativeError: If the download fails.
    """
    return (await self.predict_many([row], strict=strict))[0]

predict_many async

predict_many(rows: Sequence[Mapping[str, object]], *, strict: bool = True) -> list[Prediction]

Score several rows.

Parameters:

Name Type Description Default
rows Sequence[Mapping[str, object]]

The rows to score.

required
strict bool

Whether a feature the model does not declare is an error.

True

Returns:

Name Type Description
One list[Prediction]

class:~tempestweb.tabular.Prediction per row, in order. An

list[Prediction]

empty rows returns [] without downloading the model —

list[Prediction]

scoring nothing is valid.

Raises:

Type Description
ManifestError

If the manifest orders a different number of features than the model expects — the mismatch the manifest exists to catch, arriving from the manifest's own side.

MissingFeatureError

If a row lacks a declared feature.

UnknownFeatureError

If strict and a row carries an undeclared one.

CompactFormatError

If the file is not a compact model this reader understands.

NativeError

If the download fails.

Source code in tempestweb/tabular/compact.py
async def predict_many(
    self,
    rows: Sequence[Mapping[str, object]],
    *,
    strict: bool = True,
) -> list[Prediction]:
    """Score several rows.

    Args:
        rows: The rows to score.
        strict: Whether a feature the model does not declare is an error.

    Returns:
        One :class:`~tempestweb.tabular.Prediction` per row, in order. An
        empty ``rows`` returns ``[]`` without downloading the model —
        scoring nothing is valid.

    Raises:
        ManifestError: If the manifest orders a different number of features
            than the model expects — the mismatch the manifest exists to
            catch, arriving from the manifest's own side.
        MissingFeatureError: If a row lacks a declared feature.
        UnknownFeatureError: If ``strict`` and a row carries an undeclared
            one.
        CompactFormatError: If the file is not a compact model this reader
            understands.
        NativeError: If the download fails.
    """
    if not rows:
        return []
    manifest = await self.manifest()
    model = await self.load()
    if len(manifest.features) != model.n_features:
        raise ManifestError(
            f"the manifest orders {len(manifest.features)} features and this "
            f"{model.estimator or model.kind} model expects "
            f"{model.n_features}; a row ordered by a manifest of the wrong "
            "size still scores, it just scores the wrong coefficients"
        )
    vectors = [manifest.vector(row, strict=strict) for row in rows]
    scores = [_score(model, _preprocess(model, vector)) for vector in vectors]
    return [_finish(model, manifest, row_scores) for row_scores in scores]

CompactFormatError

Bases: TabularError

The bytes are not a compact model this reader understands.

Raised for wrong magic bytes, a layout version this reader does not implement, a section the header promised and the file does not hold, or a kind/link outside the format. Every one of them means the file was written by something other than tempest_fastapi_sdk.modelops.export_sklearn_to_compact at the version this reader was built against — guessing past that would predict on garbage.

Source code in tempestweb/tabular/errors.py
class CompactFormatError(TabularError):
    """The bytes are not a compact model this reader understands.

    Raised for wrong magic bytes, a layout version this reader does not
    implement, a section the header promised and the file does not hold, or a
    ``kind``/``link`` outside the format. Every one of them means the file was
    written by something other than
    ``tempest_fastapi_sdk.modelops.export_sklearn_to_compact`` at the version
    this reader was built against — guessing past that would predict on garbage.
    """

ManifestError

Bases: TabularError

The manifest itself is unusable.

Raised for a manifest with no features, with duplicates, or whose JSON is not the shape a manifest has. A broken manifest is a build-time mistake and is worth failing loudly on, because everything after it is built on the order it declares.

Source code in tempestweb/tabular/errors.py
class ManifestError(TabularError):
    """The manifest itself is unusable.

    Raised for a manifest with no features, with duplicates, or whose JSON is not
    the shape a manifest has. A broken manifest is a build-time mistake and is
    worth failing loudly on, because everything after it is built on the order it
    declares.
    """

MissingFeatureError

Bases: TabularError

The row does not carry every feature the model was trained on.

Attributes:

Name Type Description
missing tuple[str, ...]

The declared features the row lacks.

extra tuple[str, ...]

Features the row carries that the model does not know, listed alongside because the pair is usually one typo — idade present and age missing is one mistake, not two.

Source code in tempestweb/tabular/errors.py
class MissingFeatureError(TabularError):
    """The row does not carry every feature the model was trained on.

    Attributes:
        missing: The declared features the row lacks.
        extra: Features the row carries that the model does not know, listed
            alongside because the pair is usually one typo — ``idade`` present
            and ``age`` missing is one mistake, not two.
    """

    def __init__(self, missing: Iterable[str], extra: Iterable[str] = ()) -> None:
        """Build the error from both halves of the mismatch.

        Args:
            missing: The declared features the row lacks.
            extra: Features the row carries that the model does not know.
        """
        self.missing: tuple[str, ...] = tuple(missing)
        self.extra: tuple[str, ...] = tuple(extra)
        message = f"row is missing {len(self.missing)} feature(s): " + ", ".join(
            self.missing
        )
        if self.extra:
            message += "; it carries instead: " + ", ".join(self.extra)
        super().__init__(message)

PredictionError

Bases: TabularError

The model ran but its output could not be read as a prediction.

Source code in tempestweb/tabular/errors.py
class PredictionError(TabularError):
    """The model ran but its output could not be read as a prediction."""

TabularError

Bases: ValueError

Base class for every tabular failure.

Source code in tempestweb/tabular/errors.py
class TabularError(ValueError):
    """Base class for every tabular failure."""

UnknownFeatureError

Bases: TabularError

The row carries a feature the model was not trained on.

Attributes:

Name Type Description
unknown tuple[str, ...]

The features the model does not declare.

Source code in tempestweb/tabular/errors.py
class UnknownFeatureError(TabularError):
    """The row carries a feature the model was not trained on.

    Attributes:
        unknown: The features the model does not declare.
    """

    def __init__(self, unknown: Iterable[str]) -> None:
        """Build the error from the unexpected features.

        Args:
            unknown: The features the model does not declare.
        """
        self.unknown: tuple[str, ...] = tuple(unknown)
        super().__init__(
            "row carries feature(s) the model does not declare: "
            + ", ".join(self.unknown)
        )

FeatureManifest dataclass

What a model expects, declared rather than remembered.

Attributes:

Name Type Description
features tuple[str, ...]

The feature names, in the order the model was trained on. The order is the contract; the names are what makes it checkable.

version str

The model version, for logs and cache busting.

outputs tuple[str, ...]

The model's output names, in declaration order.

classes tuple[str, ...]

The class labels a classifier answers, in index order. Empty for a regressor.

Source code in tempestweb/tabular/manifest.py
@dataclass(frozen=True)
class FeatureManifest:
    """What a model expects, declared rather than remembered.

    Attributes:
        features: The feature names, **in the order the model was trained on**.
            The order is the contract; the names are what makes it checkable.
        version: The model version, for logs and cache busting.
        outputs: The model's output names, in declaration order.
        classes: The class labels a classifier answers, in index order. Empty for
            a regressor.
    """

    features: tuple[str, ...]
    version: str = ""
    outputs: tuple[str, ...] = ()
    classes: tuple[str, ...] = ()

    def __post_init__(self) -> None:
        """Reject a manifest that cannot describe anything.

        Raises:
            ManifestError: If it declares no features, or declares one twice —
                a duplicate makes the order ambiguous, which is exactly what the
                manifest exists to fix.
        """
        if not self.features:
            raise ManifestError("a manifest must declare at least one feature")
        duplicates = sorted(
            {name for name in self.features if self.features.count(name) > 1}
        )
        if duplicates:
            raise ManifestError(
                "a manifest cannot declare a feature twice: " + ", ".join(duplicates)
            )

    def vector(
        self,
        row: Mapping[str, Any],
        *,
        strict: bool = True,
    ) -> list[float]:
        """Order one row into the vector the model expects.

        Args:
            row: The feature values, in any order.
            strict: Whether a feature the model does not declare is an error.
                On by default: a stray key is almost always a typo, and dropping
                it silently is how a wrong prediction gets made.

        Returns:
            The values as floats, ordered by :attr:`features`.

        Raises:
            MissingFeatureError: If the row lacks a declared feature. The message
                lists what is missing **and** what was sent instead, because the
                two together are usually one typo.
            UnknownFeatureError: If ``strict`` and the row carries a feature the
                model does not declare.
            ValueError: If a value is not a number.
        """
        missing = [name for name in self.features if name not in row]
        extra = [name for name in row if name not in self.features]
        if missing:
            raise MissingFeatureError(missing, extra)
        if strict and extra:
            raise UnknownFeatureError(extra)
        return [_number(name, row[name]) for name in self.features]

    def label_of(self, index: int) -> str:
        """Name the class at an index.

        Args:
            index: The class index the model answered.

        Returns:
            The declared label, or the index as text when the manifest declares
            no classes — a regressor has none, and a classifier shipped without
            them is still usable.
        """
        if 0 <= index < len(self.classes):
            return self.classes[index]
        return str(index)

vector

vector(row: Mapping[str, Any], *, strict: bool = True) -> list[float]

Order one row into the vector the model expects.

Parameters:

Name Type Description Default
row Mapping[str, Any]

The feature values, in any order.

required
strict bool

Whether a feature the model does not declare is an error. On by default: a stray key is almost always a typo, and dropping it silently is how a wrong prediction gets made.

True

Returns:

Type Description
list[float]

The values as floats, ordered by :attr:features.

Raises:

Type Description
MissingFeatureError

If the row lacks a declared feature. The message lists what is missing and what was sent instead, because the two together are usually one typo.

UnknownFeatureError

If strict and the row carries a feature the model does not declare.

ValueError

If a value is not a number.

Source code in tempestweb/tabular/manifest.py
def vector(
    self,
    row: Mapping[str, Any],
    *,
    strict: bool = True,
) -> list[float]:
    """Order one row into the vector the model expects.

    Args:
        row: The feature values, in any order.
        strict: Whether a feature the model does not declare is an error.
            On by default: a stray key is almost always a typo, and dropping
            it silently is how a wrong prediction gets made.

    Returns:
        The values as floats, ordered by :attr:`features`.

    Raises:
        MissingFeatureError: If the row lacks a declared feature. The message
            lists what is missing **and** what was sent instead, because the
            two together are usually one typo.
        UnknownFeatureError: If ``strict`` and the row carries a feature the
            model does not declare.
        ValueError: If a value is not a number.
    """
    missing = [name for name in self.features if name not in row]
    extra = [name for name in row if name not in self.features]
    if missing:
        raise MissingFeatureError(missing, extra)
    if strict and extra:
        raise UnknownFeatureError(extra)
    return [_number(name, row[name]) for name in self.features]

label_of

label_of(index: int) -> str

Name the class at an index.

Parameters:

Name Type Description Default
index int

The class index the model answered.

required

Returns:

Type Description
str

The declared label, or the index as text when the manifest declares

str

no classes — a regressor has none, and a classifier shipped without

str

them is still usable.

Source code in tempestweb/tabular/manifest.py
def label_of(self, index: int) -> str:
    """Name the class at an index.

    Args:
        index: The class index the model answered.

    Returns:
        The declared label, or the index as text when the manifest declares
        no classes — a regressor has none, and a classifier shipped without
        them is still usable.
    """
    if 0 <= index < len(self.classes):
        return self.classes[index]
    return str(index)

Prediction dataclass

One row's answer.

Attributes:

Name Type Description
score float

The single number the model answered. For a regressor this is the value; for a classifier it is the probability of the predicted class, or the raw output when the model reports no probabilities.

label str

The predicted class name, resolved through the manifest's classes. Empty for a regressor.

index int

The predicted class index, or -1 for a regressor.

probabilities dict[str, float]

Class name to probability, empty when the model does not report them.

Source code in tempestweb/tabular/predictor.py
@dataclass(frozen=True)
class Prediction:
    """One row's answer.

    Attributes:
        score: The single number the model answered. For a regressor this is the
            value; for a classifier it is the probability of the predicted class,
            or the raw output when the model reports no probabilities.
        label: The predicted class name, resolved through the manifest's
            ``classes``. Empty for a regressor.
        index: The predicted class index, or ``-1`` for a regressor.
        probabilities: Class name to probability, empty when the model does not
            report them.
    """

    score: float = 0.0
    label: str = ""
    index: int = -1
    probabilities: dict[str, float] = field(default_factory=dict)

TabularPredictor

A sklearn-to-ONNX model, loaded lazily and addressed by feature name.

The session is created on the first prediction, not in __init__: building a predictor at module scope must not download a model, and an app that defines three predictors and uses one should pay for one.

Attributes:

Name Type Description
model_url str

Where the .onnx file is served from.

providers list[str]

Execution providers, in preference order.

Source code in tempestweb/tabular/predictor.py
class TabularPredictor:
    """A sklearn-to-ONNX model, loaded lazily and addressed by feature name.

    The session is created on the first prediction, not in ``__init__``: building
    a predictor at module scope must not download a model, and an app that
    defines three predictors and uses one should pay for one.

    Attributes:
        model_url: Where the ``.onnx`` file is served from.
        providers: Execution providers, in preference order.
    """

    def __init__(
        self,
        model_url: str,
        *,
        manifest: FeatureManifest | Mapping[str, object] | str,
        providers: Sequence[str] | None = None,
    ) -> None:
        """Describe a model without loading it.

        Args:
            model_url: Where the ``.onnx`` file is served from, same-origin in
                the artifact (``"/models/risk.onnx"``).
            manifest: The :class:`FeatureManifest`, a decoded manifest, or a URL
                to fetch one from. A URL is fetched on the first prediction,
                alongside the model.
            providers: Execution providers, in preference order.
        """
        self.model_url: str = model_url
        self.providers: list[str] = list(providers or DEFAULT_PROVIDERS)
        self._manifest_source: FeatureManifest | Mapping[str, object] | str = manifest
        self._manifest: FeatureManifest | None = None
        self._model: OnnxModel | None = None

    async def manifest(self) -> FeatureManifest:
        """Resolve the manifest, fetching it if it was given as a URL.

        Returns:
            The :class:`FeatureManifest`, cached after the first resolution.

        Raises:
            ManifestError: If the fetched document is not a valid manifest.
            NativeError: If the manifest URL cannot be fetched.
        """
        if self._manifest is not None:
            return self._manifest
        source = self._manifest_source
        if isinstance(source, FeatureManifest):
            self._manifest = source
        elif isinstance(source, str):
            response = await native_http.request("GET", source)
            self._manifest = manifest_from_json(response.text)
        else:
            self._manifest = manifest_from_dict(source)
        return self._manifest

    async def load(self) -> OnnxModel:
        """Create the inference session, or return the one already created.

        Returns:
            The :class:`~tempestweb.native.onnx.OnnxModel` handle.

        Raises:
            NativeError: If the model fails to download or compile
                (``model_load``).
        """
        if self._model is None:
            self._model = await onnx_load(self.model_url, providers=self.providers)
        return self._model

    async def predict(
        self, row: Mapping[str, object], *, strict: bool = True
    ) -> Prediction:
        """Score one row.

        Args:
            row: The feature values, in any order — the manifest imposes the one
                the model needs.
            strict: Whether a feature the model does not declare is an error.

        Returns:
            The :class:`Prediction`.

        Raises:
            MissingFeatureError: If the row lacks a declared feature.
            UnknownFeatureError: If ``strict`` and the row carries an undeclared
                one.
            PredictionError: If the model answers nothing readable.
            NativeError: If loading or inference fails.
        """
        predictions = await self.predict_many([row], strict=strict)
        if not predictions:
            raise PredictionError("the model answered no rows")
        return predictions[0]

    async def predict_many(
        self,
        rows: Sequence[Mapping[str, object]],
        *,
        strict: bool = True,
    ) -> list[Prediction]:
        """Score several rows in a single inference run.

        One run rather than one per row: crossing the bridge and entering the
        runtime dominate the cost for a model this size, so a hundred rows scored
        together are far cheaper than a hundred scored apart.

        Args:
            rows: The rows to score.
            strict: Whether a feature the model does not declare is an error.

        Returns:
            One :class:`Prediction` per row, in order. An empty ``rows`` returns
            ``[]`` without loading the model — scoring nothing is valid.

        Raises:
            MissingFeatureError: If a row lacks a declared feature.
            UnknownFeatureError: If ``strict`` and a row carries an undeclared
                one.
            PredictionError: If the model answers nothing readable.
            NativeError: If loading or inference fails.
        """
        if not rows:
            return []
        manifest = await self.manifest()
        vectors = [manifest.vector(row, strict=strict) for row in rows]

        model = await self.load()
        tensor = _float_tensor(vectors)
        outputs = await onnx_run(model.session_id, {model.input_name: tensor})
        return _read_outputs(outputs, manifest, len(rows))

manifest async

manifest() -> FeatureManifest

Resolve the manifest, fetching it if it was given as a URL.

Returns:

Name Type Description
The FeatureManifest

class:FeatureManifest, cached after the first resolution.

Raises:

Type Description
ManifestError

If the fetched document is not a valid manifest.

NativeError

If the manifest URL cannot be fetched.

Source code in tempestweb/tabular/predictor.py
async def manifest(self) -> FeatureManifest:
    """Resolve the manifest, fetching it if it was given as a URL.

    Returns:
        The :class:`FeatureManifest`, cached after the first resolution.

    Raises:
        ManifestError: If the fetched document is not a valid manifest.
        NativeError: If the manifest URL cannot be fetched.
    """
    if self._manifest is not None:
        return self._manifest
    source = self._manifest_source
    if isinstance(source, FeatureManifest):
        self._manifest = source
    elif isinstance(source, str):
        response = await native_http.request("GET", source)
        self._manifest = manifest_from_json(response.text)
    else:
        self._manifest = manifest_from_dict(source)
    return self._manifest

load async

load() -> OnnxModel

Create the inference session, or return the one already created.

Returns:

Name Type Description
The OnnxModel

class:~tempestweb.native.onnx.OnnxModel handle.

Raises:

Type Description
NativeError

If the model fails to download or compile (model_load).

Source code in tempestweb/tabular/predictor.py
async def load(self) -> OnnxModel:
    """Create the inference session, or return the one already created.

    Returns:
        The :class:`~tempestweb.native.onnx.OnnxModel` handle.

    Raises:
        NativeError: If the model fails to download or compile
            (``model_load``).
    """
    if self._model is None:
        self._model = await onnx_load(self.model_url, providers=self.providers)
    return self._model

predict async

predict(row: Mapping[str, object], *, strict: bool = True) -> Prediction

Score one row.

Parameters:

Name Type Description Default
row Mapping[str, object]

The feature values, in any order — the manifest imposes the one the model needs.

required
strict bool

Whether a feature the model does not declare is an error.

True

Returns:

Name Type Description
The Prediction

class:Prediction.

Raises:

Type Description
MissingFeatureError

If the row lacks a declared feature.

UnknownFeatureError

If strict and the row carries an undeclared one.

PredictionError

If the model answers nothing readable.

NativeError

If loading or inference fails.

Source code in tempestweb/tabular/predictor.py
async def predict(
    self, row: Mapping[str, object], *, strict: bool = True
) -> Prediction:
    """Score one row.

    Args:
        row: The feature values, in any order — the manifest imposes the one
            the model needs.
        strict: Whether a feature the model does not declare is an error.

    Returns:
        The :class:`Prediction`.

    Raises:
        MissingFeatureError: If the row lacks a declared feature.
        UnknownFeatureError: If ``strict`` and the row carries an undeclared
            one.
        PredictionError: If the model answers nothing readable.
        NativeError: If loading or inference fails.
    """
    predictions = await self.predict_many([row], strict=strict)
    if not predictions:
        raise PredictionError("the model answered no rows")
    return predictions[0]

predict_many async

predict_many(rows: Sequence[Mapping[str, object]], *, strict: bool = True) -> list[Prediction]

Score several rows in a single inference run.

One run rather than one per row: crossing the bridge and entering the runtime dominate the cost for a model this size, so a hundred rows scored together are far cheaper than a hundred scored apart.

Parameters:

Name Type Description Default
rows Sequence[Mapping[str, object]]

The rows to score.

required
strict bool

Whether a feature the model does not declare is an error.

True

Returns:

Name Type Description
One list[Prediction]

class:Prediction per row, in order. An empty rows returns

list[Prediction]

[] without loading the model — scoring nothing is valid.

Raises:

Type Description
MissingFeatureError

If a row lacks a declared feature.

UnknownFeatureError

If strict and a row carries an undeclared one.

PredictionError

If the model answers nothing readable.

NativeError

If loading or inference fails.

Source code in tempestweb/tabular/predictor.py
async def predict_many(
    self,
    rows: Sequence[Mapping[str, object]],
    *,
    strict: bool = True,
) -> list[Prediction]:
    """Score several rows in a single inference run.

    One run rather than one per row: crossing the bridge and entering the
    runtime dominate the cost for a model this size, so a hundred rows scored
    together are far cheaper than a hundred scored apart.

    Args:
        rows: The rows to score.
        strict: Whether a feature the model does not declare is an error.

    Returns:
        One :class:`Prediction` per row, in order. An empty ``rows`` returns
        ``[]`` without loading the model — scoring nothing is valid.

    Raises:
        MissingFeatureError: If a row lacks a declared feature.
        UnknownFeatureError: If ``strict`` and a row carries an undeclared
            one.
        PredictionError: If the model answers nothing readable.
        NativeError: If loading or inference fails.
    """
    if not rows:
        return []
    manifest = await self.manifest()
    vectors = [manifest.vector(row, strict=strict) for row in rows]

    model = await self.load()
    tensor = _float_tensor(vectors)
    outputs = await onnx_run(model.session_id, {model.input_name: tensor})
    return _read_outputs(outputs, manifest, len(rows))

parse

parse(data: bytes) -> CompactModel

Read compact model bytes into a :class:CompactModel.

Parameters:

Name Type Description Default
data bytes

The whole .tmc file.

required

Returns:

Type Description
CompactModel

The parsed model, arrays included.

Raises:

Type Description
CompactFormatError

If the magic bytes, the layout version, the kind/link, a section's length, or any number the header states about its own shape does not hold.

Source code in tempestweb/tabular/compact.py
def parse(data: bytes) -> CompactModel:
    """Read compact model bytes into a :class:`CompactModel`.

    Args:
        data: The whole ``.tmc`` file.

    Returns:
        The parsed model, arrays included.

    Raises:
        CompactFormatError: If the magic bytes, the layout version, the
            ``kind``/``link``, a section's length, or any number the header
            states about its own shape does not hold.
    """
    if data[: len(COMPACT_MAGIC)] != COMPACT_MAGIC:
        raise CompactFormatError(
            "not a compact model file (magic was "
            f"{data[: len(COMPACT_MAGIC)]!r}, expected {COMPACT_MAGIC!r})"
        )
    if len(data) < _PREFIX_LENGTH:
        raise CompactFormatError(
            f"the compact file is truncated: {len(data)} bytes, and the magic "
            f"plus the header length take {_PREFIX_LENGTH}"
        )
    (length,) = struct.unpack_from("<I", data, len(COMPACT_MAGIC))
    try:
        header = json.loads(
            data[_PREFIX_LENGTH : _PREFIX_LENGTH + length].decode("utf-8")
        )
    except (UnicodeDecodeError, json.JSONDecodeError) as error:
        raise CompactFormatError(f"the compact header is not JSON: {error}") from error
    if not isinstance(header, dict):
        raise CompactFormatError("the compact header is not a JSON object")

    version = header.get("schema_version")
    if version != COMPACT_SCHEMA_VERSION:
        raise CompactFormatError(
            f"compact schema {version} was written by another version of the "
            f"format; this reader implements {COMPACT_SCHEMA_VERSION}"
        )

    kind = str(header.get("kind", ""))
    if kind not in _KINDS:
        raise CompactFormatError(
            f"unsupported compact kind {kind!r}; this reader scores: "
            + ", ".join(sorted(_KINDS))
        )
    link = str(header.get("link", ""))
    if link not in _LINKS:
        raise CompactFormatError(
            f"unsupported compact link {link!r}; this reader applies: "
            + ", ".join(sorted(_LINKS))
        )

    sections = _sections(data, header, _PREFIX_LENGTH + length)
    preprocess = header.get("preprocess") or {}
    model = CompactModel(
        kind=kind,
        task=str(header.get("task", "")),
        link=link,
        classes=tuple(str(value) for value in header.get("classes", ())),
        n_features=int(header.get("n_features", 0)),
        n_outputs=int(header.get("n_outputs", 0)),
        n_trees=int(header.get("n_trees", 0)),
        estimator=str(header.get("estimator", "")),
        feature_names=tuple(str(name) for name in header.get("feature_names", ())),
        offset=tuple(float(value) for value in preprocess.get("offset", ())),
        scale=tuple(float(value) for value in preprocess.get("scale", ())),
        sections=sections,
    )
    _validate(model)
    return model

manifest_from_dict

manifest_from_dict(payload: Mapping[str, Any]) -> FeatureManifest

Read a manifest out of a decoded JSON object.

Parameters:

Name Type Description Default
payload Mapping[str, Any]

The decoded manifest.

required

Returns:

Name Type Description
The FeatureManifest

class:FeatureManifest.

Raises:

Type Description
ManifestError

If the payload is not a mapping, or its features is not a list of strings.

Source code in tempestweb/tabular/manifest.py
def manifest_from_dict(payload: Mapping[str, Any]) -> FeatureManifest:
    """Read a manifest out of a decoded JSON object.

    Args:
        payload: The decoded manifest.

    Returns:
        The :class:`FeatureManifest`.

    Raises:
        ManifestError: If the payload is not a mapping, or its ``features`` is
            not a list of strings.
    """
    if not isinstance(payload, Mapping):
        raise ManifestError("a manifest must be a JSON object")
    features = payload.get(FEATURES_KEY)
    if not isinstance(features, list) or not all(
        isinstance(name, str) for name in features
    ):
        raise ManifestError(
            f"{FEATURES_KEY!r} must be a list of strings naming the model's inputs"
        )
    return FeatureManifest(
        features=tuple(features),
        version=str(payload.get(VERSION_KEY, "")),
        outputs=tuple(_strings(payload.get(OUTPUTS_KEY))),
        classes=tuple(_strings(payload.get(CLASSES_KEY))),
    )

manifest_from_json

manifest_from_json(text: str) -> FeatureManifest

Read a manifest out of JSON text.

Parameters:

Name Type Description Default
text str

The manifest's JSON.

required

Returns:

Name Type Description
The FeatureManifest

class:FeatureManifest.

Raises:

Type Description
ManifestError

If the text is not valid JSON, or not a valid manifest.

Source code in tempestweb/tabular/manifest.py
def manifest_from_json(text: str) -> FeatureManifest:
    """Read a manifest out of JSON text.

    Args:
        text: The manifest's JSON.

    Returns:
        The :class:`FeatureManifest`.

    Raises:
        ManifestError: If the text is not valid JSON, or not a valid manifest.
    """
    try:
        payload = json.loads(text)
    except (TypeError, ValueError) as exc:
        raise ManifestError(f"the manifest is not valid JSON: {exc}") from exc
    return manifest_from_dict(payload)