Extension and Plugin Architecture

updatesupport keeps the core package domain-neutral. Domain-specific use cases can layer on their own metrics, Q presets, compilers, and report profiles through small extension packages.

The core package owns:

  • finite problem compilation

  • public/hidden support logic

  • built-in Q presets

  • row-metric and plugin protocols

  • sensitivity, refinement, and report machinery

Extension packages own domain vocabulary. For example, updatesupport-finance supplies financial model-risk metrics and report profiles without adding finance-specific concepts to core.

Installing Plugins

Install a plugin package directly:

# with pip
pip install updatesupport-finance

# with uv
uv add updatesupport-finance

Or install through a core extra:

# with pip
pip install "updatesupport[finance]"

# with uv
uv add "updatesupport[finance]"

Discovering Plugins

Plugins may register Python entry points under updatesupport.plugins.

import updatesupport as us

plugins = us.discover_plugins()
print([plugin.name for plugin in plugins])

Direct imports remain supported and are usually clearer in application code:

import updatesupport_finance as usf

metric = usf.expected_loss(pd="pd", lgd="lgd")

Plugin Descriptor

A plugin exposes an UpdateSupportPlugin descriptor:

import updatesupport as us

plugin = us.UpdateSupportPlugin(
    name="finance",
    version="0.1.0",
    description="Financial model-risk extensions for updatesupport.",
    metrics={"expected_loss": expected_loss},
    q_presets={"portfolio_mix_shift": q_portfolio_mix_shift},
    report_profiles={"model_risk": model_risk_report},
    compilers={"portfolio": from_portfolio},
    metadata=us.PluginMetadata(
        package="updatesupport-finance",
        homepage="https://github.com/nahuaque/updatesupport",
        domain="financial-model-risk",
        tags=("credit-risk", "expected-loss", "model-validation"),
        min_updatesupport_version="0.1.1",
    ),
)

The descriptor is intentionally lightweight. It gives discovery, namespacing, and lookup, while each plugin keeps its own user-facing API.

Plugin SDK Checks

Core validates plugin descriptors before registration. A valid plugin has a non-empty slug-like name, optional string metadata, and callable values in each surface map.

report = us.validate_plugin(plugin)
report.raise_for_errors()

print(plugin.as_dict())

Use this in plugin tests so packaging errors fail before release:

def test_plugin_contract():
    assert us.validate_plugin(plugin).ok

register_plugin(...) protects duplicate plugin names by default:

us.register_plugin(plugin)

# Only use replace=True intentionally, for example in an interactive notebook
# after reloading local plugin code.
us.register_plugin(plugin, replace=True)

Entry-point discovery uses the same validation and duplicate-name checks. A package that exposes a plugin entry point should keep the entry point name and the descriptor name aligned:

[project.entry-points."updatesupport.plugins"]
finance = "updatesupport_finance:plugin"

Row Metrics

Plugins can provide row-level target abstractions with RowMetric.

metric = us.row_metric(
    "expected_loss_rate",
    lambda row: row["pd"] * row["lgd"],
    columns=("pd", "lgd"),
    description="expected loss rate",
)

grouped = us.from_dataframe(
    rows,
    public=["product", "region"],
    hidden=["product", "region", "channel"],
    target=metric,
    weight="ead",
)

This compiles to the same GroupedProblem representation as a target column. The plugin only controls how the row-level target value is computed.

First Plugin: updatesupport-finance

updatesupport-finance provides:

  • expected_loss(...)

  • expected_loss_amount(...)

  • default_rate(...)

  • loss_given_default(...)

  • finance_sensitivity_grid(...)

  • from_portfolio(...)

  • model_risk_report(...)

  • certify_portfolio_segmentation(...)

  • q_portfolio_mix_shift(...)

  • q_exposure_weighted_tv(...)

  • q_factor_exposure_shift(...)

  • q_regional_concentration_shift(...)

Example:

import updatesupport_finance as usf

report = usf.model_risk_report(
    portfolio,
    public=["product", "region", "fico_band", "ltv_band"],
    hidden=[
        "product",
        "region",
        "fico_band",
        "ltv_band",
        "broker_channel",
        "employment_type",
        "vintage",
    ],
    metric=usf.expected_loss(pd="pd", lgd="lgd"),
    exposure="ead",
    candidate_refinements=["broker_channel", "employment_type", "vintage"],
    q="saturated",
)

print(report.to_markdown())

The finance plugin intentionally leads with saturated fixed-public-law stress because it is the easiest model-risk control to explain: it requires the public segmentation, retained internal refinements, and supplied metric, but no hand-chosen radius. Radius-based mix, TV, factor-exposure, and regional concentration presets are better treated as sensitivity scenarios with a documented governance rationale.